fix: grid algorithm — 4:3 breakpoint, 16:9 cap, uniform tile sizes
- Lower landscape breakpoint from width>height to width/height>=4:3 - fitTile helper caps tiles at 16:9 and shrinks to fit both dimensions, preventing overflow on ultrawide containers - Mixed candidate uses uniform tile sizes for all rows; partial rows are centered with the same tile dimensions as full rows
This commit is contained in:
parent
f099e97981
commit
13f9a5c13d
1 changed files with 44 additions and 69 deletions
113
src/app/call.ts
113
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue