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;
}