export interface LayoutOptions { aspect: number; count: number; logoScale: number; sizeVariation: number; spacing: number; rotation: number; seed: number; } export const PLACEMENT_STRIDE = 4; const SIZE_BANDS = [ [0.12, 0.18, 0.259], [0.42, 0.13, 0.19], [0.84, 0.084, 0.141], [1, 0.054, 0.09], ] as const; class Random { private state: number; constructor(seed: number) { this.state = seed >>> 0 || 0x6d2b79f5; } next(): number { let value = (this.state += 0x6d2b79f5); value = Math.imul(value ^ (value >>> 15), value | 1); value ^= value + Math.imul(value ^ (value >>> 7), value | 61); return ((value ^ (value >>> 14)) >>> 0) / 4294967296; } } function pickWidth(random: Random, scale: number, variation: number): number { const roll = random.next(); for (const [threshold, low, high] of SIZE_BANDS) { if (roll <= threshold) { const sampledWidth = low + random.next() * (high - low); const uniformWidth = 0.115; return Math.max(0.025, uniformWidth + (sampledWidth - uniformWidth) * variation) * scale; } } return 0.07 * scale; } function cellIndex(value: number, cellSize: number, limit: number): number { return Math.max(0, Math.min(limit - 1, Math.floor(value / cellSize))); } export function generateLayout(options: LayoutOptions): Float32Array { const random = new Random(options.seed); const count = Math.max(1, Math.floor(options.count)); const width = Math.max(0.5, options.aspect); const height = 1; const widths = new Float32Array(count); for (let index = 0; index < count; index += 1) { widths[index] = pickWidth(random, options.logoScale, Math.max(0, options.sizeVariation)); } widths.sort(); widths.reverse(); const maxRadius = (widths[0] ?? 0.1) * 0.49; const minimumGap = 0.0037; const cellSize = Math.max(0.025, maxRadius * 1.7 + minimumGap); const gridWidth = Math.max(1, Math.ceil(width / cellSize)); const gridHeight = Math.max(1, Math.ceil(height / cellSize)); const grid: number[][] = Array.from({ length: gridWidth * gridHeight }, () => []); const output = new Float32Array(count * PLACEMENT_STRIDE); const neighborScore = (x: number, y: number, radius: number): number => { const reach = Math.ceil((radius + maxRadius + minimumGap) / cellSize); const centerX = cellIndex(x, cellSize, gridWidth); const centerY = cellIndex(y, cellSize, gridHeight); let closest = Number.POSITIVE_INFINITY; for (let gridY = Math.max(0, centerY - reach); gridY <= Math.min(gridHeight - 1, centerY + reach); gridY += 1) { for (let gridX = Math.max(0, centerX - reach); gridX <= Math.min(gridWidth - 1, centerX + reach); gridX += 1) { const bucket = grid[gridY * gridWidth + gridX]; if (!bucket) continue; for (const other of bucket) { const offset = other * PLACEMENT_STRIDE; const dx = x - (output[offset] ?? 0); const dy = y - (output[offset + 1] ?? 0); const otherRadius = (output[offset + 2] ?? 0) * 0.49; closest = Math.min(closest, Math.hypot(dx, dy) - radius - otherRadius); } } } return closest; }; for (let index = 0; index < count; index += 1) { const logoWidth = widths[index] ?? 0.08; const radius = logoWidth * 0.49; const bleed = radius * 0.34; let bestX = width * 0.5; let bestY = height * 0.5; let bestScore = Number.NEGATIVE_INFINITY; const attempts = index < 24 ? 42 : index < 200 ? 30 : 18; for (let attempt = 0; attempt < attempts; attempt += 1) { const x = -bleed + random.next() * (width + bleed * 2); const y = -bleed + random.next() * (height + bleed * 2); const gap = neighborScore(x, y, radius); const edgeDistance = Math.min(x + radius, width - x + radius, y + radius, height - y + radius); const score = gap + Math.min(0, edgeDistance) * 0.65; if (score > bestScore) { bestScore = score; bestX = x; bestY = y; } } const triangular = random.next() + random.next() - 1; const rawAngle = triangular * options.rotation; const angle = rawAngle > 0 ? rawAngle * 1.35 : rawAngle * 0.72; const offset = index * PLACEMENT_STRIDE; output[offset] = bestX; output[offset + 1] = bestY; output[offset + 2] = logoWidth; output[offset + 3] = angle; const gridX = cellIndex(bestX, cellSize, gridWidth); const gridY = cellIndex(bestY, cellSize, gridHeight); grid[gridY * gridWidth + gridX]?.push(index); } // Morph one stable layout continuously instead of reseeding or switching // between discrete placement modes as the spacing slider moves. const spacingOffset = options.spacing - 1; const spread = 1 + spacingOffset * 0.055; const logoSpacingScale = 1 / (1 + spacingOffset * 0.28); for (let offset = 0; offset < output.length; offset += PLACEMENT_STRIDE) { output[offset] = width * 0.5 + ((output[offset] ?? width * 0.5) - width * 0.5) * spread; output[offset + 1] = 0.5 + ((output[offset + 1] ?? 0.5) - 0.5) * spread; output[offset + 2] = (output[offset + 2] ?? 0.08) * logoSpacingScale; } return output; }