File size: 5,169 Bytes
5aed101 3a2fcd3 5aed101 3a2fcd3 5aed101 3a2fcd3 5aed101 3a2fcd3 5aed101 c0515fc 5aed101 7bd60d9 5aed101 c0515fc 5aed101 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 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;
}
|