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/browser.js CHANGED
@@ -61,6 +61,136 @@ const $616009579e3d72c5$export$bb9e4790bc99ae59 = {
61
61
  PI: Math.PI,
62
62
  PHI: (1 + Math.sqrt(5)) / 2
63
63
  };
64
+ function $616009579e3d72c5$export$bbde7fbaaf9a8d66(rng) {
65
+ // Build a deterministic permutation table (256 entries, doubled)
66
+ const perm = new Uint8Array(512);
67
+ const p = new Uint8Array(256);
68
+ for(let i = 0; i < 256; i++)p[i] = i;
69
+ // Fisher-Yates shuffle with our seeded RNG
70
+ for(let i = 255; i > 0; i--){
71
+ const j = Math.floor(rng() * (i + 1));
72
+ const tmp = p[i];
73
+ p[i] = p[j];
74
+ p[j] = tmp;
75
+ }
76
+ for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
77
+ // 12 gradient vectors for 2D simplex
78
+ const GRAD2 = [
79
+ [
80
+ 1,
81
+ 1
82
+ ],
83
+ [
84
+ -1,
85
+ 1
86
+ ],
87
+ [
88
+ 1,
89
+ -1
90
+ ],
91
+ [
92
+ -1,
93
+ -1
94
+ ],
95
+ [
96
+ 1,
97
+ 0
98
+ ],
99
+ [
100
+ -1,
101
+ 0
102
+ ],
103
+ [
104
+ 0,
105
+ 1
106
+ ],
107
+ [
108
+ 0,
109
+ -1
110
+ ],
111
+ [
112
+ 1,
113
+ 1
114
+ ],
115
+ [
116
+ -1,
117
+ 1
118
+ ],
119
+ [
120
+ 1,
121
+ -1
122
+ ],
123
+ [
124
+ -1,
125
+ -1
126
+ ]
127
+ ];
128
+ const F2 = 0.5 * (Math.sqrt(3) - 1);
129
+ const G2 = (3 - Math.sqrt(3)) / 6;
130
+ function dot2(g, x, y) {
131
+ return g[0] * x + g[1] * y;
132
+ }
133
+ return function noise2D(xin, yin) {
134
+ const s = (xin + yin) * F2;
135
+ const i = Math.floor(xin + s);
136
+ const j = Math.floor(yin + s);
137
+ const t = (i + j) * G2;
138
+ const X0 = i - t;
139
+ const Y0 = j - t;
140
+ const x0 = xin - X0;
141
+ const y0 = yin - Y0;
142
+ let i1, j1;
143
+ if (x0 > y0) {
144
+ i1 = 1;
145
+ j1 = 0;
146
+ } else {
147
+ i1 = 0;
148
+ j1 = 1;
149
+ }
150
+ const x1 = x0 - i1 + G2;
151
+ const y1 = y0 - j1 + G2;
152
+ const x2 = x0 - 1 + 2 * G2;
153
+ const y2 = y0 - 1 + 2 * G2;
154
+ const ii = i & 255;
155
+ const jj = j & 255;
156
+ let n0 = 0, n1 = 0, n2 = 0;
157
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
158
+ if (t0 >= 0) {
159
+ t0 *= t0;
160
+ const gi0 = perm[ii + perm[jj]] % 12;
161
+ n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
162
+ }
163
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
164
+ if (t1 >= 0) {
165
+ t1 *= t1;
166
+ const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
167
+ n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
168
+ }
169
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
170
+ if (t2 >= 0) {
171
+ t2 *= t2;
172
+ const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
173
+ n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
174
+ }
175
+ // Scale to approximately [-1, 1]
176
+ return 70 * (n0 + n1 + n2);
177
+ };
178
+ }
179
+ function $616009579e3d72c5$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
180
+ return function fbm(x, y) {
181
+ let value = 0;
182
+ let amplitude = 1;
183
+ let frequency = 1;
184
+ let maxAmp = 0;
185
+ for(let i = 0; i < octaves; i++){
186
+ value += noise(x * frequency, y * frequency) * amplitude;
187
+ maxAmp += amplitude;
188
+ amplitude *= gain;
189
+ frequency *= lacunarity;
190
+ }
191
+ return value / maxAmp;
192
+ };
193
+ }
64
194
  class $616009579e3d72c5$export$da2372f11bc66b3f {
65
195
  static getProportionalSize(baseSize, proportion) {
66
196
  return baseSize * proportion;
@@ -254,6 +384,48 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
254
384
  $b5a262d09b87e373$var$hslToHex(baseHue, 0.7, 0.35)
255
385
  ];
256
386
  }
387
+ case "split-complementary":
388
+ {
389
+ // Base hue + two colors flanking the complement (±30°)
390
+ const comp = (baseHue + 180) % 360;
391
+ const split1 = (comp - 30 + 360) % 360;
392
+ const split2 = (comp + 30) % 360;
393
+ const sat = 0.55 + this.rng() * 0.25;
394
+ return [
395
+ $b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
396
+ $b5a262d09b87e373$var$hslToHex(baseHue, sat * 0.8, 0.65),
397
+ $b5a262d09b87e373$var$hslToHex(split1, sat, 0.5),
398
+ $b5a262d09b87e373$var$hslToHex(split2, sat, 0.5),
399
+ $b5a262d09b87e373$var$hslToHex(split1, sat * 0.7, 0.7)
400
+ ];
401
+ }
402
+ case "analogous-accent":
403
+ {
404
+ // Tight cluster of 3 analogous hues + 1 distant accent
405
+ const step = 15 + this.rng() * 20; // 15-35° apart
406
+ const h1 = (baseHue - step + 360) % 360;
407
+ const h2 = (baseHue + step) % 360;
408
+ const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
409
+ const sat = 0.5 + this.rng() * 0.3;
410
+ return [
411
+ $b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
412
+ $b5a262d09b87e373$var$hslToHex(h1, sat, 0.55),
413
+ $b5a262d09b87e373$var$hslToHex(h2, sat, 0.45),
414
+ $b5a262d09b87e373$var$hslToHex(accentHue, sat + 0.15, 0.5)
415
+ ];
416
+ }
417
+ case "limited-palette":
418
+ {
419
+ // Only 3 colors — like a risograph print
420
+ const h2 = (baseHue + 120 + this.rng() * 40) % 360;
421
+ const h3 = (baseHue + 220 + this.rng() * 40) % 360;
422
+ const sat = 0.6 + this.rng() * 0.2;
423
+ return [
424
+ $b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
425
+ $b5a262d09b87e373$var$hslToHex(h2, sat, 0.5),
426
+ $b5a262d09b87e373$var$hslToHex(h3, sat * 0.9, 0.55)
427
+ ];
428
+ }
257
429
  case "harmonious":
258
430
  default:
259
431
  return this.getColors();
@@ -274,6 +446,14 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
274
446
  "#f5f5f0",
275
447
  "#e8e8e0"
276
448
  ];
449
+ case "split-complementary":
450
+ case "analogous-accent":
451
+ return this.getBackgroundColors();
452
+ case "limited-palette":
453
+ return [
454
+ $b5a262d09b87e373$var$hslToHex(this.seed % 360, 0.08, 0.94),
455
+ $b5a262d09b87e373$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
456
+ ];
277
457
  case "neon":
278
458
  return [
279
459
  "#0a0a12",
@@ -400,15 +580,17 @@ function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
400
580
  accent: colors[colors.length - 1] || "#888888",
401
581
  all: colors
402
582
  };
403
- // Pick dominant as the color closest to the palette's average hue
583
+ // Pick dominant as the color with the highest chroma (saturation × distance from gray)
584
+ // This selects the most visually prominent color rather than the average
404
585
  const hsls = colors.map((c)=>$b5a262d09b87e373$var$hexToHsl(c));
405
- const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
406
586
  let dominantIdx = 0;
407
- let minDist = 360;
587
+ let maxChroma = -1;
408
588
  for(let i = 0; i < hsls.length; i++){
409
- const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
410
- if (d < minDist) {
411
- minDist = d;
589
+ // Chroma approximation: saturation × how far lightness is from 50% (gray)
590
+ const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
591
+ const chroma = hsls[i][1] * lightnessVibrancy;
592
+ if (chroma > maxChroma) {
593
+ maxChroma = chroma;
412
594
  dominantIdx = i;
413
595
  }
414
596
  }
@@ -527,7 +709,8 @@ function $b5a262d09b87e373$export$703ba40a4347f77a(base, layerRatio, hueShiftPer
527
709
  return {
528
710
  dominant: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.dominant, shift),
529
711
  secondary: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
530
- accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
712
+ accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
713
+ all: base.all.map((c)=>$b5a262d09b87e373$export$1793a1bfbe4f6ff5(c, shift * 0.6))
531
714
  };
532
715
  }
533
716
 
@@ -1910,6 +2093,23 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
1910
2093
  ctx.fill();
1911
2094
  ctx.fillStyle = origFill;
1912
2095
  ctx.restore();
2096
+ // Pass 4: Organic edge erosion — irregular bites along the boundary
2097
+ if (rng && size > 20) {
2098
+ const erosionBites = 6 + Math.floor(rng() * 8);
2099
+ const edgeRadius = size * 0.45;
2100
+ ctx.save();
2101
+ ctx.globalCompositeOperation = "destination-out";
2102
+ ctx.globalAlpha = 0.6 + rng() * 0.3;
2103
+ for(let eb = 0; eb < erosionBites; eb++){
2104
+ const biteAngle = rng() * Math.PI * 2;
2105
+ const biteDist = edgeRadius * (0.85 + rng() * 0.25);
2106
+ const biteR = size * (0.02 + rng() * 0.04);
2107
+ ctx.beginPath();
2108
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2109
+ ctx.fill();
2110
+ }
2111
+ ctx.restore();
2112
+ }
1913
2113
  ctx.globalAlpha = savedAlpha;
1914
2114
  // Soft stroke on top — thinner than normal for delicacy
1915
2115
  ctx.globalAlpha *= 0.25;
@@ -2219,6 +2419,23 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
2219
2419
  ctx.stroke();
2220
2420
  ctx.restore();
2221
2421
  }
2422
+ // Organic edge erosion — small irregular bites for rough paper feel
2423
+ if (rng && size > 20) {
2424
+ const erosionBites = 4 + Math.floor(rng() * 6);
2425
+ const edgeRadius = size * 0.42;
2426
+ ctx.save();
2427
+ ctx.globalCompositeOperation = "destination-out";
2428
+ ctx.globalAlpha = 0.5 + rng() * 0.3;
2429
+ for(let eb = 0; eb < erosionBites; eb++){
2430
+ const biteAngle = rng() * Math.PI * 2;
2431
+ const biteDist = edgeRadius * (0.9 + rng() * 0.2);
2432
+ const biteR = size * (0.015 + rng() * 0.03);
2433
+ ctx.beginPath();
2434
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2435
+ ctx.fill();
2436
+ }
2437
+ ctx.restore();
2438
+ }
2222
2439
  ctx.globalAlpha = savedAlphaHD;
2223
2440
  break;
2224
2441
  }
@@ -2230,12 +2447,20 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
2230
2447
  }
2231
2448
  }
2232
2449
  function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2233
- 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;
2450
+ 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;
2234
2451
  ctx.save();
2235
2452
  ctx.translate(x, y);
2236
2453
  ctx.rotate(rotation * Math.PI / 180);
2237
- // Glow / shadow effect
2238
- if (glowRadius > 0) {
2454
+ // ── Drop shadow — soft colored shadow offset along light direction ──
2455
+ if (lightAngle !== undefined && size > 10) {
2456
+ const shadowDist = size * 0.035;
2457
+ const shadowBlurR = size * 0.06;
2458
+ ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
2459
+ ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
2460
+ ctx.shadowBlur = shadowBlurR;
2461
+ ctx.shadowColor = "rgba(0,0,0,0.12)";
2462
+ } else if (glowRadius > 0) {
2463
+ // Glow / shadow effect (legacy path)
2239
2464
  ctx.shadowBlur = glowRadius;
2240
2465
  ctx.shadowColor = glowColor || fillColor;
2241
2466
  ctx.shadowOffsetX = 0;
@@ -2257,8 +2482,39 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2257
2482
  });
2258
2483
  $e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
2259
2484
  }
2260
- // Reset shadow so patterns aren't double-glowed
2261
- if (glowRadius > 0) ctx.shadowBlur = 0;
2485
+ // Reset shadow so patterns and highlight aren't double-shadowed
2486
+ ctx.shadowBlur = 0;
2487
+ ctx.shadowOffsetX = 0;
2488
+ ctx.shadowOffsetY = 0;
2489
+ ctx.shadowColor = "transparent";
2490
+ // ── Specular highlight — tinted arc on the light-facing side ──
2491
+ if (lightAngle !== undefined && size > 15 && rng) {
2492
+ const hlRadius = size * 0.35;
2493
+ const hlDist = size * 0.15;
2494
+ const hlX = Math.cos(lightAngle) * hlDist;
2495
+ const hlY = Math.sin(lightAngle) * hlDist;
2496
+ const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2497
+ // Tint highlight warm/cool based on fill color for cohesion
2498
+ // Parse fill to detect warmth — fallback to white for non-parseable
2499
+ let hlBase = "255,255,255";
2500
+ if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
2501
+ const r = parseInt(fillColor.slice(1, 3), 16);
2502
+ const g = parseInt(fillColor.slice(3, 5), 16);
2503
+ const b = parseInt(fillColor.slice(5, 7), 16);
2504
+ // Blend toward white but keep a hint of the fill's warmth
2505
+ hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
2506
+ }
2507
+ hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
2508
+ hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
2509
+ hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
2510
+ const savedOp = ctx.globalCompositeOperation;
2511
+ ctx.globalCompositeOperation = "soft-light";
2512
+ ctx.fillStyle = hlGrad;
2513
+ ctx.beginPath();
2514
+ ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
2515
+ ctx.fill();
2516
+ ctx.globalCompositeOperation = savedOp;
2517
+ }
2262
2518
  // Layer additional patterns if specified
2263
2519
  if (patterns.length > 0) (0, $616009579e3d72c5$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
2264
2520
  baseSize: size,
@@ -3312,6 +3568,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3312
3568
  "watercolor",
3313
3569
  "fill-only"
3314
3570
  ],
3571
+ preferredCompositions: [
3572
+ "clustered",
3573
+ "flow-field",
3574
+ "radial"
3575
+ ],
3315
3576
  flowLineMultiplier: 2.5,
3316
3577
  heroShape: false,
3317
3578
  glowMultiplier: 0.5,
@@ -3333,6 +3594,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3333
3594
  "stroke-only",
3334
3595
  "incomplete"
3335
3596
  ],
3597
+ preferredCompositions: [
3598
+ "golden-spiral",
3599
+ "grid-subdivision"
3600
+ ],
3336
3601
  flowLineMultiplier: 0.3,
3337
3602
  heroShape: true,
3338
3603
  glowMultiplier: 0,
@@ -3354,6 +3619,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3354
3619
  "fill-only",
3355
3620
  "incomplete"
3356
3621
  ],
3622
+ preferredCompositions: [
3623
+ "flow-field",
3624
+ "golden-spiral",
3625
+ "spiral"
3626
+ ],
3357
3627
  flowLineMultiplier: 4,
3358
3628
  heroShape: false,
3359
3629
  glowMultiplier: 0.3,
@@ -3376,6 +3646,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3376
3646
  "double-stroke",
3377
3647
  "hatched"
3378
3648
  ],
3649
+ preferredCompositions: [
3650
+ "grid-subdivision",
3651
+ "radial"
3652
+ ],
3379
3653
  flowLineMultiplier: 0,
3380
3654
  heroShape: false,
3381
3655
  glowMultiplier: 0,
@@ -3397,6 +3671,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3397
3671
  "incomplete",
3398
3672
  "fill-only"
3399
3673
  ],
3674
+ preferredCompositions: [
3675
+ "golden-spiral",
3676
+ "radial",
3677
+ "spiral"
3678
+ ],
3400
3679
  flowLineMultiplier: 1.5,
3401
3680
  heroShape: true,
3402
3681
  glowMultiplier: 2,
@@ -3417,6 +3696,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3417
3696
  "fill-and-stroke",
3418
3697
  "double-stroke"
3419
3698
  ],
3699
+ preferredCompositions: [
3700
+ "grid-subdivision",
3701
+ "golden-spiral"
3702
+ ],
3420
3703
  flowLineMultiplier: 0,
3421
3704
  heroShape: true,
3422
3705
  glowMultiplier: 0,
@@ -3438,6 +3721,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3438
3721
  "double-stroke",
3439
3722
  "dashed"
3440
3723
  ],
3724
+ preferredCompositions: [
3725
+ "radial",
3726
+ "spiral",
3727
+ "clustered"
3728
+ ],
3441
3729
  flowLineMultiplier: 2,
3442
3730
  heroShape: true,
3443
3731
  glowMultiplier: 3,
@@ -3460,6 +3748,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3460
3748
  "stroke-only",
3461
3749
  "dashed"
3462
3750
  ],
3751
+ preferredCompositions: [
3752
+ "flow-field",
3753
+ "grid-subdivision",
3754
+ "clustered"
3755
+ ],
3463
3756
  flowLineMultiplier: 1.5,
3464
3757
  heroShape: false,
3465
3758
  glowMultiplier: 0,
@@ -3481,6 +3774,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3481
3774
  "watercolor",
3482
3775
  "fill-and-stroke"
3483
3776
  ],
3777
+ preferredCompositions: [
3778
+ "radial",
3779
+ "spiral",
3780
+ "golden-spiral"
3781
+ ],
3484
3782
  flowLineMultiplier: 3,
3485
3783
  heroShape: true,
3486
3784
  glowMultiplier: 2.5,
@@ -3502,6 +3800,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3502
3800
  "fill-only",
3503
3801
  "incomplete"
3504
3802
  ],
3803
+ preferredCompositions: [
3804
+ "golden-spiral",
3805
+ "flow-field",
3806
+ "radial"
3807
+ ],
3505
3808
  flowLineMultiplier: 0.5,
3506
3809
  heroShape: false,
3507
3810
  glowMultiplier: 0.3,
@@ -3523,6 +3826,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3523
3826
  "stroke-only",
3524
3827
  "dashed"
3525
3828
  ],
3829
+ preferredCompositions: [
3830
+ "grid-subdivision",
3831
+ "radial"
3832
+ ],
3526
3833
  flowLineMultiplier: 0,
3527
3834
  heroShape: false,
3528
3835
  glowMultiplier: 0,
@@ -3544,6 +3851,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3544
3851
  "fill-only",
3545
3852
  "double-stroke"
3546
3853
  ],
3854
+ preferredCompositions: [
3855
+ "grid-subdivision",
3856
+ "clustered"
3857
+ ],
3547
3858
  flowLineMultiplier: 0,
3548
3859
  heroShape: true,
3549
3860
  glowMultiplier: 0,
@@ -3565,6 +3876,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3565
3876
  "watercolor",
3566
3877
  "fill-only"
3567
3878
  ],
3879
+ preferredCompositions: [
3880
+ "radial",
3881
+ "golden-spiral",
3882
+ "flow-field"
3883
+ ],
3568
3884
  flowLineMultiplier: 1,
3569
3885
  heroShape: true,
3570
3886
  glowMultiplier: 1,
@@ -3586,6 +3902,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3586
3902
  "stroke-only",
3587
3903
  "fill-only"
3588
3904
  ],
3905
+ preferredCompositions: [
3906
+ "clustered",
3907
+ "grid-subdivision",
3908
+ "radial"
3909
+ ],
3589
3910
  flowLineMultiplier: 0,
3590
3911
  heroShape: false,
3591
3912
  glowMultiplier: 0.3,
@@ -3607,6 +3928,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3607
3928
  "fill-only",
3608
3929
  "incomplete"
3609
3930
  ],
3931
+ preferredCompositions: [
3932
+ "flow-field",
3933
+ "golden-spiral",
3934
+ "spiral"
3935
+ ],
3610
3936
  flowLineMultiplier: 3,
3611
3937
  heroShape: true,
3612
3938
  glowMultiplier: 0.2,
@@ -3628,6 +3954,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3628
3954
  "fill-only",
3629
3955
  "hatched"
3630
3956
  ],
3957
+ preferredCompositions: [
3958
+ "radial",
3959
+ "clustered",
3960
+ "flow-field"
3961
+ ],
3631
3962
  flowLineMultiplier: 0,
3632
3963
  heroShape: false,
3633
3964
  glowMultiplier: 0,
@@ -3650,6 +3981,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3650
3981
  "stroke-only",
3651
3982
  "incomplete"
3652
3983
  ],
3984
+ preferredCompositions: [
3985
+ "spiral",
3986
+ "radial",
3987
+ "golden-spiral"
3988
+ ],
3653
3989
  flowLineMultiplier: 2,
3654
3990
  heroShape: true,
3655
3991
  glowMultiplier: 2.5,
@@ -3673,6 +4009,12 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3673
4009
  ...b.preferredStyles
3674
4010
  ])
3675
4011
  ];
4012
+ const mergedCompositions = [
4013
+ ...new Set([
4014
+ ...a.preferredCompositions,
4015
+ ...b.preferredCompositions
4016
+ ])
4017
+ ];
3676
4018
  return {
3677
4019
  name: `${a.name}+${b.name}`,
3678
4020
  gridSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.gridSize, b.gridSize, t)),
@@ -3684,6 +4026,7 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3684
4026
  backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3685
4027
  paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3686
4028
  preferredStyles: mergedStyles,
4029
+ preferredCompositions: mergedCompositions,
3687
4030
  flowLineMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3688
4031
  heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3689
4032
  glowMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
@@ -3717,12 +4060,14 @@ const $1f63dc64b5593c73$var$SACRED_SHAPES = [
3717
4060
  "torus",
3718
4061
  "eggOfLife"
3719
4062
  ];
3720
- const $1f63dc64b5593c73$var$COMPOSITION_MODES = [
4063
+ // ── Composition modes ───────────────────────────────────────────────
4064
+ const $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES = [
3721
4065
  "radial",
3722
4066
  "flow-field",
3723
4067
  "spiral",
3724
4068
  "grid-subdivision",
3725
- "clustered"
4069
+ "clustered",
4070
+ "golden-spiral"
3726
4071
  ];
3727
4072
  // ── Helper: get position based on composition mode ──────────────────
3728
4073
  function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
@@ -3780,6 +4125,21 @@ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height,
3780
4125
  x: rng() * width,
3781
4126
  y: rng() * height
3782
4127
  };
4128
+ case "golden-spiral":
4129
+ {
4130
+ // Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
4131
+ const PHI = (1 + Math.sqrt(5)) / 2;
4132
+ const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
4133
+ const t = shapeIndex / totalShapes;
4134
+ const angle = shapeIndex * goldenAngle + rng() * 0.3;
4135
+ const maxR = Math.min(width, height) * 0.44;
4136
+ // Shapes spiral outward with sqrt distribution for even area coverage
4137
+ const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
4138
+ return {
4139
+ x: cx + Math.cos(angle) * r,
4140
+ y: cy + Math.sin(angle) * r
4141
+ };
4142
+ }
3783
4143
  }
3784
4144
  }
3785
4145
  // ── Helper: positional color from hierarchy ─────────────────────────
@@ -3803,7 +4163,67 @@ function $1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones) {
3803
4163
  }
3804
4164
  return false;
3805
4165
  }
3806
- // ── Helper: density check ───────────────────────────────────────────
4166
+ // ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
4167
+ class $1f63dc64b5593c73$var$SpatialGrid {
4168
+ constructor(cellSize){
4169
+ this.cells = new Map();
4170
+ this.cellSize = cellSize;
4171
+ }
4172
+ key(cx, cy) {
4173
+ return `${cx},${cy}`;
4174
+ }
4175
+ insert(item) {
4176
+ const cx = Math.floor(item.x / this.cellSize);
4177
+ const cy = Math.floor(item.y / this.cellSize);
4178
+ const k = this.key(cx, cy);
4179
+ const cell = this.cells.get(k);
4180
+ if (cell) cell.push(item);
4181
+ else this.cells.set(k, [
4182
+ item
4183
+ ]);
4184
+ }
4185
+ /** Count items within radius of (x, y) */ countNear(x, y, radius) {
4186
+ const r2 = radius * radius;
4187
+ const minCx = Math.floor((x - radius) / this.cellSize);
4188
+ const maxCx = Math.floor((x + radius) / this.cellSize);
4189
+ const minCy = Math.floor((y - radius) / this.cellSize);
4190
+ const maxCy = Math.floor((y + radius) / this.cellSize);
4191
+ let count = 0;
4192
+ for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
4193
+ const cell = this.cells.get(this.key(cx, cy));
4194
+ if (!cell) continue;
4195
+ for (const p of cell){
4196
+ const dx = x - p.x;
4197
+ const dy = y - p.y;
4198
+ if (dx * dx + dy * dy < r2) count++;
4199
+ }
4200
+ }
4201
+ return count;
4202
+ }
4203
+ /** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
4204
+ const minCx = Math.floor((x - searchRadius) / this.cellSize);
4205
+ const maxCx = Math.floor((x + searchRadius) / this.cellSize);
4206
+ const minCy = Math.floor((y - searchRadius) / this.cellSize);
4207
+ const maxCy = Math.floor((y + searchRadius) / this.cellSize);
4208
+ let nearest = null;
4209
+ let bestDist2 = Infinity;
4210
+ for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
4211
+ const cell = this.cells.get(this.key(cx, cy));
4212
+ if (!cell) continue;
4213
+ for (const p of cell){
4214
+ const dx = x - p.x;
4215
+ const dy = y - p.y;
4216
+ const d2 = dx * dx + dy * dy;
4217
+ if (d2 > 0 && d2 < bestDist2) {
4218
+ bestDist2 = d2;
4219
+ nearest = p;
4220
+ }
4221
+ }
4222
+ }
4223
+ return nearest;
4224
+ }
4225
+ }
4226
+ // ── Helper: density check (legacy wrapper) ──────────────────────────
3807
4227
  function $1f63dc64b5593c73$var$localDensity(x, y, positions, radius) {
3808
4228
  let count = 0;
3809
4229
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -4102,42 +4522,43 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4102
4522
  const patternOpacity = 0.02 + rng() * 0.04;
4103
4523
  const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4104
4524
  if (bgPatternRoll < 0.2) {
4105
- // Dot grid
4525
+ // Dot grid — batched into a single path
4106
4526
  const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4107
4527
  const dotR = dotSpacing * 0.08;
4108
4528
  ctx.globalAlpha = patternOpacity;
4109
4529
  ctx.fillStyle = patternColor;
4530
+ ctx.beginPath();
4110
4531
  for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4111
- ctx.beginPath();
4532
+ ctx.moveTo(px + dotR, py);
4112
4533
  ctx.arc(px, py, dotR, 0, Math.PI * 2);
4113
- ctx.fill();
4114
4534
  }
4535
+ ctx.fill();
4115
4536
  } else if (bgPatternRoll < 0.4) {
4116
- // Diagonal lines
4537
+ // Diagonal lines — batched into a single path
4117
4538
  const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4118
4539
  ctx.globalAlpha = patternOpacity;
4119
4540
  ctx.strokeStyle = patternColor;
4120
4541
  ctx.lineWidth = 0.5 * scaleFactor;
4121
4542
  const diag = Math.hypot(width, height);
4543
+ ctx.beginPath();
4122
4544
  for(let d = -diag; d < diag; d += lineSpacing){
4123
- ctx.beginPath();
4124
4545
  ctx.moveTo(d, 0);
4125
4546
  ctx.lineTo(d + height, height);
4126
- ctx.stroke();
4127
4547
  }
4548
+ ctx.stroke();
4128
4549
  } else {
4129
- // Tessellation — hexagonal grid of tiny shapes
4550
+ // Tessellation — hexagonal grid, batched into a single path
4130
4551
  const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4131
4552
  const tessH = tessSize * Math.sqrt(3);
4132
4553
  ctx.globalAlpha = patternOpacity * 0.7;
4133
4554
  ctx.strokeStyle = patternColor;
4134
4555
  ctx.lineWidth = 0.4 * scaleFactor;
4556
+ ctx.beginPath();
4135
4557
  for(let row = 0; row * tessH < height + tessH; row++){
4136
4558
  const offsetX = row % 2 * tessSize * 0.75;
4137
4559
  for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4138
4560
  const hx = col * tessSize * 1.5 + offsetX;
4139
4561
  const hy = row * tessH;
4140
- ctx.beginPath();
4141
4562
  for(let s = 0; s < 6; s++){
4142
4563
  const angle = Math.PI / 3 * s - Math.PI / 6;
4143
4564
  const vx = hx + Math.cos(angle) * tessSize * 0.5;
@@ -4146,18 +4567,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4146
4567
  else ctx.lineTo(vx, vy);
4147
4568
  }
4148
4569
  ctx.closePath();
4149
- ctx.stroke();
4150
4570
  }
4151
4571
  }
4572
+ ctx.stroke();
4152
4573
  }
4153
4574
  ctx.restore();
4154
4575
  }
4155
4576
  ctx.globalCompositeOperation = "source-over";
4156
- // ── 2. Composition mode ────────────────────────────────────────
4157
- const compositionMode = $1f63dc64b5593c73$var$COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$COMPOSITION_MODES.length)];
4577
+ // ── 2. Composition mode — archetype-aware selection ──────────────
4578
+ const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES.length)];
4158
4579
  const symRoll = rng();
4159
4580
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
4160
- // ── 3. Focal points + void zones ───────────────────────────────
4581
+ // ── 3. Focal points + void zones (archetype-aware) ───────────────
4161
4582
  const THIRDS_POINTS = [
4162
4583
  {
4163
4584
  x: 1 / 3,
@@ -4190,9 +4611,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4190
4611
  y: height * (0.2 + rng() * 0.6),
4191
4612
  strength: 0.3 + rng() * 0.4
4192
4613
  });
4193
- const numVoids = Math.floor(rng() * 2) + 1;
4614
+ // Archetype-aware void zones: dense archetypes get fewer/no voids,
4615
+ // minimal archetypes get golden-ratio positioned voids
4616
+ const PHI = (1 + Math.sqrt(5)) / 2;
4617
+ const isMinimalArchetype = archetype.gridSize <= 3;
4618
+ const isDenseArchetype = archetype.gridSize >= 8;
4619
+ const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
4194
4620
  const voidZones = [];
4195
- for(let v = 0; v < numVoids; v++)voidZones.push({
4621
+ for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
4622
+ // Place voids at golden-ratio positions for intentional negative space
4623
+ const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
4624
+ const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
4625
+ voidZones.push({
4626
+ x: width * (gx + (rng() - 0.5) * 0.05),
4627
+ y: height * (gy + (rng() - 0.5) * 0.05),
4628
+ radius: Math.min(width, height) * (0.08 + rng() * 0.08)
4629
+ });
4630
+ } else voidZones.push({
4196
4631
  x: width * (0.15 + rng() * 0.7),
4197
4632
  y: height * (0.15 + rng() * 0.7),
4198
4633
  radius: Math.min(width, height) * (0.06 + rng() * 0.1)
@@ -4248,14 +4683,30 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4248
4683
  }
4249
4684
  }
4250
4685
  ctx.globalAlpha = 1;
4251
- // ── 4. Flow field seed values ──────────────────────────────────
4686
+ // ── 4. Flow field simplex noise for organic variation ─────────
4687
+ // Create a seeded simplex noise field (unique per hash)
4688
+ const noiseFieldRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 333));
4689
+ const simplexNoise = (0, $616009579e3d72c5$export$bbde7fbaaf9a8d66)(noiseFieldRng);
4690
+ const fbmNoise = (0, $616009579e3d72c5$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
4252
4691
  const fieldAngleBase = rng() * Math.PI * 2;
4253
- const fieldFreq = 0.5 + rng() * 2;
4692
+ const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
4254
4693
  function flowAngle(x, y) {
4255
- 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;
4694
+ // Sample FBM noise at the position, scaled by frequency
4695
+ const nx = x / width * fieldFreq;
4696
+ const ny = y / height * fieldFreq;
4697
+ return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
4698
+ }
4699
+ // Noise-based size modulation — shapes in "high noise" areas get scaled
4700
+ function noiseSizeModulation(x, y) {
4701
+ const n = simplexNoise(x / width * 3, y / height * 3);
4702
+ // Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
4703
+ return 0.7 + (n + 1) * 0.3;
4256
4704
  }
4257
4705
  // Track all placed shapes for density checks and connecting curves
4258
4706
  const shapePositions = [];
4707
+ // Spatial grid for O(1) density and nearest-neighbor lookups
4708
+ const densityCheckRadius = Math.min(width, height) * 0.08;
4709
+ const spatialGrid = new $1f63dc64b5593c73$var$SpatialGrid(densityCheckRadius);
4259
4710
  // Hero avoidance radius — shapes near the hero orient toward it
4260
4711
  let heroCenter = null;
4261
4712
  // ── 4b. Hero shape — a dominant focal element ───────────────────
@@ -4286,7 +4737,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4286
4737
  glowColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(heroStroke, 0.4),
4287
4738
  gradientFillEnd: (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
4288
4739
  renderStyle: heroStyle,
4289
- rng: rng
4740
+ rng: rng,
4741
+ lightAngle: lightAngle,
4742
+ scaleFactor: scaleFactor
4290
4743
  });
4291
4744
  heroCenter = {
4292
4745
  x: heroFocal.x,
@@ -4299,9 +4752,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4299
4752
  size: heroSize,
4300
4753
  shape: heroShape
4301
4754
  });
4755
+ spatialGrid.insert({
4756
+ x: heroFocal.x,
4757
+ y: heroFocal.y,
4758
+ size: heroSize,
4759
+ shape: heroShape
4760
+ });
4302
4761
  }
4303
4762
  // ── 5. Shape layers ────────────────────────────────────────────
4304
- const densityCheckRadius = Math.min(width, height) * 0.08;
4305
4763
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4306
4764
  for(let layer = 0; layer < layers; layer++){
4307
4765
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
@@ -4340,12 +4798,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4340
4798
  if ($1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones)) {
4341
4799
  if (rng() < 0.85) continue;
4342
4800
  }
4343
- if ($1f63dc64b5593c73$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
4801
+ if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
4344
4802
  if (rng() < 0.6) continue;
4345
4803
  }
4346
4804
  // Power distribution for size — archetype controls the curve
4347
4805
  const sizeT = Math.pow(rng(), archetype.sizePower);
4348
- const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
4806
+ const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
4349
4807
  // Size fraction for affinity-aware shape selection
4350
4808
  const sizeFraction = size / adjustedMaxSize;
4351
4809
  // Palette-driven shape selection (replaces naive pickShape)
@@ -4400,17 +4858,11 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4400
4858
  let finalX = x;
4401
4859
  let finalY = y;
4402
4860
  if (shapePositions.length > 0 && rng() < 0.25) {
4403
- // Find nearest placed shape
4404
- let nearestDist = Infinity;
4405
- let nearestPos = null;
4406
- for (const sp of shapePositions){
4407
- const d = Math.hypot(x - sp.x, y - sp.y);
4408
- if (d < nearestDist && d > 0) {
4409
- nearestDist = d;
4410
- nearestPos = sp;
4411
- }
4412
- }
4861
+ // Use spatial grid for O(1) nearest-neighbor lookup
4862
+ const searchRadius = adjustedMaxSize * 3;
4863
+ const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
4413
4864
  if (nearestPos) {
4865
+ const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
4414
4866
  // Target distance: edges kissing (sum of half-sizes)
4415
4867
  const targetDist = (size + nearestPos.size) * 0.5;
4416
4868
  if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
@@ -4448,7 +4900,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4448
4900
  glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
4449
4901
  gradientFillEnd: gradientEnd,
4450
4902
  renderStyle: finalRenderStyle,
4451
- rng: rng
4903
+ rng: rng,
4904
+ lightAngle: lightAngle,
4905
+ scaleFactor: scaleFactor
4452
4906
  };
4453
4907
  if (shouldMirror) (0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4454
4908
  ...shapeConfig,
@@ -4481,6 +4935,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4481
4935
  size: size,
4482
4936
  shape: shape
4483
4937
  });
4938
+ spatialGrid.insert({
4939
+ x: finalX,
4940
+ y: finalY,
4941
+ size: size,
4942
+ shape: shape
4943
+ });
4484
4944
  // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
4485
4945
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
4486
4946
  const echoCount = 2 + Math.floor(rng() * 2);
@@ -4509,6 +4969,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4509
4969
  size: echoSize,
4510
4970
  shape: shape
4511
4971
  });
4972
+ spatialGrid.insert({
4973
+ x: echoX,
4974
+ y: echoY,
4975
+ size: echoSize,
4976
+ shape: shape
4977
+ });
4512
4978
  }
4513
4979
  }
4514
4980
  // ── 5d. Recursive nesting ──────────────────────────────────
@@ -4573,12 +5039,119 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4573
5039
  size: member.size,
4574
5040
  shape: memberShape
4575
5041
  });
5042
+ spatialGrid.insert({
5043
+ x: mx,
5044
+ y: my,
5045
+ size: member.size,
5046
+ shape: memberShape
5047
+ });
5048
+ }
5049
+ }
5050
+ // ── 5f. Rhythm placement — deliberate geometric progressions ──
5051
+ // ~12% of medium-large shapes spawn a rhythmic sequence
5052
+ if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
5053
+ const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
5054
+ const rhythmAngle = rng() * Math.PI * 2;
5055
+ const rhythmSpacing = size * (0.8 + rng() * 0.6);
5056
+ const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
5057
+ const rhythmShape = shape; // same shape for visual rhythm
5058
+ let rhythmSize = size * 0.6;
5059
+ for(let r = 0; r < rhythmCount; r++){
5060
+ const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
5061
+ const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
5062
+ if (rx < 0 || rx > width || ry < 0 || ry > height) break;
5063
+ if ($1f63dc64b5593c73$var$isInVoidZone(rx, ry, voidZones)) break;
5064
+ rhythmSize *= rhythmDecay;
5065
+ if (rhythmSize < adjustedMinSize) break;
5066
+ const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
5067
+ ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
5068
+ const rhythmFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
5069
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
5070
+ fillColor: rhythmFill,
5071
+ strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
5072
+ strokeWidth: strokeWidth * 0.7,
5073
+ size: rhythmSize,
5074
+ rotation: rotation + (r + 1) * 12,
5075
+ proportionType: "GOLDEN_RATIO",
5076
+ renderStyle: finalRenderStyle,
5077
+ rng: rng
5078
+ });
5079
+ shapePositions.push({
5080
+ x: rx,
5081
+ y: ry,
5082
+ size: rhythmSize,
5083
+ shape: rhythmShape
5084
+ });
5085
+ spatialGrid.insert({
5086
+ x: rx,
5087
+ y: ry,
5088
+ size: rhythmSize,
5089
+ shape: rhythmShape
5090
+ });
4576
5091
  }
4577
5092
  }
4578
5093
  }
4579
5094
  }
4580
5095
  // Reset blend mode for post-processing passes
4581
5096
  ctx.globalCompositeOperation = "source-over";
5097
+ // ── 5g. Layered masking / cutout portals ───────────────────────
5098
+ // ~18% of images get 1-3 portal windows that paint over foreground
5099
+ // with a tinted background wash, creating a "peek through" effect.
5100
+ if (rng() < 0.18 && shapePositions.length > 3) {
5101
+ const portalCount = 1 + Math.floor(rng() * 2);
5102
+ for(let p = 0; p < portalCount; p++){
5103
+ // Pick a position biased toward placed shapes
5104
+ const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
5105
+ const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
5106
+ const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
5107
+ const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
5108
+ // Pick a portal shape from the palette
5109
+ const portalShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
5110
+ const portalRotation = rng() * 360;
5111
+ const portalAlpha = 0.6 + rng() * 0.35;
5112
+ ctx.save();
5113
+ ctx.translate(portalX, portalY);
5114
+ ctx.rotate(portalRotation * Math.PI / 180);
5115
+ // Step 1: Clip to the portal shape and fill with background wash
5116
+ ctx.beginPath();
5117
+ (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
5118
+ ctx.clip();
5119
+ // Fill the clipped region with a radial gradient from background colors
5120
+ const portalColor = (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
5121
+ const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
5122
+ portalGrad.addColorStop(0, portalColor);
5123
+ portalGrad.addColorStop(1, bgEnd);
5124
+ ctx.globalAlpha = portalAlpha;
5125
+ ctx.fillStyle = portalGrad;
5126
+ ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
5127
+ // Optional: subtle inner texture — a few tiny dots inside the portal
5128
+ if (rng() < 0.5) {
5129
+ const dotCount = 3 + Math.floor(rng() * 5);
5130
+ ctx.globalAlpha = portalAlpha * 0.3;
5131
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
5132
+ for(let d = 0; d < dotCount; d++){
5133
+ const dx = (rng() - 0.5) * portalSize * 1.4;
5134
+ const dy = (rng() - 0.5) * portalSize * 1.4;
5135
+ const dr = (1 + rng() * 3) * scaleFactor;
5136
+ ctx.beginPath();
5137
+ ctx.arc(dx, dy, dr, 0, Math.PI * 2);
5138
+ ctx.fill();
5139
+ }
5140
+ }
5141
+ ctx.restore();
5142
+ // Step 2: Draw a border ring around the portal (outside the clip)
5143
+ ctx.save();
5144
+ ctx.translate(portalX, portalY);
5145
+ ctx.rotate(portalRotation * Math.PI / 180);
5146
+ ctx.globalAlpha = 0.15 + rng() * 0.2;
5147
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
5148
+ ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
5149
+ ctx.beginPath();
5150
+ (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
5151
+ ctx.stroke();
5152
+ ctx.restore();
5153
+ }
5154
+ }
4582
5155
  // ── 6. Flow-line pass — variable color, branching, pressure ────
4583
5156
  const baseFlowLines = 6 + Math.floor(rng() * 10);
4584
5157
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
@@ -4602,6 +5175,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4602
5175
  fx += Math.cos(angle) * stepLen;
4603
5176
  fy += Math.sin(angle) * stepLen;
4604
5177
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
5178
+ // Skip segments that pass through void zones
5179
+ if ($1f63dc64b5593c73$var$isInVoidZone(fx, fy, voidZones)) {
5180
+ prevX = fx;
5181
+ prevY = fy;
5182
+ continue;
5183
+ }
4605
5184
  const t = s / steps;
4606
5185
  // Taper + pressure
4607
5186
  const taper = 1 - t * 0.8;
@@ -4699,30 +5278,60 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4699
5278
  }
4700
5279
  ctx.restore();
4701
5280
  }
4702
- // ── 7. Noise texture overlay ───────────────────────────────────
5281
+ // ── 7. Noise texture overlay — batched via ImageData ─────────────
4703
5282
  const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
4704
5283
  const noiseDensity = Math.floor(width * height / 800);
4705
- for(let i = 0; i < noiseDensity; i++){
4706
- const nx = noiseRng() * width;
4707
- const ny = noiseRng() * height;
4708
- const brightness = noiseRng() > 0.5 ? 255 : 0;
4709
- const alpha = 0.01 + noiseRng() * 0.03;
4710
- ctx.globalAlpha = alpha;
4711
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
4712
- ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5284
+ try {
5285
+ const imageData = ctx.getImageData(0, 0, width, height);
5286
+ const data = imageData.data;
5287
+ const pixelScale = Math.max(1, Math.round(scaleFactor));
5288
+ for(let i = 0; i < noiseDensity; i++){
5289
+ const nx = Math.floor(noiseRng() * width);
5290
+ const ny = Math.floor(noiseRng() * height);
5291
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5292
+ const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
5293
+ // Write a small block of pixels for scale
5294
+ for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
5295
+ const idx = ((ny + dy) * width + (nx + dx)) * 4;
5296
+ // Alpha-blend the noise dot onto existing pixel data
5297
+ const srcA = alpha / 255;
5298
+ const invA = 1 - srcA;
5299
+ data[idx] = Math.round(data[idx] * invA + brightness * srcA);
5300
+ data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
5301
+ data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
5302
+ // Keep existing alpha
5303
+ }
5304
+ }
5305
+ ctx.putImageData(imageData, 0, 0);
5306
+ } catch {
5307
+ // Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
5308
+ for(let i = 0; i < noiseDensity; i++){
5309
+ const nx = noiseRng() * width;
5310
+ const ny = noiseRng() * height;
5311
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5312
+ const alpha = 0.01 + noiseRng() * 0.03;
5313
+ ctx.globalAlpha = alpha;
5314
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5315
+ ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5316
+ }
4713
5317
  }
4714
5318
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
4715
5319
  ctx.globalAlpha = 1;
4716
5320
  const vignetteStrength = 0.25 + rng() * 0.2;
4717
5321
  const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
5322
+ // Tint vignette based on background: warm sepia for light, cool blue for dark
5323
+ const isLightBg = bgLum > 0.5;
5324
+ const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
5325
+ : `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
4718
5326
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
4719
5327
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
4720
- vigGrad.addColorStop(1, `rgba(0,0,0,${vignetteStrength.toFixed(3)})`);
5328
+ vigGrad.addColorStop(1, vignetteColor);
4721
5329
  ctx.fillStyle = vigGrad;
4722
5330
  ctx.fillRect(0, 0, width, height);
4723
- // ── 9. Organic connecting curves ───────────────────────────────
5331
+ // ── 9. Organic connecting curves — proximity-aware ───────────────
4724
5332
  if (shapePositions.length > 1) {
4725
5333
  const numCurves = Math.floor(8 * (width * height) / 1048576);
5334
+ const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
4726
5335
  ctx.lineWidth = 0.8 * scaleFactor;
4727
5336
  for(let i = 0; i < numCurves; i++){
4728
5337
  const idxA = Math.floor(rng() * shapePositions.length);
@@ -4730,11 +5339,13 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4730
5339
  const idxB = (idxA + offset) % shapePositions.length;
4731
5340
  const a = shapePositions[idxA];
4732
5341
  const b = shapePositions[idxB];
4733
- const mx = (a.x + b.x) / 2;
4734
- const my = (a.y + b.y) / 2;
4735
5342
  const dx = b.x - a.x;
4736
5343
  const dy = b.y - a.y;
4737
5344
  const dist = Math.hypot(dx, dy);
5345
+ // Skip connections between distant shapes
5346
+ if (dist > maxCurveDist) continue;
5347
+ const mx = (a.x + b.x) / 2;
5348
+ const my = (a.y + b.y) / 2;
4738
5349
  const bulge = (rng() - 0.5) * dist * 0.4;
4739
5350
  const cpx = mx + -dy / (dist || 1) * bulge;
4740
5351
  const cpy = my + dx / (dist || 1) * bulge;
@@ -4790,13 +5401,211 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4790
5401
  ctx.restore();
4791
5402
  ctx.globalCompositeOperation = "source-over";
4792
5403
  }
4793
- // ── 11. Signature markunique geometric chop from hash prefix ──
5404
+ // 10d. Gradient mapmap luminance through a two-color gradient
5405
+ // Uses dominant→accent as the dark→light ramp for a cohesive tonal look
5406
+ if (rng() < 0.35) {
5407
+ const gmDark = colorHierarchy.dominant;
5408
+ const gmLight = colorHierarchy.accent;
5409
+ ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
5410
+ ctx.globalCompositeOperation = "color";
5411
+ // Paint a linear gradient from dark color (top) to light color (bottom)
5412
+ const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
5413
+ gmGrad.addColorStop(0, gmDark);
5414
+ gmGrad.addColorStop(1, gmLight);
5415
+ ctx.fillStyle = gmGrad;
5416
+ ctx.fillRect(0, 0, width, height);
5417
+ ctx.globalCompositeOperation = "source-over";
5418
+ }
5419
+ // ── 10e. Generative borders — archetype-driven decorative frames ──
5420
+ {
5421
+ ctx.save();
5422
+ ctx.globalAlpha = 1;
5423
+ ctx.globalCompositeOperation = "source-over";
5424
+ const borderRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 314));
5425
+ const borderPad = Math.min(width, height) * 0.025;
5426
+ const borderColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5427
+ const borderColorSolid = colorHierarchy.accent;
5428
+ const archName = archetype.name;
5429
+ if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
5430
+ // Clean ruled lines with corner ornaments
5431
+ ctx.strokeStyle = borderColor;
5432
+ ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
5433
+ ctx.globalAlpha = 0.18 + borderRng() * 0.1;
5434
+ // Outer rule
5435
+ ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
5436
+ // Inner rule (thinner, offset)
5437
+ const innerPad = borderPad * 1.8;
5438
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5439
+ ctx.globalAlpha *= 0.7;
5440
+ ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
5441
+ // Corner ornaments — small squares at each corner
5442
+ const ornSize = borderPad * 0.6;
5443
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(borderColorSolid, 0.12);
5444
+ const corners = [
5445
+ [
5446
+ borderPad,
5447
+ borderPad
5448
+ ],
5449
+ [
5450
+ width - borderPad - ornSize,
5451
+ borderPad
5452
+ ],
5453
+ [
5454
+ borderPad,
5455
+ height - borderPad - ornSize
5456
+ ],
5457
+ [
5458
+ width - borderPad - ornSize,
5459
+ height - borderPad - ornSize
5460
+ ]
5461
+ ];
5462
+ for (const [cx2, cy2] of corners){
5463
+ ctx.fillRect(cx2, cy2, ornSize, ornSize);
5464
+ // Diagonal cross inside ornament
5465
+ ctx.beginPath();
5466
+ ctx.moveTo(cx2, cy2);
5467
+ ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
5468
+ ctx.moveTo(cx2 + ornSize, cy2);
5469
+ ctx.lineTo(cx2, cy2 + ornSize);
5470
+ ctx.stroke();
5471
+ }
5472
+ } else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
5473
+ // Vine tendrils — organic curving lines along edges
5474
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
5475
+ ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
5476
+ ctx.globalAlpha = 0.12 + borderRng() * 0.08;
5477
+ ctx.lineCap = "round";
5478
+ const tendrilCount = 8 + Math.floor(borderRng() * 8);
5479
+ for(let t = 0; t < tendrilCount; t++){
5480
+ // Start from a random edge point
5481
+ const edge = Math.floor(borderRng() * 4);
5482
+ let tx, ty;
5483
+ if (edge === 0) {
5484
+ tx = borderRng() * width;
5485
+ ty = borderPad;
5486
+ } else if (edge === 1) {
5487
+ tx = borderRng() * width;
5488
+ ty = height - borderPad;
5489
+ } else if (edge === 2) {
5490
+ tx = borderPad;
5491
+ ty = borderRng() * height;
5492
+ } else {
5493
+ tx = width - borderPad;
5494
+ ty = borderRng() * height;
5495
+ }
5496
+ ctx.beginPath();
5497
+ ctx.moveTo(tx, ty);
5498
+ const segs = 3 + Math.floor(borderRng() * 4);
5499
+ for(let s = 0; s < segs; s++){
5500
+ const inward = borderPad * (1 + borderRng() * 2);
5501
+ // Curl inward from edge
5502
+ const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
5503
+ const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
5504
+ const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
5505
+ const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
5506
+ tx = cpx3;
5507
+ ty = cpy3;
5508
+ ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
5509
+ }
5510
+ ctx.stroke();
5511
+ // Small leaf/dot at tendril end
5512
+ if (borderRng() < 0.6) {
5513
+ ctx.beginPath();
5514
+ ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
5515
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
5516
+ ctx.fill();
5517
+ }
5518
+ }
5519
+ } else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
5520
+ // Star-studded arcs along edges
5521
+ ctx.globalAlpha = 0.1 + borderRng() * 0.08;
5522
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5523
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
5524
+ ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
5525
+ // Subtle arc along top and bottom
5526
+ ctx.beginPath();
5527
+ ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
5528
+ ctx.stroke();
5529
+ ctx.beginPath();
5530
+ ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
5531
+ ctx.stroke();
5532
+ // Scatter small stars along the border region
5533
+ const starCount = 15 + Math.floor(borderRng() * 15);
5534
+ for(let s = 0; s < starCount; s++){
5535
+ const edge = Math.floor(borderRng() * 4);
5536
+ let sx, sy;
5537
+ if (edge === 0) {
5538
+ sx = borderRng() * width;
5539
+ sy = borderPad * (0.5 + borderRng());
5540
+ } else if (edge === 1) {
5541
+ sx = borderRng() * width;
5542
+ sy = height - borderPad * (0.5 + borderRng());
5543
+ } else if (edge === 2) {
5544
+ sx = borderPad * (0.5 + borderRng());
5545
+ sy = borderRng() * height;
5546
+ } else {
5547
+ sx = width - borderPad * (0.5 + borderRng());
5548
+ sy = borderRng() * height;
5549
+ }
5550
+ const starR = (1 + borderRng() * 2.5) * scaleFactor;
5551
+ // 4-point star
5552
+ ctx.beginPath();
5553
+ for(let p = 0; p < 8; p++){
5554
+ const a = p / 8 * Math.PI * 2;
5555
+ const r = p % 2 === 0 ? starR : starR * 0.4;
5556
+ const px2 = sx + Math.cos(a) * r;
5557
+ const py2 = sy + Math.sin(a) * r;
5558
+ if (p === 0) ctx.moveTo(px2, py2);
5559
+ else ctx.lineTo(px2, py2);
5560
+ }
5561
+ ctx.closePath();
5562
+ ctx.fill();
5563
+ }
5564
+ } else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
5565
+ // Thin single rule — understated elegance
5566
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
5567
+ ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
5568
+ ctx.globalAlpha = 0.1 + borderRng() * 0.06;
5569
+ ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
5570
+ }
5571
+ // Other archetypes: no border (intentional — not every image needs one)
5572
+ ctx.restore();
5573
+ }
5574
+ // ── 11. Signature mark — placed in the least-dense corner ──────
4794
5575
  {
4795
5576
  const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
4796
5577
  const sigSize = Math.min(width, height) * 0.025;
4797
- // Bottom-right corner with padding
4798
- const sigX = width - sigSize * 2.5;
4799
- const sigY = height - sigSize * 2.5;
5578
+ const sigMargin = sigSize * 2.5;
5579
+ // Find the corner with the lowest local density
5580
+ const cornerCandidates = [
5581
+ {
5582
+ x: sigMargin,
5583
+ y: sigMargin
5584
+ },
5585
+ {
5586
+ x: width - sigMargin,
5587
+ y: sigMargin
5588
+ },
5589
+ {
5590
+ x: sigMargin,
5591
+ y: height - sigMargin
5592
+ },
5593
+ {
5594
+ x: width - sigMargin,
5595
+ y: height - sigMargin
5596
+ }
5597
+ ];
5598
+ let bestCorner = cornerCandidates[3]; // default: bottom-right
5599
+ let minDensity = Infinity;
5600
+ for (const corner of cornerCandidates){
5601
+ const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
5602
+ if (density < minDensity) {
5603
+ minDensity = density;
5604
+ bestCorner = corner;
5605
+ }
5606
+ }
5607
+ const sigX = bestCorner.x;
5608
+ const sigY = bestCorner.y;
4800
5609
  const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
4801
5610
  const sigColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
4802
5611
  ctx.save();