// SPDX-License-Identifier: AGPL-3.0-or-later export const VOICE_GRID_TILE_ASPECT_RATIO = 16 / 9; export const VOICE_GRID_COLUMN_RULES = [ {columns: 4, minTileCount: 10, minWidth: 1180, minHeight: 460}, {columns: 3, minTileCount: 5, minWidth: 860, minHeight: 360}, {columns: 2, minTileCount: 2, minWidth: 520, minHeight: 260}, ] as const; export const VOICE_GRID_DEFAULT_GAP_PX = 12; export const VOICE_GRID_DEFAULT_SIDE_PADDING_PX = 12; export const VOICE_GRID_DEFAULT_VERTICAL_PADDING_PX = 14; export const VOICE_GRID_MIN_TILE_WIDTH_PX = 220; export const VOICE_GRID_MIN_TILE_HEIGHT_PX = VOICE_GRID_MIN_TILE_WIDTH_PX / VOICE_GRID_TILE_ASPECT_RATIO; export const VOICE_GRID_COMPACT_MIN_TILE_WIDTH_PX = 148; export const VOICE_GRID_COMPACT_MIN_TILE_HEIGHT_PX = VOICE_GRID_COMPACT_MIN_TILE_WIDTH_PX / VOICE_GRID_TILE_ASPECT_RATIO; type VoiceGridRowVariable = | '--voice-grid-rows-1' | '--voice-grid-rows-2' | '--voice-grid-rows-3' | '--voice-grid-rows-4'; export type VoiceGridRowStyle = Record; export interface VoiceGridLayoutMetricsInput { tileCount: number; containerWidth: number; containerHeight: number; compact?: boolean; edgeToEdge?: boolean; } export interface VoiceGridVisibleTileCapacityInput extends VoiceGridLayoutMetricsInput { minTileWidth?: number; minTileHeight?: number; } interface VoiceGridLayoutMetricsForColumnsInput extends VoiceGridLayoutMetricsInput { columns: number; } export interface VoiceGridLayoutMetrics { columns: number; rows: number; gap: number; sidePadding: number; verticalPadding: number; availableWidth: number; availableHeight: number; tileWidth: number; tileHeight: number; contentWidth: number; contentHeight: number; } export interface VoiceGridPackedLayoutMetrics extends VoiceGridLayoutMetrics { visibleTileCount: number; } function resolveVoiceGridEmptyLayoutMetrics({ tileCount, containerWidth, containerHeight, compact = false, edgeToEdge = false, }: VoiceGridLayoutMetricsInput): VoiceGridLayoutMetrics { const count = sanitizeCount(tileCount); const width = sanitizeDimension(containerWidth); const height = sanitizeDimension(containerHeight); const gap = getVoiceGridGap({tileCount: count, compact, containerHeight: height}); const {sidePadding, verticalPadding} = getVoiceGridPadding({ containerWidth: width, containerHeight: height, compact, edgeToEdge, }); const availableWidth = Math.max(0, width - sidePadding * 2); const availableHeight = Math.max(0, height - verticalPadding * 2); return { columns: 1, rows: 1, gap, sidePadding, verticalPadding, availableWidth, availableHeight, tileWidth: 0, tileHeight: 0, contentWidth: sidePadding * 2, contentHeight: verticalPadding * 2, }; } function sanitizeCount(tileCount: number): number { if (!Number.isFinite(tileCount)) return 0; return Math.max(0, Math.floor(tileCount)); } function sanitizeDimension(value: number): number { if (!Number.isFinite(value)) return 0; return Math.max(0, value); } function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); } export function getVoiceGridRowCount(tileCount: number, columnCount: number): number { return Math.max(1, Math.ceil(sanitizeCount(tileCount) / Math.max(1, Math.floor(columnCount)))); } export function getVoiceGridRowsByColumnCount(tileCount: number): {1: number; 2: number; 3: number; 4: number} { return { 1: getVoiceGridRowCount(tileCount, 1), 2: getVoiceGridRowCount(tileCount, 2), 3: getVoiceGridRowCount(tileCount, 3), 4: getVoiceGridRowCount(tileCount, 4), }; } export function getVoiceGridRowStyle(tileCount: number): VoiceGridRowStyle { const rows = getVoiceGridRowsByColumnCount(tileCount); return { '--voice-grid-rows-1': `${rows[1]}`, '--voice-grid-rows-2': `${rows[2]}`, '--voice-grid-rows-3': `${rows[3]}`, '--voice-grid-rows-4': `${rows[4]}`, }; } export function getVoiceGridColumnCount({ tileCount, containerWidth, containerHeight, }: Pick): number { const count = sanitizeCount(tileCount); const width = sanitizeDimension(containerWidth); const height = sanitizeDimension(containerHeight); for (const rule of VOICE_GRID_COLUMN_RULES) { if (count >= rule.minTileCount && width >= rule.minWidth && height >= rule.minHeight) { return rule.columns; } } return 1; } export function getVoiceGridGap({ tileCount, compact = false, containerHeight, }: Pick): number { const count = sanitizeCount(tileCount); if (count >= 40) return 4; if (compact && count >= 24) return 5; if (count >= 24) return 6; if (compact && count >= 12) return 6; if (count >= 12) return 8; if (count >= 6) return 10; if (compact) return clamp(sanitizeDimension(containerHeight) * 0.024, 6, 10); return VOICE_GRID_DEFAULT_GAP_PX; } export function getVoiceGridPadding({ containerWidth, containerHeight, compact = false, edgeToEdge = false, }: Pick): { sidePadding: number; verticalPadding: number; } { if (edgeToEdge) { return {sidePadding: 0, verticalPadding: 0}; } const width = sanitizeDimension(containerWidth); const height = sanitizeDimension(containerHeight); const sidePadding = compact ? clamp(height * 0.02, 6, 12) : width <= 419 ? 6 : width <= 759 ? 8 : VOICE_GRID_DEFAULT_SIDE_PADDING_PX; const verticalPadding = compact ? clamp(height * 0.018, 5, 10) : height <= 359 ? 8 : height <= 519 ? 10 : VOICE_GRID_DEFAULT_VERTICAL_PADDING_PX; return {sidePadding, verticalPadding}; } export function getVoiceGridMinTileSize(compact = false): {minTileWidth: number; minTileHeight: number} { return compact ? {minTileWidth: VOICE_GRID_COMPACT_MIN_TILE_WIDTH_PX, minTileHeight: VOICE_GRID_COMPACT_MIN_TILE_HEIGHT_PX} : {minTileWidth: VOICE_GRID_MIN_TILE_WIDTH_PX, minTileHeight: VOICE_GRID_MIN_TILE_HEIGHT_PX}; } function resolveVoiceGridLayoutMetricsForColumns({ tileCount, containerWidth, containerHeight, compact = false, edgeToEdge = false, columns, }: VoiceGridLayoutMetricsForColumnsInput): VoiceGridLayoutMetrics { const count = sanitizeCount(tileCount); if (count <= 0) { return resolveVoiceGridEmptyLayoutMetrics({ tileCount: count, containerWidth, containerHeight, compact, edgeToEdge, }); } const width = sanitizeDimension(containerWidth); const height = sanitizeDimension(containerHeight); const resolvedColumns = Math.max(1, Math.min(Math.max(1, count), Math.floor(columns))); const rows = getVoiceGridRowCount(count, resolvedColumns); const gap = getVoiceGridGap({tileCount: count, compact, containerHeight: height}); const {sidePadding, verticalPadding} = getVoiceGridPadding({ containerWidth: width, containerHeight: height, compact, edgeToEdge, }); const availableWidth = Math.max(0, width - sidePadding * 2); const availableHeight = Math.max(0, height - verticalPadding * 2); const columnWidth = Math.max(0, (availableWidth - gap * Math.max(0, resolvedColumns - 1)) / resolvedColumns); const rowHeight = Math.max(0, (availableHeight - gap * Math.max(0, rows - 1)) / rows); const tileWidth = Math.min(columnWidth, rowHeight * VOICE_GRID_TILE_ASPECT_RATIO); const tileHeight = tileWidth / VOICE_GRID_TILE_ASPECT_RATIO; const contentWidth = sidePadding * 2 + tileWidth * Math.min(count, resolvedColumns) + gap * Math.max(0, Math.min(count, resolvedColumns) - 1); const contentHeight = verticalPadding * 2 + tileHeight * rows + gap * Math.max(0, rows - 1); return { columns: resolvedColumns, rows, gap, sidePadding, verticalPadding, availableWidth, availableHeight, tileWidth, tileHeight, contentWidth, contentHeight, }; } export function resolveVoiceGridLayoutMetrics({ tileCount, containerWidth, containerHeight, compact = false, edgeToEdge = false, }: VoiceGridLayoutMetricsInput): VoiceGridLayoutMetrics { const count = sanitizeCount(tileCount); const width = sanitizeDimension(containerWidth); const height = sanitizeDimension(containerHeight); const columns = getVoiceGridColumnCount({tileCount: count, containerWidth: width, containerHeight: height}); return resolveVoiceGridLayoutMetricsForColumns({ tileCount: count, containerWidth: width, containerHeight: height, compact, edgeToEdge, columns, }); } export function resolveVoiceGridPackedLayoutMetrics({ tileCount, containerWidth, containerHeight, compact = false, edgeToEdge = false, minTileWidth, minTileHeight, }: VoiceGridVisibleTileCapacityInput): VoiceGridPackedLayoutMetrics { const count = sanitizeCount(tileCount); const width = sanitizeDimension(containerWidth); const height = sanitizeDimension(containerHeight); if (count <= 0) { return { ...resolveVoiceGridEmptyLayoutMetrics({ tileCount: 0, containerWidth: width, containerHeight: height, compact, edgeToEdge, }), visibleTileCount: 0, }; } const minSize = getVoiceGridMinTileSize(compact); const effectiveMinTileWidth = sanitizeDimension(minTileWidth ?? minSize.minTileWidth); const effectiveMinTileHeight = sanitizeDimension(minTileHeight ?? minSize.minTileHeight); for (let visibleCount = count; visibleCount >= 1; visibleCount--) { let bestMetrics: VoiceGridLayoutMetrics | null = null; for (let columns = 1; columns <= visibleCount; columns++) { const metrics = resolveVoiceGridLayoutMetricsForColumns({ tileCount: visibleCount, containerWidth: width, containerHeight: height, compact, edgeToEdge, columns, }); if (metrics.tileWidth < effectiveMinTileWidth || metrics.tileHeight < effectiveMinTileHeight) continue; if (!bestMetrics || metrics.tileWidth * metrics.tileHeight > bestMetrics.tileWidth * bestMetrics.tileHeight) { bestMetrics = metrics; } } if (bestMetrics) { return {...bestMetrics, visibleTileCount: visibleCount}; } } return { ...resolveVoiceGridEmptyLayoutMetrics({ tileCount: count, containerWidth: width, containerHeight: height, compact, edgeToEdge, }), visibleTileCount: 0, }; } export function getVoiceGridVisibleTileCapacity(input: VoiceGridVisibleTileCapacityInput): number { return resolveVoiceGridPackedLayoutMetrics(input).visibleTileCount; }