git-hash-art 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.js CHANGED
@@ -62,6 +62,136 @@ const $461134e0b6ce0619$export$bb9e4790bc99ae59 = {
62
62
  PI: Math.PI,
63
63
  PHI: (1 + Math.sqrt(5)) / 2
64
64
  };
65
+ function $461134e0b6ce0619$export$bbde7fbaaf9a8d66(rng) {
66
+ // Build a deterministic permutation table (256 entries, doubled)
67
+ const perm = new Uint8Array(512);
68
+ const p = new Uint8Array(256);
69
+ for(let i = 0; i < 256; i++)p[i] = i;
70
+ // Fisher-Yates shuffle with our seeded RNG
71
+ for(let i = 255; i > 0; i--){
72
+ const j = Math.floor(rng() * (i + 1));
73
+ const tmp = p[i];
74
+ p[i] = p[j];
75
+ p[j] = tmp;
76
+ }
77
+ for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
78
+ // 12 gradient vectors for 2D simplex
79
+ const GRAD2 = [
80
+ [
81
+ 1,
82
+ 1
83
+ ],
84
+ [
85
+ -1,
86
+ 1
87
+ ],
88
+ [
89
+ 1,
90
+ -1
91
+ ],
92
+ [
93
+ -1,
94
+ -1
95
+ ],
96
+ [
97
+ 1,
98
+ 0
99
+ ],
100
+ [
101
+ -1,
102
+ 0
103
+ ],
104
+ [
105
+ 0,
106
+ 1
107
+ ],
108
+ [
109
+ 0,
110
+ -1
111
+ ],
112
+ [
113
+ 1,
114
+ 1
115
+ ],
116
+ [
117
+ -1,
118
+ 1
119
+ ],
120
+ [
121
+ 1,
122
+ -1
123
+ ],
124
+ [
125
+ -1,
126
+ -1
127
+ ]
128
+ ];
129
+ const F2 = 0.5 * (Math.sqrt(3) - 1);
130
+ const G2 = (3 - Math.sqrt(3)) / 6;
131
+ function dot2(g, x, y) {
132
+ return g[0] * x + g[1] * y;
133
+ }
134
+ return function noise2D(xin, yin) {
135
+ const s = (xin + yin) * F2;
136
+ const i = Math.floor(xin + s);
137
+ const j = Math.floor(yin + s);
138
+ const t = (i + j) * G2;
139
+ const X0 = i - t;
140
+ const Y0 = j - t;
141
+ const x0 = xin - X0;
142
+ const y0 = yin - Y0;
143
+ let i1, j1;
144
+ if (x0 > y0) {
145
+ i1 = 1;
146
+ j1 = 0;
147
+ } else {
148
+ i1 = 0;
149
+ j1 = 1;
150
+ }
151
+ const x1 = x0 - i1 + G2;
152
+ const y1 = y0 - j1 + G2;
153
+ const x2 = x0 - 1 + 2 * G2;
154
+ const y2 = y0 - 1 + 2 * G2;
155
+ const ii = i & 255;
156
+ const jj = j & 255;
157
+ let n0 = 0, n1 = 0, n2 = 0;
158
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
159
+ if (t0 >= 0) {
160
+ t0 *= t0;
161
+ const gi0 = perm[ii + perm[jj]] % 12;
162
+ n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
163
+ }
164
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
165
+ if (t1 >= 0) {
166
+ t1 *= t1;
167
+ const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
168
+ n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
169
+ }
170
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
171
+ if (t2 >= 0) {
172
+ t2 *= t2;
173
+ const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
174
+ n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
175
+ }
176
+ // Scale to approximately [-1, 1]
177
+ return 70 * (n0 + n1 + n2);
178
+ };
179
+ }
180
+ function $461134e0b6ce0619$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
181
+ return function fbm(x, y) {
182
+ let value = 0;
183
+ let amplitude = 1;
184
+ let frequency = 1;
185
+ let maxAmp = 0;
186
+ for(let i = 0; i < octaves; i++){
187
+ value += noise(x * frequency, y * frequency) * amplitude;
188
+ maxAmp += amplitude;
189
+ amplitude *= gain;
190
+ frequency *= lacunarity;
191
+ }
192
+ return value / maxAmp;
193
+ };
194
+ }
65
195
  class $461134e0b6ce0619$export$da2372f11bc66b3f {
66
196
  static getProportionalSize(baseSize, proportion) {
67
197
  return baseSize * proportion;
@@ -263,6 +393,48 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
263
393
  $9d614e7d77fc2947$var$hslToHex(baseHue, 0.7, 0.35)
264
394
  ];
265
395
  }
396
+ case "split-complementary":
397
+ {
398
+ // Base hue + two colors flanking the complement (±30°)
399
+ const comp = (baseHue + 180) % 360;
400
+ const split1 = (comp - 30 + 360) % 360;
401
+ const split2 = (comp + 30) % 360;
402
+ const sat = 0.55 + this.rng() * 0.25;
403
+ return [
404
+ $9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
405
+ $9d614e7d77fc2947$var$hslToHex(baseHue, sat * 0.8, 0.65),
406
+ $9d614e7d77fc2947$var$hslToHex(split1, sat, 0.5),
407
+ $9d614e7d77fc2947$var$hslToHex(split2, sat, 0.5),
408
+ $9d614e7d77fc2947$var$hslToHex(split1, sat * 0.7, 0.7)
409
+ ];
410
+ }
411
+ case "analogous-accent":
412
+ {
413
+ // Tight cluster of 3 analogous hues + 1 distant accent
414
+ const step = 15 + this.rng() * 20; // 15-35° apart
415
+ const h1 = (baseHue - step + 360) % 360;
416
+ const h2 = (baseHue + step) % 360;
417
+ const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
418
+ const sat = 0.5 + this.rng() * 0.3;
419
+ return [
420
+ $9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
421
+ $9d614e7d77fc2947$var$hslToHex(h1, sat, 0.55),
422
+ $9d614e7d77fc2947$var$hslToHex(h2, sat, 0.45),
423
+ $9d614e7d77fc2947$var$hslToHex(accentHue, sat + 0.15, 0.5)
424
+ ];
425
+ }
426
+ case "limited-palette":
427
+ {
428
+ // Only 3 colors — like a risograph print
429
+ const h2 = (baseHue + 120 + this.rng() * 40) % 360;
430
+ const h3 = (baseHue + 220 + this.rng() * 40) % 360;
431
+ const sat = 0.6 + this.rng() * 0.2;
432
+ return [
433
+ $9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
434
+ $9d614e7d77fc2947$var$hslToHex(h2, sat, 0.5),
435
+ $9d614e7d77fc2947$var$hslToHex(h3, sat * 0.9, 0.55)
436
+ ];
437
+ }
266
438
  case "harmonious":
267
439
  default:
268
440
  return this.getColors();
@@ -283,6 +455,14 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
283
455
  "#f5f5f0",
284
456
  "#e8e8e0"
285
457
  ];
458
+ case "split-complementary":
459
+ case "analogous-accent":
460
+ return this.getBackgroundColors();
461
+ case "limited-palette":
462
+ return [
463
+ $9d614e7d77fc2947$var$hslToHex(this.seed % 360, 0.08, 0.94),
464
+ $9d614e7d77fc2947$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
465
+ ];
286
466
  case "neon":
287
467
  return [
288
468
  "#0a0a12",
@@ -409,15 +589,17 @@ function $9d614e7d77fc2947$export$fabac4600b87056(colors, rng) {
409
589
  accent: colors[colors.length - 1] || "#888888",
410
590
  all: colors
411
591
  };
412
- // Pick dominant as the color closest to the palette's average hue
592
+ // Pick dominant as the color with the highest chroma (saturation × distance from gray)
593
+ // This selects the most visually prominent color rather than the average
413
594
  const hsls = colors.map((c)=>$9d614e7d77fc2947$var$hexToHsl(c));
414
- const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
415
595
  let dominantIdx = 0;
416
- let minDist = 360;
596
+ let maxChroma = -1;
417
597
  for(let i = 0; i < hsls.length; i++){
418
- const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
419
- if (d < minDist) {
420
- minDist = d;
598
+ // Chroma approximation: saturation × how far lightness is from 50% (gray)
599
+ const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
600
+ const chroma = hsls[i][1] * lightnessVibrancy;
601
+ if (chroma > maxChroma) {
602
+ maxChroma = chroma;
421
603
  dominantIdx = i;
422
604
  }
423
605
  }
@@ -536,7 +718,8 @@ function $9d614e7d77fc2947$export$703ba40a4347f77a(base, layerRatio, hueShiftPer
536
718
  return {
537
719
  dominant: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.dominant, shift),
538
720
  secondary: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
539
- accent: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
721
+ accent: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
722
+ all: base.all.map((c)=>$9d614e7d77fc2947$export$1793a1bfbe4f6ff5(c, shift * 0.6))
540
723
  };
541
724
  }
542
725
 
@@ -1919,6 +2102,23 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
1919
2102
  ctx.fill();
1920
2103
  ctx.fillStyle = origFill;
1921
2104
  ctx.restore();
2105
+ // Pass 4: Organic edge erosion — irregular bites along the boundary
2106
+ if (rng && size > 20) {
2107
+ const erosionBites = 6 + Math.floor(rng() * 8);
2108
+ const edgeRadius = size * 0.45;
2109
+ ctx.save();
2110
+ ctx.globalCompositeOperation = "destination-out";
2111
+ ctx.globalAlpha = 0.6 + rng() * 0.3;
2112
+ for(let eb = 0; eb < erosionBites; eb++){
2113
+ const biteAngle = rng() * Math.PI * 2;
2114
+ const biteDist = edgeRadius * (0.85 + rng() * 0.25);
2115
+ const biteR = size * (0.02 + rng() * 0.04);
2116
+ ctx.beginPath();
2117
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2118
+ ctx.fill();
2119
+ }
2120
+ ctx.restore();
2121
+ }
1922
2122
  ctx.globalAlpha = savedAlpha;
1923
2123
  // Soft stroke on top — thinner than normal for delicacy
1924
2124
  ctx.globalAlpha *= 0.25;
@@ -2228,6 +2428,23 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
2228
2428
  ctx.stroke();
2229
2429
  ctx.restore();
2230
2430
  }
2431
+ // Organic edge erosion — small irregular bites for rough paper feel
2432
+ if (rng && size > 20) {
2433
+ const erosionBites = 4 + Math.floor(rng() * 6);
2434
+ const edgeRadius = size * 0.42;
2435
+ ctx.save();
2436
+ ctx.globalCompositeOperation = "destination-out";
2437
+ ctx.globalAlpha = 0.5 + rng() * 0.3;
2438
+ for(let eb = 0; eb < erosionBites; eb++){
2439
+ const biteAngle = rng() * Math.PI * 2;
2440
+ const biteDist = edgeRadius * (0.9 + rng() * 0.2);
2441
+ const biteR = size * (0.015 + rng() * 0.03);
2442
+ ctx.beginPath();
2443
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2444
+ ctx.fill();
2445
+ }
2446
+ ctx.restore();
2447
+ }
2231
2448
  ctx.globalAlpha = savedAlphaHD;
2232
2449
  break;
2233
2450
  }
@@ -2239,12 +2456,20 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
2239
2456
  }
2240
2457
  }
2241
2458
  function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2242
- const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, patterns: patterns = [], proportionType: proportionType = "GOLDEN_RATIO", baseOpacity: baseOpacity = 0.6, opacityReduction: opacityReduction = 0.1, glowRadius: glowRadius = 0, glowColor: glowColor, gradientFillEnd: gradientFillEnd, renderStyle: renderStyle = "fill-and-stroke", rng: rng } = config;
2459
+ const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, patterns: patterns = [], proportionType: proportionType = "GOLDEN_RATIO", baseOpacity: baseOpacity = 0.6, opacityReduction: opacityReduction = 0.1, glowRadius: glowRadius = 0, glowColor: glowColor, gradientFillEnd: gradientFillEnd, renderStyle: renderStyle = "fill-and-stroke", rng: rng, lightAngle: lightAngle, scaleFactor: scaleFactor = 1 } = config;
2243
2460
  ctx.save();
2244
2461
  ctx.translate(x, y);
2245
2462
  ctx.rotate(rotation * Math.PI / 180);
2246
- // Glow / shadow effect
2247
- if (glowRadius > 0) {
2463
+ // ── Drop shadow — soft colored shadow offset along light direction ──
2464
+ if (lightAngle !== undefined && size > 10) {
2465
+ const shadowDist = size * 0.035;
2466
+ const shadowBlurR = size * 0.06;
2467
+ ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
2468
+ ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
2469
+ ctx.shadowBlur = shadowBlurR;
2470
+ ctx.shadowColor = "rgba(0,0,0,0.12)";
2471
+ } else if (glowRadius > 0) {
2472
+ // Glow / shadow effect (legacy path)
2248
2473
  ctx.shadowBlur = glowRadius;
2249
2474
  ctx.shadowColor = glowColor || fillColor;
2250
2475
  ctx.shadowOffsetX = 0;
@@ -2266,8 +2491,39 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2266
2491
  });
2267
2492
  $9beb8f41637c29fd$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
2268
2493
  }
2269
- // Reset shadow so patterns aren't double-glowed
2270
- if (glowRadius > 0) ctx.shadowBlur = 0;
2494
+ // Reset shadow so patterns and highlight aren't double-shadowed
2495
+ ctx.shadowBlur = 0;
2496
+ ctx.shadowOffsetX = 0;
2497
+ ctx.shadowOffsetY = 0;
2498
+ ctx.shadowColor = "transparent";
2499
+ // ── Specular highlight — tinted arc on the light-facing side ──
2500
+ if (lightAngle !== undefined && size > 15 && rng) {
2501
+ const hlRadius = size * 0.35;
2502
+ const hlDist = size * 0.15;
2503
+ const hlX = Math.cos(lightAngle) * hlDist;
2504
+ const hlY = Math.sin(lightAngle) * hlDist;
2505
+ const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2506
+ // Tint highlight warm/cool based on fill color for cohesion
2507
+ // Parse fill to detect warmth — fallback to white for non-parseable
2508
+ let hlBase = "255,255,255";
2509
+ if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
2510
+ const r = parseInt(fillColor.slice(1, 3), 16);
2511
+ const g = parseInt(fillColor.slice(3, 5), 16);
2512
+ const b = parseInt(fillColor.slice(5, 7), 16);
2513
+ // Blend toward white but keep a hint of the fill's warmth
2514
+ hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
2515
+ }
2516
+ hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
2517
+ hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
2518
+ hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
2519
+ const savedOp = ctx.globalCompositeOperation;
2520
+ ctx.globalCompositeOperation = "soft-light";
2521
+ ctx.fillStyle = hlGrad;
2522
+ ctx.beginPath();
2523
+ ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
2524
+ ctx.fill();
2525
+ ctx.globalCompositeOperation = savedOp;
2526
+ }
2271
2527
  // Layer additional patterns if specified
2272
2528
  if (patterns.length > 0) (0, $461134e0b6ce0619$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
2273
2529
  baseSize: size,
@@ -3321,6 +3577,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3321
3577
  "watercolor",
3322
3578
  "fill-only"
3323
3579
  ],
3580
+ preferredCompositions: [
3581
+ "clustered",
3582
+ "flow-field",
3583
+ "radial"
3584
+ ],
3324
3585
  flowLineMultiplier: 2.5,
3325
3586
  heroShape: false,
3326
3587
  glowMultiplier: 0.5,
@@ -3342,6 +3603,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3342
3603
  "stroke-only",
3343
3604
  "incomplete"
3344
3605
  ],
3606
+ preferredCompositions: [
3607
+ "golden-spiral",
3608
+ "grid-subdivision"
3609
+ ],
3345
3610
  flowLineMultiplier: 0.3,
3346
3611
  heroShape: true,
3347
3612
  glowMultiplier: 0,
@@ -3363,6 +3628,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3363
3628
  "fill-only",
3364
3629
  "incomplete"
3365
3630
  ],
3631
+ preferredCompositions: [
3632
+ "flow-field",
3633
+ "golden-spiral",
3634
+ "spiral"
3635
+ ],
3366
3636
  flowLineMultiplier: 4,
3367
3637
  heroShape: false,
3368
3638
  glowMultiplier: 0.3,
@@ -3385,6 +3655,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3385
3655
  "double-stroke",
3386
3656
  "hatched"
3387
3657
  ],
3658
+ preferredCompositions: [
3659
+ "grid-subdivision",
3660
+ "radial"
3661
+ ],
3388
3662
  flowLineMultiplier: 0,
3389
3663
  heroShape: false,
3390
3664
  glowMultiplier: 0,
@@ -3406,6 +3680,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3406
3680
  "incomplete",
3407
3681
  "fill-only"
3408
3682
  ],
3683
+ preferredCompositions: [
3684
+ "golden-spiral",
3685
+ "radial",
3686
+ "spiral"
3687
+ ],
3409
3688
  flowLineMultiplier: 1.5,
3410
3689
  heroShape: true,
3411
3690
  glowMultiplier: 2,
@@ -3426,6 +3705,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3426
3705
  "fill-and-stroke",
3427
3706
  "double-stroke"
3428
3707
  ],
3708
+ preferredCompositions: [
3709
+ "grid-subdivision",
3710
+ "golden-spiral"
3711
+ ],
3429
3712
  flowLineMultiplier: 0,
3430
3713
  heroShape: true,
3431
3714
  glowMultiplier: 0,
@@ -3447,6 +3730,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3447
3730
  "double-stroke",
3448
3731
  "dashed"
3449
3732
  ],
3733
+ preferredCompositions: [
3734
+ "radial",
3735
+ "spiral",
3736
+ "clustered"
3737
+ ],
3450
3738
  flowLineMultiplier: 2,
3451
3739
  heroShape: true,
3452
3740
  glowMultiplier: 3,
@@ -3469,6 +3757,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3469
3757
  "stroke-only",
3470
3758
  "dashed"
3471
3759
  ],
3760
+ preferredCompositions: [
3761
+ "flow-field",
3762
+ "grid-subdivision",
3763
+ "clustered"
3764
+ ],
3472
3765
  flowLineMultiplier: 1.5,
3473
3766
  heroShape: false,
3474
3767
  glowMultiplier: 0,
@@ -3490,6 +3783,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3490
3783
  "watercolor",
3491
3784
  "fill-and-stroke"
3492
3785
  ],
3786
+ preferredCompositions: [
3787
+ "radial",
3788
+ "spiral",
3789
+ "golden-spiral"
3790
+ ],
3493
3791
  flowLineMultiplier: 3,
3494
3792
  heroShape: true,
3495
3793
  glowMultiplier: 2.5,
@@ -3511,6 +3809,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3511
3809
  "fill-only",
3512
3810
  "incomplete"
3513
3811
  ],
3812
+ preferredCompositions: [
3813
+ "golden-spiral",
3814
+ "flow-field",
3815
+ "radial"
3816
+ ],
3514
3817
  flowLineMultiplier: 0.5,
3515
3818
  heroShape: false,
3516
3819
  glowMultiplier: 0.3,
@@ -3532,6 +3835,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3532
3835
  "stroke-only",
3533
3836
  "dashed"
3534
3837
  ],
3838
+ preferredCompositions: [
3839
+ "grid-subdivision",
3840
+ "radial"
3841
+ ],
3535
3842
  flowLineMultiplier: 0,
3536
3843
  heroShape: false,
3537
3844
  glowMultiplier: 0,
@@ -3553,6 +3860,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3553
3860
  "fill-only",
3554
3861
  "double-stroke"
3555
3862
  ],
3863
+ preferredCompositions: [
3864
+ "grid-subdivision",
3865
+ "clustered"
3866
+ ],
3556
3867
  flowLineMultiplier: 0,
3557
3868
  heroShape: true,
3558
3869
  glowMultiplier: 0,
@@ -3574,6 +3885,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3574
3885
  "watercolor",
3575
3886
  "fill-only"
3576
3887
  ],
3888
+ preferredCompositions: [
3889
+ "radial",
3890
+ "golden-spiral",
3891
+ "flow-field"
3892
+ ],
3577
3893
  flowLineMultiplier: 1,
3578
3894
  heroShape: true,
3579
3895
  glowMultiplier: 1,
@@ -3595,6 +3911,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3595
3911
  "stroke-only",
3596
3912
  "fill-only"
3597
3913
  ],
3914
+ preferredCompositions: [
3915
+ "clustered",
3916
+ "grid-subdivision",
3917
+ "radial"
3918
+ ],
3598
3919
  flowLineMultiplier: 0,
3599
3920
  heroShape: false,
3600
3921
  glowMultiplier: 0.3,
@@ -3616,6 +3937,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3616
3937
  "fill-only",
3617
3938
  "incomplete"
3618
3939
  ],
3940
+ preferredCompositions: [
3941
+ "flow-field",
3942
+ "golden-spiral",
3943
+ "spiral"
3944
+ ],
3619
3945
  flowLineMultiplier: 3,
3620
3946
  heroShape: true,
3621
3947
  glowMultiplier: 0.2,
@@ -3637,6 +3963,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3637
3963
  "fill-only",
3638
3964
  "hatched"
3639
3965
  ],
3966
+ preferredCompositions: [
3967
+ "radial",
3968
+ "clustered",
3969
+ "flow-field"
3970
+ ],
3640
3971
  flowLineMultiplier: 0,
3641
3972
  heroShape: false,
3642
3973
  glowMultiplier: 0,
@@ -3659,6 +3990,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3659
3990
  "stroke-only",
3660
3991
  "incomplete"
3661
3992
  ],
3993
+ preferredCompositions: [
3994
+ "spiral",
3995
+ "radial",
3996
+ "golden-spiral"
3997
+ ],
3662
3998
  flowLineMultiplier: 2,
3663
3999
  heroShape: true,
3664
4000
  glowMultiplier: 2.5,
@@ -3682,6 +4018,12 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3682
4018
  ...b.preferredStyles
3683
4019
  ])
3684
4020
  ];
4021
+ const mergedCompositions = [
4022
+ ...new Set([
4023
+ ...a.preferredCompositions,
4024
+ ...b.preferredCompositions
4025
+ ])
4026
+ ];
3685
4027
  return {
3686
4028
  name: `${a.name}+${b.name}`,
3687
4029
  gridSize: Math.round($3faa2521b78398cf$var$lerpNum(a.gridSize, b.gridSize, t)),
@@ -3693,6 +4035,7 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3693
4035
  backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3694
4036
  paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3695
4037
  preferredStyles: mergedStyles,
4038
+ preferredCompositions: mergedCompositions,
3696
4039
  flowLineMultiplier: $3faa2521b78398cf$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3697
4040
  heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3698
4041
  glowMultiplier: $3faa2521b78398cf$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
@@ -3726,12 +4069,14 @@ const $b623126c6e9cbb71$var$SACRED_SHAPES = [
3726
4069
  "torus",
3727
4070
  "eggOfLife"
3728
4071
  ];
3729
- const $b623126c6e9cbb71$var$COMPOSITION_MODES = [
4072
+ // ── Composition modes ───────────────────────────────────────────────
4073
+ const $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES = [
3730
4074
  "radial",
3731
4075
  "flow-field",
3732
4076
  "spiral",
3733
4077
  "grid-subdivision",
3734
- "clustered"
4078
+ "clustered",
4079
+ "golden-spiral"
3735
4080
  ];
3736
4081
  // ── Helper: get position based on composition mode ──────────────────
3737
4082
  function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
@@ -3789,6 +4134,21 @@ function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height,
3789
4134
  x: rng() * width,
3790
4135
  y: rng() * height
3791
4136
  };
4137
+ case "golden-spiral":
4138
+ {
4139
+ // Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
4140
+ const PHI = (1 + Math.sqrt(5)) / 2;
4141
+ const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
4142
+ const t = shapeIndex / totalShapes;
4143
+ const angle = shapeIndex * goldenAngle + rng() * 0.3;
4144
+ const maxR = Math.min(width, height) * 0.44;
4145
+ // Shapes spiral outward with sqrt distribution for even area coverage
4146
+ const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
4147
+ return {
4148
+ x: cx + Math.cos(angle) * r,
4149
+ y: cy + Math.sin(angle) * r
4150
+ };
4151
+ }
3792
4152
  }
3793
4153
  }
3794
4154
  // ── Helper: positional color from hierarchy ─────────────────────────
@@ -3812,7 +4172,69 @@ function $b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones) {
3812
4172
  }
3813
4173
  return false;
3814
4174
  }
3815
- // ── Helper: density check ───────────────────────────────────────────
4175
+ // ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
4176
+ class $b623126c6e9cbb71$var$SpatialGrid {
4177
+ cells;
4178
+ cellSize;
4179
+ constructor(cellSize){
4180
+ this.cells = new Map();
4181
+ this.cellSize = cellSize;
4182
+ }
4183
+ key(cx, cy) {
4184
+ return `${cx},${cy}`;
4185
+ }
4186
+ insert(item) {
4187
+ const cx = Math.floor(item.x / this.cellSize);
4188
+ const cy = Math.floor(item.y / this.cellSize);
4189
+ const k = this.key(cx, cy);
4190
+ const cell = this.cells.get(k);
4191
+ if (cell) cell.push(item);
4192
+ else this.cells.set(k, [
4193
+ item
4194
+ ]);
4195
+ }
4196
+ /** Count items within radius of (x, y) */ countNear(x, y, radius) {
4197
+ const r2 = radius * radius;
4198
+ const minCx = Math.floor((x - radius) / this.cellSize);
4199
+ const maxCx = Math.floor((x + radius) / this.cellSize);
4200
+ const minCy = Math.floor((y - radius) / this.cellSize);
4201
+ const maxCy = Math.floor((y + radius) / this.cellSize);
4202
+ let count = 0;
4203
+ for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
4204
+ const cell = this.cells.get(this.key(cx, cy));
4205
+ if (!cell) continue;
4206
+ for (const p of cell){
4207
+ const dx = x - p.x;
4208
+ const dy = y - p.y;
4209
+ if (dx * dx + dy * dy < r2) count++;
4210
+ }
4211
+ }
4212
+ return count;
4213
+ }
4214
+ /** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
4215
+ const minCx = Math.floor((x - searchRadius) / this.cellSize);
4216
+ const maxCx = Math.floor((x + searchRadius) / this.cellSize);
4217
+ const minCy = Math.floor((y - searchRadius) / this.cellSize);
4218
+ const maxCy = Math.floor((y + searchRadius) / this.cellSize);
4219
+ let nearest = null;
4220
+ let bestDist2 = Infinity;
4221
+ for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
4222
+ const cell = this.cells.get(this.key(cx, cy));
4223
+ if (!cell) continue;
4224
+ for (const p of cell){
4225
+ const dx = x - p.x;
4226
+ const dy = y - p.y;
4227
+ const d2 = dx * dx + dy * dy;
4228
+ if (d2 > 0 && d2 < bestDist2) {
4229
+ bestDist2 = d2;
4230
+ nearest = p;
4231
+ }
4232
+ }
4233
+ }
4234
+ return nearest;
4235
+ }
4236
+ }
4237
+ // ── Helper: density check (legacy wrapper) ──────────────────────────
3816
4238
  function $b623126c6e9cbb71$var$localDensity(x, y, positions, radius) {
3817
4239
  let count = 0;
3818
4240
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -4111,42 +4533,43 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4111
4533
  const patternOpacity = 0.02 + rng() * 0.04;
4112
4534
  const patternColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4113
4535
  if (bgPatternRoll < 0.2) {
4114
- // Dot grid
4536
+ // Dot grid — batched into a single path
4115
4537
  const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4116
4538
  const dotR = dotSpacing * 0.08;
4117
4539
  ctx.globalAlpha = patternOpacity;
4118
4540
  ctx.fillStyle = patternColor;
4541
+ ctx.beginPath();
4119
4542
  for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4120
- ctx.beginPath();
4543
+ ctx.moveTo(px + dotR, py);
4121
4544
  ctx.arc(px, py, dotR, 0, Math.PI * 2);
4122
- ctx.fill();
4123
4545
  }
4546
+ ctx.fill();
4124
4547
  } else if (bgPatternRoll < 0.4) {
4125
- // Diagonal lines
4548
+ // Diagonal lines — batched into a single path
4126
4549
  const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4127
4550
  ctx.globalAlpha = patternOpacity;
4128
4551
  ctx.strokeStyle = patternColor;
4129
4552
  ctx.lineWidth = 0.5 * scaleFactor;
4130
4553
  const diag = Math.hypot(width, height);
4554
+ ctx.beginPath();
4131
4555
  for(let d = -diag; d < diag; d += lineSpacing){
4132
- ctx.beginPath();
4133
4556
  ctx.moveTo(d, 0);
4134
4557
  ctx.lineTo(d + height, height);
4135
- ctx.stroke();
4136
4558
  }
4559
+ ctx.stroke();
4137
4560
  } else {
4138
- // Tessellation — hexagonal grid of tiny shapes
4561
+ // Tessellation — hexagonal grid, batched into a single path
4139
4562
  const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4140
4563
  const tessH = tessSize * Math.sqrt(3);
4141
4564
  ctx.globalAlpha = patternOpacity * 0.7;
4142
4565
  ctx.strokeStyle = patternColor;
4143
4566
  ctx.lineWidth = 0.4 * scaleFactor;
4567
+ ctx.beginPath();
4144
4568
  for(let row = 0; row * tessH < height + tessH; row++){
4145
4569
  const offsetX = row % 2 * tessSize * 0.75;
4146
4570
  for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4147
4571
  const hx = col * tessSize * 1.5 + offsetX;
4148
4572
  const hy = row * tessH;
4149
- ctx.beginPath();
4150
4573
  for(let s = 0; s < 6; s++){
4151
4574
  const angle = Math.PI / 3 * s - Math.PI / 6;
4152
4575
  const vx = hx + Math.cos(angle) * tessSize * 0.5;
@@ -4155,18 +4578,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4155
4578
  else ctx.lineTo(vx, vy);
4156
4579
  }
4157
4580
  ctx.closePath();
4158
- ctx.stroke();
4159
4581
  }
4160
4582
  }
4583
+ ctx.stroke();
4161
4584
  }
4162
4585
  ctx.restore();
4163
4586
  }
4164
4587
  ctx.globalCompositeOperation = "source-over";
4165
- // ── 2. Composition mode ────────────────────────────────────────
4166
- const compositionMode = $b623126c6e9cbb71$var$COMPOSITION_MODES[Math.floor(rng() * $b623126c6e9cbb71$var$COMPOSITION_MODES.length)];
4588
+ // ── 2. Composition mode — archetype-aware selection ──────────────
4589
+ const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES.length)];
4167
4590
  const symRoll = rng();
4168
4591
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
4169
- // ── 3. Focal points + void zones ───────────────────────────────
4592
+ // ── 3. Focal points + void zones (archetype-aware) ───────────────
4170
4593
  const THIRDS_POINTS = [
4171
4594
  {
4172
4595
  x: 1 / 3,
@@ -4199,9 +4622,23 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4199
4622
  y: height * (0.2 + rng() * 0.6),
4200
4623
  strength: 0.3 + rng() * 0.4
4201
4624
  });
4202
- const numVoids = Math.floor(rng() * 2) + 1;
4625
+ // Archetype-aware void zones: dense archetypes get fewer/no voids,
4626
+ // minimal archetypes get golden-ratio positioned voids
4627
+ const PHI = (1 + Math.sqrt(5)) / 2;
4628
+ const isMinimalArchetype = archetype.gridSize <= 3;
4629
+ const isDenseArchetype = archetype.gridSize >= 8;
4630
+ const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
4203
4631
  const voidZones = [];
4204
- for(let v = 0; v < numVoids; v++)voidZones.push({
4632
+ for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
4633
+ // Place voids at golden-ratio positions for intentional negative space
4634
+ const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
4635
+ const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
4636
+ voidZones.push({
4637
+ x: width * (gx + (rng() - 0.5) * 0.05),
4638
+ y: height * (gy + (rng() - 0.5) * 0.05),
4639
+ radius: Math.min(width, height) * (0.08 + rng() * 0.08)
4640
+ });
4641
+ } else voidZones.push({
4205
4642
  x: width * (0.15 + rng() * 0.7),
4206
4643
  y: height * (0.15 + rng() * 0.7),
4207
4644
  radius: Math.min(width, height) * (0.06 + rng() * 0.1)
@@ -4257,14 +4694,30 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4257
4694
  }
4258
4695
  }
4259
4696
  ctx.globalAlpha = 1;
4260
- // ── 4. Flow field seed values ──────────────────────────────────
4697
+ // ── 4. Flow field simplex noise for organic variation ─────────
4698
+ // Create a seeded simplex noise field (unique per hash)
4699
+ const noiseFieldRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 333));
4700
+ const simplexNoise = (0, $461134e0b6ce0619$export$bbde7fbaaf9a8d66)(noiseFieldRng);
4701
+ const fbmNoise = (0, $461134e0b6ce0619$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
4261
4702
  const fieldAngleBase = rng() * Math.PI * 2;
4262
- const fieldFreq = 0.5 + rng() * 2;
4703
+ const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
4263
4704
  function flowAngle(x, y) {
4264
- return fieldAngleBase + Math.sin(x / width * fieldFreq * Math.PI * 2) * Math.PI * 0.5 + Math.cos(y / height * fieldFreq * Math.PI * 2) * Math.PI * 0.5;
4705
+ // Sample FBM noise at the position, scaled by frequency
4706
+ const nx = x / width * fieldFreq;
4707
+ const ny = y / height * fieldFreq;
4708
+ return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
4709
+ }
4710
+ // Noise-based size modulation — shapes in "high noise" areas get scaled
4711
+ function noiseSizeModulation(x, y) {
4712
+ const n = simplexNoise(x / width * 3, y / height * 3);
4713
+ // Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
4714
+ return 0.7 + (n + 1) * 0.3;
4265
4715
  }
4266
4716
  // Track all placed shapes for density checks and connecting curves
4267
4717
  const shapePositions = [];
4718
+ // Spatial grid for O(1) density and nearest-neighbor lookups
4719
+ const densityCheckRadius = Math.min(width, height) * 0.08;
4720
+ const spatialGrid = new $b623126c6e9cbb71$var$SpatialGrid(densityCheckRadius);
4268
4721
  // Hero avoidance radius — shapes near the hero orient toward it
4269
4722
  let heroCenter = null;
4270
4723
  // ── 4b. Hero shape — a dominant focal element ───────────────────
@@ -4295,7 +4748,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4295
4748
  glowColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(heroStroke, 0.4),
4296
4749
  gradientFillEnd: (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
4297
4750
  renderStyle: heroStyle,
4298
- rng: rng
4751
+ rng: rng,
4752
+ lightAngle: lightAngle,
4753
+ scaleFactor: scaleFactor
4299
4754
  });
4300
4755
  heroCenter = {
4301
4756
  x: heroFocal.x,
@@ -4308,9 +4763,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4308
4763
  size: heroSize,
4309
4764
  shape: heroShape
4310
4765
  });
4766
+ spatialGrid.insert({
4767
+ x: heroFocal.x,
4768
+ y: heroFocal.y,
4769
+ size: heroSize,
4770
+ shape: heroShape
4771
+ });
4311
4772
  }
4312
4773
  // ── 5. Shape layers ────────────────────────────────────────────
4313
- const densityCheckRadius = Math.min(width, height) * 0.08;
4314
4774
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4315
4775
  for(let layer = 0; layer < layers; layer++){
4316
4776
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
@@ -4349,12 +4809,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4349
4809
  if ($b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones)) {
4350
4810
  if (rng() < 0.85) continue;
4351
4811
  }
4352
- if ($b623126c6e9cbb71$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
4812
+ if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
4353
4813
  if (rng() < 0.6) continue;
4354
4814
  }
4355
4815
  // Power distribution for size — archetype controls the curve
4356
4816
  const sizeT = Math.pow(rng(), archetype.sizePower);
4357
- const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
4817
+ const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
4358
4818
  // Size fraction for affinity-aware shape selection
4359
4819
  const sizeFraction = size / adjustedMaxSize;
4360
4820
  // Palette-driven shape selection (replaces naive pickShape)
@@ -4409,17 +4869,11 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4409
4869
  let finalX = x;
4410
4870
  let finalY = y;
4411
4871
  if (shapePositions.length > 0 && rng() < 0.25) {
4412
- // Find nearest placed shape
4413
- let nearestDist = Infinity;
4414
- let nearestPos = null;
4415
- for (const sp of shapePositions){
4416
- const d = Math.hypot(x - sp.x, y - sp.y);
4417
- if (d < nearestDist && d > 0) {
4418
- nearestDist = d;
4419
- nearestPos = sp;
4420
- }
4421
- }
4872
+ // Use spatial grid for O(1) nearest-neighbor lookup
4873
+ const searchRadius = adjustedMaxSize * 3;
4874
+ const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
4422
4875
  if (nearestPos) {
4876
+ const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
4423
4877
  // Target distance: edges kissing (sum of half-sizes)
4424
4878
  const targetDist = (size + nearestPos.size) * 0.5;
4425
4879
  if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
@@ -4457,7 +4911,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4457
4911
  glowColor: hasGlow ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
4458
4912
  gradientFillEnd: gradientEnd,
4459
4913
  renderStyle: finalRenderStyle,
4460
- rng: rng
4914
+ rng: rng,
4915
+ lightAngle: lightAngle,
4916
+ scaleFactor: scaleFactor
4461
4917
  };
4462
4918
  if (shouldMirror) (0, $9beb8f41637c29fd$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4463
4919
  ...shapeConfig,
@@ -4490,6 +4946,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4490
4946
  size: size,
4491
4947
  shape: shape
4492
4948
  });
4949
+ spatialGrid.insert({
4950
+ x: finalX,
4951
+ y: finalY,
4952
+ size: size,
4953
+ shape: shape
4954
+ });
4493
4955
  // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
4494
4956
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
4495
4957
  const echoCount = 2 + Math.floor(rng() * 2);
@@ -4518,6 +4980,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4518
4980
  size: echoSize,
4519
4981
  shape: shape
4520
4982
  });
4983
+ spatialGrid.insert({
4984
+ x: echoX,
4985
+ y: echoY,
4986
+ size: echoSize,
4987
+ shape: shape
4988
+ });
4521
4989
  }
4522
4990
  }
4523
4991
  // ── 5d. Recursive nesting ──────────────────────────────────
@@ -4582,12 +5050,119 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4582
5050
  size: member.size,
4583
5051
  shape: memberShape
4584
5052
  });
5053
+ spatialGrid.insert({
5054
+ x: mx,
5055
+ y: my,
5056
+ size: member.size,
5057
+ shape: memberShape
5058
+ });
5059
+ }
5060
+ }
5061
+ // ── 5f. Rhythm placement — deliberate geometric progressions ──
5062
+ // ~12% of medium-large shapes spawn a rhythmic sequence
5063
+ if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
5064
+ const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
5065
+ const rhythmAngle = rng() * Math.PI * 2;
5066
+ const rhythmSpacing = size * (0.8 + rng() * 0.6);
5067
+ const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
5068
+ const rhythmShape = shape; // same shape for visual rhythm
5069
+ let rhythmSize = size * 0.6;
5070
+ for(let r = 0; r < rhythmCount; r++){
5071
+ const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
5072
+ const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
5073
+ if (rx < 0 || rx > width || ry < 0 || ry > height) break;
5074
+ if ($b623126c6e9cbb71$var$isInVoidZone(rx, ry, voidZones)) break;
5075
+ rhythmSize *= rhythmDecay;
5076
+ if (rhythmSize < adjustedMinSize) break;
5077
+ const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
5078
+ ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
5079
+ const rhythmFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
5080
+ (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
5081
+ fillColor: rhythmFill,
5082
+ strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.5),
5083
+ strokeWidth: strokeWidth * 0.7,
5084
+ size: rhythmSize,
5085
+ rotation: rotation + (r + 1) * 12,
5086
+ proportionType: "GOLDEN_RATIO",
5087
+ renderStyle: finalRenderStyle,
5088
+ rng: rng
5089
+ });
5090
+ shapePositions.push({
5091
+ x: rx,
5092
+ y: ry,
5093
+ size: rhythmSize,
5094
+ shape: rhythmShape
5095
+ });
5096
+ spatialGrid.insert({
5097
+ x: rx,
5098
+ y: ry,
5099
+ size: rhythmSize,
5100
+ shape: rhythmShape
5101
+ });
4585
5102
  }
4586
5103
  }
4587
5104
  }
4588
5105
  }
4589
5106
  // Reset blend mode for post-processing passes
4590
5107
  ctx.globalCompositeOperation = "source-over";
5108
+ // ── 5g. Layered masking / cutout portals ───────────────────────
5109
+ // ~18% of images get 1-3 portal windows that paint over foreground
5110
+ // with a tinted background wash, creating a "peek through" effect.
5111
+ if (rng() < 0.18 && shapePositions.length > 3) {
5112
+ const portalCount = 1 + Math.floor(rng() * 2);
5113
+ for(let p = 0; p < portalCount; p++){
5114
+ // Pick a position biased toward placed shapes
5115
+ const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
5116
+ const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
5117
+ const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
5118
+ const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
5119
+ // Pick a portal shape from the palette
5120
+ const portalShape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
5121
+ const portalRotation = rng() * 360;
5122
+ const portalAlpha = 0.6 + rng() * 0.35;
5123
+ ctx.save();
5124
+ ctx.translate(portalX, portalY);
5125
+ ctx.rotate(portalRotation * Math.PI / 180);
5126
+ // Step 1: Clip to the portal shape and fill with background wash
5127
+ ctx.beginPath();
5128
+ (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
5129
+ ctx.clip();
5130
+ // Fill the clipped region with a radial gradient from background colors
5131
+ const portalColor = (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
5132
+ const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
5133
+ portalGrad.addColorStop(0, portalColor);
5134
+ portalGrad.addColorStop(1, bgEnd);
5135
+ ctx.globalAlpha = portalAlpha;
5136
+ ctx.fillStyle = portalGrad;
5137
+ ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
5138
+ // Optional: subtle inner texture — a few tiny dots inside the portal
5139
+ if (rng() < 0.5) {
5140
+ const dotCount = 3 + Math.floor(rng() * 5);
5141
+ ctx.globalAlpha = portalAlpha * 0.3;
5142
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
5143
+ for(let d = 0; d < dotCount; d++){
5144
+ const dx = (rng() - 0.5) * portalSize * 1.4;
5145
+ const dy = (rng() - 0.5) * portalSize * 1.4;
5146
+ const dr = (1 + rng() * 3) * scaleFactor;
5147
+ ctx.beginPath();
5148
+ ctx.arc(dx, dy, dr, 0, Math.PI * 2);
5149
+ ctx.fill();
5150
+ }
5151
+ }
5152
+ ctx.restore();
5153
+ // Step 2: Draw a border ring around the portal (outside the clip)
5154
+ ctx.save();
5155
+ ctx.translate(portalX, portalY);
5156
+ ctx.rotate(portalRotation * Math.PI / 180);
5157
+ ctx.globalAlpha = 0.15 + rng() * 0.2;
5158
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
5159
+ ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
5160
+ ctx.beginPath();
5161
+ (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
5162
+ ctx.stroke();
5163
+ ctx.restore();
5164
+ }
5165
+ }
4591
5166
  // ── 6. Flow-line pass — variable color, branching, pressure ────
4592
5167
  const baseFlowLines = 6 + Math.floor(rng() * 10);
4593
5168
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
@@ -4611,6 +5186,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4611
5186
  fx += Math.cos(angle) * stepLen;
4612
5187
  fy += Math.sin(angle) * stepLen;
4613
5188
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
5189
+ // Skip segments that pass through void zones
5190
+ if ($b623126c6e9cbb71$var$isInVoidZone(fx, fy, voidZones)) {
5191
+ prevX = fx;
5192
+ prevY = fy;
5193
+ continue;
5194
+ }
4614
5195
  const t = s / steps;
4615
5196
  // Taper + pressure
4616
5197
  const taper = 1 - t * 0.8;
@@ -4708,30 +5289,60 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4708
5289
  }
4709
5290
  ctx.restore();
4710
5291
  }
4711
- // ── 7. Noise texture overlay ───────────────────────────────────
5292
+ // ── 7. Noise texture overlay — batched via ImageData ─────────────
4712
5293
  const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
4713
5294
  const noiseDensity = Math.floor(width * height / 800);
4714
- for(let i = 0; i < noiseDensity; i++){
4715
- const nx = noiseRng() * width;
4716
- const ny = noiseRng() * height;
4717
- const brightness = noiseRng() > 0.5 ? 255 : 0;
4718
- const alpha = 0.01 + noiseRng() * 0.03;
4719
- ctx.globalAlpha = alpha;
4720
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
4721
- ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5295
+ try {
5296
+ const imageData = ctx.getImageData(0, 0, width, height);
5297
+ const data = imageData.data;
5298
+ const pixelScale = Math.max(1, Math.round(scaleFactor));
5299
+ for(let i = 0; i < noiseDensity; i++){
5300
+ const nx = Math.floor(noiseRng() * width);
5301
+ const ny = Math.floor(noiseRng() * height);
5302
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5303
+ const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
5304
+ // Write a small block of pixels for scale
5305
+ for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
5306
+ const idx = ((ny + dy) * width + (nx + dx)) * 4;
5307
+ // Alpha-blend the noise dot onto existing pixel data
5308
+ const srcA = alpha / 255;
5309
+ const invA = 1 - srcA;
5310
+ data[idx] = Math.round(data[idx] * invA + brightness * srcA);
5311
+ data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
5312
+ data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
5313
+ // Keep existing alpha
5314
+ }
5315
+ }
5316
+ ctx.putImageData(imageData, 0, 0);
5317
+ } catch {
5318
+ // Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
5319
+ for(let i = 0; i < noiseDensity; i++){
5320
+ const nx = noiseRng() * width;
5321
+ const ny = noiseRng() * height;
5322
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5323
+ const alpha = 0.01 + noiseRng() * 0.03;
5324
+ ctx.globalAlpha = alpha;
5325
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5326
+ ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5327
+ }
4722
5328
  }
4723
5329
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
4724
5330
  ctx.globalAlpha = 1;
4725
5331
  const vignetteStrength = 0.25 + rng() * 0.2;
4726
5332
  const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
5333
+ // Tint vignette based on background: warm sepia for light, cool blue for dark
5334
+ const isLightBg = bgLum > 0.5;
5335
+ const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
5336
+ : `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
4727
5337
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
4728
5338
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
4729
- vigGrad.addColorStop(1, `rgba(0,0,0,${vignetteStrength.toFixed(3)})`);
5339
+ vigGrad.addColorStop(1, vignetteColor);
4730
5340
  ctx.fillStyle = vigGrad;
4731
5341
  ctx.fillRect(0, 0, width, height);
4732
- // ── 9. Organic connecting curves ───────────────────────────────
5342
+ // ── 9. Organic connecting curves — proximity-aware ───────────────
4733
5343
  if (shapePositions.length > 1) {
4734
5344
  const numCurves = Math.floor(8 * (width * height) / 1048576);
5345
+ const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
4735
5346
  ctx.lineWidth = 0.8 * scaleFactor;
4736
5347
  for(let i = 0; i < numCurves; i++){
4737
5348
  const idxA = Math.floor(rng() * shapePositions.length);
@@ -4739,11 +5350,13 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4739
5350
  const idxB = (idxA + offset) % shapePositions.length;
4740
5351
  const a = shapePositions[idxA];
4741
5352
  const b = shapePositions[idxB];
4742
- const mx = (a.x + b.x) / 2;
4743
- const my = (a.y + b.y) / 2;
4744
5353
  const dx = b.x - a.x;
4745
5354
  const dy = b.y - a.y;
4746
5355
  const dist = Math.hypot(dx, dy);
5356
+ // Skip connections between distant shapes
5357
+ if (dist > maxCurveDist) continue;
5358
+ const mx = (a.x + b.x) / 2;
5359
+ const my = (a.y + b.y) / 2;
4747
5360
  const bulge = (rng() - 0.5) * dist * 0.4;
4748
5361
  const cpx = mx + -dy / (dist || 1) * bulge;
4749
5362
  const cpy = my + dx / (dist || 1) * bulge;
@@ -4799,13 +5412,211 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4799
5412
  ctx.restore();
4800
5413
  ctx.globalCompositeOperation = "source-over";
4801
5414
  }
4802
- // ── 11. Signature markunique geometric chop from hash prefix ──
5415
+ // 10d. Gradient mapmap luminance through a two-color gradient
5416
+ // Uses dominant→accent as the dark→light ramp for a cohesive tonal look
5417
+ if (rng() < 0.35) {
5418
+ const gmDark = colorHierarchy.dominant;
5419
+ const gmLight = colorHierarchy.accent;
5420
+ ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
5421
+ ctx.globalCompositeOperation = "color";
5422
+ // Paint a linear gradient from dark color (top) to light color (bottom)
5423
+ const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
5424
+ gmGrad.addColorStop(0, gmDark);
5425
+ gmGrad.addColorStop(1, gmLight);
5426
+ ctx.fillStyle = gmGrad;
5427
+ ctx.fillRect(0, 0, width, height);
5428
+ ctx.globalCompositeOperation = "source-over";
5429
+ }
5430
+ // ── 10e. Generative borders — archetype-driven decorative frames ──
5431
+ {
5432
+ ctx.save();
5433
+ ctx.globalAlpha = 1;
5434
+ ctx.globalCompositeOperation = "source-over";
5435
+ const borderRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 314));
5436
+ const borderPad = Math.min(width, height) * 0.025;
5437
+ const borderColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5438
+ const borderColorSolid = colorHierarchy.accent;
5439
+ const archName = archetype.name;
5440
+ if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
5441
+ // Clean ruled lines with corner ornaments
5442
+ ctx.strokeStyle = borderColor;
5443
+ ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
5444
+ ctx.globalAlpha = 0.18 + borderRng() * 0.1;
5445
+ // Outer rule
5446
+ ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
5447
+ // Inner rule (thinner, offset)
5448
+ const innerPad = borderPad * 1.8;
5449
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5450
+ ctx.globalAlpha *= 0.7;
5451
+ ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
5452
+ // Corner ornaments — small squares at each corner
5453
+ const ornSize = borderPad * 0.6;
5454
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(borderColorSolid, 0.12);
5455
+ const corners = [
5456
+ [
5457
+ borderPad,
5458
+ borderPad
5459
+ ],
5460
+ [
5461
+ width - borderPad - ornSize,
5462
+ borderPad
5463
+ ],
5464
+ [
5465
+ borderPad,
5466
+ height - borderPad - ornSize
5467
+ ],
5468
+ [
5469
+ width - borderPad - ornSize,
5470
+ height - borderPad - ornSize
5471
+ ]
5472
+ ];
5473
+ for (const [cx2, cy2] of corners){
5474
+ ctx.fillRect(cx2, cy2, ornSize, ornSize);
5475
+ // Diagonal cross inside ornament
5476
+ ctx.beginPath();
5477
+ ctx.moveTo(cx2, cy2);
5478
+ ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
5479
+ ctx.moveTo(cx2 + ornSize, cy2);
5480
+ ctx.lineTo(cx2, cy2 + ornSize);
5481
+ ctx.stroke();
5482
+ }
5483
+ } else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
5484
+ // Vine tendrils — organic curving lines along edges
5485
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
5486
+ ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
5487
+ ctx.globalAlpha = 0.12 + borderRng() * 0.08;
5488
+ ctx.lineCap = "round";
5489
+ const tendrilCount = 8 + Math.floor(borderRng() * 8);
5490
+ for(let t = 0; t < tendrilCount; t++){
5491
+ // Start from a random edge point
5492
+ const edge = Math.floor(borderRng() * 4);
5493
+ let tx, ty;
5494
+ if (edge === 0) {
5495
+ tx = borderRng() * width;
5496
+ ty = borderPad;
5497
+ } else if (edge === 1) {
5498
+ tx = borderRng() * width;
5499
+ ty = height - borderPad;
5500
+ } else if (edge === 2) {
5501
+ tx = borderPad;
5502
+ ty = borderRng() * height;
5503
+ } else {
5504
+ tx = width - borderPad;
5505
+ ty = borderRng() * height;
5506
+ }
5507
+ ctx.beginPath();
5508
+ ctx.moveTo(tx, ty);
5509
+ const segs = 3 + Math.floor(borderRng() * 4);
5510
+ for(let s = 0; s < segs; s++){
5511
+ const inward = borderPad * (1 + borderRng() * 2);
5512
+ // Curl inward from edge
5513
+ const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
5514
+ const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
5515
+ const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
5516
+ const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
5517
+ tx = cpx3;
5518
+ ty = cpy3;
5519
+ ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
5520
+ }
5521
+ ctx.stroke();
5522
+ // Small leaf/dot at tendril end
5523
+ if (borderRng() < 0.6) {
5524
+ ctx.beginPath();
5525
+ ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
5526
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
5527
+ ctx.fill();
5528
+ }
5529
+ }
5530
+ } else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
5531
+ // Star-studded arcs along edges
5532
+ ctx.globalAlpha = 0.1 + borderRng() * 0.08;
5533
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5534
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
5535
+ ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
5536
+ // Subtle arc along top and bottom
5537
+ ctx.beginPath();
5538
+ ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
5539
+ ctx.stroke();
5540
+ ctx.beginPath();
5541
+ ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
5542
+ ctx.stroke();
5543
+ // Scatter small stars along the border region
5544
+ const starCount = 15 + Math.floor(borderRng() * 15);
5545
+ for(let s = 0; s < starCount; s++){
5546
+ const edge = Math.floor(borderRng() * 4);
5547
+ let sx, sy;
5548
+ if (edge === 0) {
5549
+ sx = borderRng() * width;
5550
+ sy = borderPad * (0.5 + borderRng());
5551
+ } else if (edge === 1) {
5552
+ sx = borderRng() * width;
5553
+ sy = height - borderPad * (0.5 + borderRng());
5554
+ } else if (edge === 2) {
5555
+ sx = borderPad * (0.5 + borderRng());
5556
+ sy = borderRng() * height;
5557
+ } else {
5558
+ sx = width - borderPad * (0.5 + borderRng());
5559
+ sy = borderRng() * height;
5560
+ }
5561
+ const starR = (1 + borderRng() * 2.5) * scaleFactor;
5562
+ // 4-point star
5563
+ ctx.beginPath();
5564
+ for(let p = 0; p < 8; p++){
5565
+ const a = p / 8 * Math.PI * 2;
5566
+ const r = p % 2 === 0 ? starR : starR * 0.4;
5567
+ const px2 = sx + Math.cos(a) * r;
5568
+ const py2 = sy + Math.sin(a) * r;
5569
+ if (p === 0) ctx.moveTo(px2, py2);
5570
+ else ctx.lineTo(px2, py2);
5571
+ }
5572
+ ctx.closePath();
5573
+ ctx.fill();
5574
+ }
5575
+ } else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
5576
+ // Thin single rule — understated elegance
5577
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
5578
+ ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
5579
+ ctx.globalAlpha = 0.1 + borderRng() * 0.06;
5580
+ ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
5581
+ }
5582
+ // Other archetypes: no border (intentional — not every image needs one)
5583
+ ctx.restore();
5584
+ }
5585
+ // ── 11. Signature mark — placed in the least-dense corner ──────
4803
5586
  {
4804
5587
  const sigRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
4805
5588
  const sigSize = Math.min(width, height) * 0.025;
4806
- // Bottom-right corner with padding
4807
- const sigX = width - sigSize * 2.5;
4808
- const sigY = height - sigSize * 2.5;
5589
+ const sigMargin = sigSize * 2.5;
5590
+ // Find the corner with the lowest local density
5591
+ const cornerCandidates = [
5592
+ {
5593
+ x: sigMargin,
5594
+ y: sigMargin
5595
+ },
5596
+ {
5597
+ x: width - sigMargin,
5598
+ y: sigMargin
5599
+ },
5600
+ {
5601
+ x: sigMargin,
5602
+ y: height - sigMargin
5603
+ },
5604
+ {
5605
+ x: width - sigMargin,
5606
+ y: height - sigMargin
5607
+ }
5608
+ ];
5609
+ let bestCorner = cornerCandidates[3]; // default: bottom-right
5610
+ let minDensity = Infinity;
5611
+ for (const corner of cornerCandidates){
5612
+ const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
5613
+ if (density < minDensity) {
5614
+ minDensity = density;
5615
+ bestCorner = corner;
5616
+ }
5617
+ }
5618
+ const sigX = bestCorner.x;
5619
+ const sigY = bestCorner.y;
4809
5620
  const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
4810
5621
  const sigColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
4811
5622
  ctx.save();