diff --git a/src/app/call.ts b/src/app/call.ts index 84ec1960..caaff464 100644 --- a/src/app/call.ts +++ b/src/app/call.ts @@ -62,13 +62,12 @@ export type TileRow = { } /** - * Adaptive tile grid: rows can have different tile sizes, allowing - * a Jitsi-style layout where tiles flex between 3:4 and 16:9 and - * partial rows use wider tiles to fill whitespace. + * Adaptive tile grid: all tiles share the same dimensions. Full rows + * fill the container width; partial rows are centered. * * Example (3 tiles, 2 columns): * row 0: [ square ] [ square ] - * row 1: [ 16:9 ] (centered) + * row 1: [ square ] (centered, same size) */ export type AdaptiveTileGrid = { rows: TileRow[] @@ -112,10 +111,26 @@ const compareScores = (a: LayoutScore, b: LayoutScore): number => { return 0 } +/** + * Compute the largest tile size that fits within a given width and height, + * bounded by [minAspect, TILE_ASPECT_LANDSCAPE]. The tile is shrunk to fit + * whichever dimension is more constraining, so it never overflows. + */ +const fitTile = (availWidth: number, availHeight: number, minAspect: number) => { + const fillAspect = availWidth / availHeight + const aspectRatio = Math.max(minAspect, Math.min(TILE_ASPECT_LANDSCAPE, fillAspect)) + if (fillAspect >= aspectRatio) { + const tileWidth = availHeight * aspectRatio + return {tileWidth, tileHeight: availHeight, aspectRatio} + } + const tileHeight = availWidth / aspectRatio + return {tileWidth: availWidth, tileHeight, aspectRatio} +} + /** * Build a single-row TileRow for the uniform case where all rows - * have the same number of tiles. No upper-bound clamping on aspect - * ratio — tiles can be wider than 16:9 to avoid overflow. + * have the same number of tiles. Tiles are centered within the row + * and capped at 16:9 to avoid overflow on ultrawide containers. */ const buildUniformRow = ( columnCount: number, @@ -125,26 +140,16 @@ const buildUniformRow = ( minAspect: number, ): TileRow => { const gap = TILE_GAP - const availableHeight = containerHeight - (rowCount - 1) * gap - const rowHeight = availableHeight / rowCount - const tileWidth = (containerWidth - (columnCount - 1) * gap) / columnCount - const fillAspect = tileWidth / rowHeight - const aspectRatio = Math.max(minAspect, Math.min(TILE_ASPECT_LANDSCAPE, fillAspect)) - const tileHeight = tileWidth / aspectRatio + const availHeight = (containerHeight - (rowCount - 1) * gap) / rowCount + const availWidth = (containerWidth - (columnCount - 1) * gap) / columnCount + const {tileWidth, tileHeight, aspectRatio} = fitTile(availWidth, availHeight, minAspect) return {columnCount, tileWidth, tileHeight, aspectRatio} } /** * Build a mixed-size layout candidate for a given column count. - * Uses a "match heights" approach: the last (partial) row is at 16:9 - * (per review #1: "single centered 16:9 tile") and full-row tiles - * match that same height. This creates a cohesive Jitsi-style look - * where all tiles in all rows are the same height, with different - * widths per row (full-row tiles are narrower/squarer, last-row - * tiles are wider). - * - * If the full-row aspect would go below the portrait bound (3:4), - * the height diverges: full rows clamp to 3:4 portrait. + * All tiles share the same dimensions — full rows fill the width, + * and the partial last row is centered with the same tile size. */ const buildMixedCandidate = ( tileCount: number, @@ -158,48 +163,18 @@ const buildMixedCandidate = ( const remainder = tileCount % columnCount const totalRowCount = fullRowCount + 1 - const tileWidthFull = (containerWidth - (columnCount - 1) * gap) / columnCount - if (tileWidthFull <= 0) return undefined - - const tileWidthLast = (containerWidth - (remainder - 1) * gap) / remainder - if (tileWidthLast <= 0) return undefined - - const maxPossibleHeight = Math.min( - tileWidthFull / minAspect, - tileWidthLast / minAspect, - (containerHeight - (totalRowCount - 1) * gap) / totalRowCount, - ) - - // Target 16:9 for the last row if possible - const targetHeight = tileWidthLast / TILE_ASPECT_LANDSCAPE - const commonHeight = Math.min(maxPossibleHeight, targetHeight) - - const tileHeightFull = commonHeight - const tileHeightLast = commonHeight - const fullRowAspectRatio = tileWidthFull / commonHeight - const lastRowAspectRatio = tileWidthLast / commonHeight - - const totalHeight = fullRowCount * tileHeightFull + tileHeightLast + (totalRowCount - 1) * gap - const totalWidth = Math.max( - columnCount * tileWidthFull + (columnCount - 1) * gap, - remainder * tileWidthLast + (remainder - 1) * gap, - ) + const availHeight = (containerHeight - (totalRowCount - 1) * gap) / totalRowCount + const availWidth = (containerWidth - (columnCount - 1) * gap) / columnCount + const {tileWidth, tileHeight, aspectRatio} = fitTile(availWidth, availHeight, minAspect) const rows: TileRow[] = [] for (let r = 0; r < fullRowCount; r++) { - rows.push({ - columnCount, - tileWidth: tileWidthFull, - tileHeight: tileHeightFull, - aspectRatio: fullRowAspectRatio, - }) + rows.push({columnCount, tileWidth, tileHeight, aspectRatio}) } - rows.push({ - columnCount: remainder, - tileWidth: tileWidthLast, - tileHeight: tileHeightLast, - aspectRatio: lastRowAspectRatio, - }) + rows.push({columnCount: remainder, tileWidth, tileHeight, aspectRatio}) + + const totalHeight = totalRowCount * tileHeight + (totalRowCount - 1) * gap + const totalWidth = columnCount * tileWidth + (columnCount - 1) * gap return { rows, @@ -214,12 +189,12 @@ const buildMixedCandidate = ( * * - Tiles flex between 3:4 (portrait) and wider than 16:9 if needed * to avoid overflow. No upper bound on aspect ratio — tiles can - * be wider than 16:9 when the container is very wide. - * - When the last row is partial (fewer tiles than a full row), - * those tiles are wider (16:9) while full rows use square-ish - * tiles, filling whitespace per review #1. - * - Only allows overflow (scroll) when tiles would be below - * MIN_TILE_HEIGHT (review #2: ~6-8 tiles on portrait phone). + * Tiles flex between a minimum aspect (1:1 on landscape, 3:4 on portrait) + * and 16:9, capped so they never overflow the container. Partial rows + * use the same tile size as full rows, centered. + * + * Only allows overflow (scroll) when tiles would be below + * MIN_TILE_HEIGHT (review #2: ~6-8 tiles on portrait phone). * * Prioritises: * 1. No overflow (tiles shrink to fit within aspect bounds) @@ -234,10 +209,10 @@ export const computeAdaptiveGrid = ( ): AdaptiveTileGrid | undefined => { if (tileCount <= 0 || containerWidth <= 0 || containerHeight <= 0) return undefined - // On landscape viewports, bound tiles at 1:1 (square) so they don't - // become tall and narrow. On portrait viewports, allow 3:4 so more - // tiles fit on a phone screen. - const minAspect = containerWidth > containerHeight ? 1 : TILE_ASPECT_PORTRAIT + // When the container is at least 4:3 landscape, bound tiles at 1:1 (square) + // so they don't become tall and narrow. On narrower/portrait viewports, + // allow 3:4 so more tiles fit on a phone screen. + const minAspect = containerWidth / containerHeight >= 4 / 3 ? 1 : TILE_ASPECT_PORTRAIT let best: AdaptiveTileGrid | undefined let bestScore: LayoutScore | undefined