PR-C1: adaptive video grid sizing (foundation) #2
2 changed files with 30 additions and 28 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue