fix: mixed-row rendering bug and Jitsi-style algorithm refinements

This commit is contained in:
Agent 2026-08-12 09:49:24 -04:00
parent 90b9bda329
commit 0d9e511f41
2 changed files with 30 additions and 28 deletions

View file

@ -85,6 +85,8 @@ export type AdaptiveTileGrid = {
type LayoutScore = {
/** Penalty for vertical overflow: 0 if none, else huge */
verticalOverflowPenalty: number
/** Penalty for horizontal overflow: 0 if none, else huge */
horizontalOverflowPenalty: number
/** Penalty when tiles fall below MIN_TILE_HEIGHT even while fitting */
minTileHeightPenalty: number
/** Negative tile area (we want to maximize area, so negate) */
@ -98,6 +100,7 @@ type LayoutScore = {
const compareScores = (a: LayoutScore, b: LayoutScore): number => {
const fields: (keyof LayoutScore)[] = [
"verticalOverflowPenalty",
"horizontalOverflowPenalty",
"minTileHeightPenalty",
"negativeArea",
"whitespace",
@ -125,8 +128,8 @@ const buildUniformRow = (
const rowHeight = availableHeight / rowCount
const tileWidth = (containerWidth - (columnCount - 1) * gap) / columnCount
const fillAspect = tileWidth / rowHeight
// No upper bound — allow wider than 16:9 to avoid overflow
const aspectRatio = Math.max(TILE_ASPECT_PORTRAIT, fillAspect)
// Cap at 16:9 to avoid horizontally stretching video (review feedback)
const aspectRatio = Math.max(TILE_ASPECT_PORTRAIT, Math.min(TILE_ASPECT_LANDSCAPE, fillAspect))
const tileHeight = tileWidth / aspectRatio
return {columnCount, tileWidth, tileHeight, aspectRatio}
}
@ -160,31 +163,21 @@ const buildMixedCandidate = (
const tileWidthLast = (containerWidth - (remainder - 1) * gap) / remainder
if (tileWidthLast <= 0) return undefined
// Last row at 16:9 (widest aspect, per review: "single centered 16:9 tile")
const lastRowAspect = TILE_ASPECT_LANDSCAPE
const commonHeight = tileWidthLast / lastRowAspect
// Find a common height that fits both row types and container
const maxPossibleHeight = Math.min(
tileWidthFull / TILE_ASPECT_PORTRAIT,
tileWidthLast / TILE_ASPECT_PORTRAIT,
(containerHeight - (totalRowCount - 1) * gap) / totalRowCount,
)
// Full-row tile that matches the common height
const fullRowAspect = tileWidthFull / commonHeight
// Target 16:9 for the last row if possible
const targetHeight = tileWidthLast / TILE_ASPECT_LANDSCAPE
const commonHeight = Math.min(maxPossibleHeight, targetHeight)
let tileHeightFull: number
let tileHeightLast: number
let fullRowAspectRatio: number
let lastRowAspectRatio: number
if (fullRowAspect >= TILE_ASPECT_PORTRAIT) {
// Full-row aspect is within bounds — match heights cleanly
tileHeightFull = commonHeight
tileHeightLast = commonHeight
fullRowAspectRatio = fullRowAspect
lastRowAspectRatio = lastRowAspect
} else {
// Full rows would be too portrait — clamp to portrait bound
fullRowAspectRatio = TILE_ASPECT_PORTRAIT
tileHeightFull = tileWidthFull / fullRowAspectRatio
tileHeightLast = commonHeight
lastRowAspectRatio = lastRowAspect
}
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(
@ -293,11 +286,16 @@ export const computeAdaptiveGrid = (
// unacceptable — heavily penalize. If tiles are below MIN_TILE_HEIGHT,
// overflow is acceptable (tiles are already too small to shrink further).
const minTileHeightInCandidate = Math.min(...candidate.rows.map(r => r.tileHeight))
const overflowPenalty =
const verticalOverflowPenalty =
verticalOverflow > 0 && minTileHeightInCandidate >= MIN_TILE_HEIGHT
? verticalOverflow * 1_000_000
: verticalOverflow
const horizontalOverflowPenalty =
horizontalOverflow > 0 && minTileHeightInCandidate >= MIN_TILE_HEIGHT
? horizontalOverflow * 1_000_000
: horizontalOverflow
// Min-tile-height penalty: even when fitting, discourage layouts
// where tiles are below the minimum acceptable size.
const minTileHeightPenalty =
@ -306,7 +304,8 @@ export const computeAdaptiveGrid = (
: 0
const score: LayoutScore = {
verticalOverflowPenalty: overflowPenalty,
verticalOverflowPenalty,
horizontalOverflowPenalty,
minTileHeightPenalty,
negativeArea: -totalArea,
whitespace,

View file

@ -296,10 +296,13 @@
{#if tileGrid}
<div class="flex flex-col items-center gap-2">
{#each tileGrid.rows as row, rowIndex}
{@const offset = tileGrid.rows
.slice(0, rowIndex)
.reduce((sum, r) => sum + r.columnCount, 0)}
<div
class="flex flex-wrap justify-center gap-2"
style={`max-width: ${row.columnCount * row.tileWidth + (row.columnCount - 1) * 8}px`}>
{#each videoTiles.slice(rowIndex * row.columnCount, rowIndex * row.columnCount + row.columnCount) as tile (tileKey(tile))}
{#each videoTiles.slice(offset, offset + row.columnCount) as tile (tileKey(tile))}
<div
class="overflow-hidden rounded-2xl shrink-0"
style={`width: ${row.tileWidth}px; height: ${row.tileHeight}px`}>