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/main.js CHANGED
@@ -76,6 +76,136 @@ const $e4b03e131ed2a289$export$bb9e4790bc99ae59 = {
76
76
  PI: Math.PI,
77
77
  PHI: (1 + Math.sqrt(5)) / 2
78
78
  };
79
+ function $e4b03e131ed2a289$export$bbde7fbaaf9a8d66(rng) {
80
+ // Build a deterministic permutation table (256 entries, doubled)
81
+ const perm = new Uint8Array(512);
82
+ const p = new Uint8Array(256);
83
+ for(let i = 0; i < 256; i++)p[i] = i;
84
+ // Fisher-Yates shuffle with our seeded RNG
85
+ for(let i = 255; i > 0; i--){
86
+ const j = Math.floor(rng() * (i + 1));
87
+ const tmp = p[i];
88
+ p[i] = p[j];
89
+ p[j] = tmp;
90
+ }
91
+ for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
92
+ // 12 gradient vectors for 2D simplex
93
+ const GRAD2 = [
94
+ [
95
+ 1,
96
+ 1
97
+ ],
98
+ [
99
+ -1,
100
+ 1
101
+ ],
102
+ [
103
+ 1,
104
+ -1
105
+ ],
106
+ [
107
+ -1,
108
+ -1
109
+ ],
110
+ [
111
+ 1,
112
+ 0
113
+ ],
114
+ [
115
+ -1,
116
+ 0
117
+ ],
118
+ [
119
+ 0,
120
+ 1
121
+ ],
122
+ [
123
+ 0,
124
+ -1
125
+ ],
126
+ [
127
+ 1,
128
+ 1
129
+ ],
130
+ [
131
+ -1,
132
+ 1
133
+ ],
134
+ [
135
+ 1,
136
+ -1
137
+ ],
138
+ [
139
+ -1,
140
+ -1
141
+ ]
142
+ ];
143
+ const F2 = 0.5 * (Math.sqrt(3) - 1);
144
+ const G2 = (3 - Math.sqrt(3)) / 6;
145
+ function dot2(g, x, y) {
146
+ return g[0] * x + g[1] * y;
147
+ }
148
+ return function noise2D(xin, yin) {
149
+ const s = (xin + yin) * F2;
150
+ const i = Math.floor(xin + s);
151
+ const j = Math.floor(yin + s);
152
+ const t = (i + j) * G2;
153
+ const X0 = i - t;
154
+ const Y0 = j - t;
155
+ const x0 = xin - X0;
156
+ const y0 = yin - Y0;
157
+ let i1, j1;
158
+ if (x0 > y0) {
159
+ i1 = 1;
160
+ j1 = 0;
161
+ } else {
162
+ i1 = 0;
163
+ j1 = 1;
164
+ }
165
+ const x1 = x0 - i1 + G2;
166
+ const y1 = y0 - j1 + G2;
167
+ const x2 = x0 - 1 + 2 * G2;
168
+ const y2 = y0 - 1 + 2 * G2;
169
+ const ii = i & 255;
170
+ const jj = j & 255;
171
+ let n0 = 0, n1 = 0, n2 = 0;
172
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
173
+ if (t0 >= 0) {
174
+ t0 *= t0;
175
+ const gi0 = perm[ii + perm[jj]] % 12;
176
+ n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
177
+ }
178
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
179
+ if (t1 >= 0) {
180
+ t1 *= t1;
181
+ const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
182
+ n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
183
+ }
184
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
185
+ if (t2 >= 0) {
186
+ t2 *= t2;
187
+ const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
188
+ n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
189
+ }
190
+ // Scale to approximately [-1, 1]
191
+ return 70 * (n0 + n1 + n2);
192
+ };
193
+ }
194
+ function $e4b03e131ed2a289$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
195
+ return function fbm(x, y) {
196
+ let value = 0;
197
+ let amplitude = 1;
198
+ let frequency = 1;
199
+ let maxAmp = 0;
200
+ for(let i = 0; i < octaves; i++){
201
+ value += noise(x * frequency, y * frequency) * amplitude;
202
+ maxAmp += amplitude;
203
+ amplitude *= gain;
204
+ frequency *= lacunarity;
205
+ }
206
+ return value / maxAmp;
207
+ };
208
+ }
79
209
  class $e4b03e131ed2a289$export$da2372f11bc66b3f {
80
210
  static getProportionalSize(baseSize, proportion) {
81
211
  return baseSize * proportion;
@@ -277,6 +407,48 @@ class $d016ad53434219a1$export$ab958c550f521376 {
277
407
  $d016ad53434219a1$var$hslToHex(baseHue, 0.7, 0.35)
278
408
  ];
279
409
  }
410
+ case "split-complementary":
411
+ {
412
+ // Base hue + two colors flanking the complement (±30°)
413
+ const comp = (baseHue + 180) % 360;
414
+ const split1 = (comp - 30 + 360) % 360;
415
+ const split2 = (comp + 30) % 360;
416
+ const sat = 0.55 + this.rng() * 0.25;
417
+ return [
418
+ $d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
419
+ $d016ad53434219a1$var$hslToHex(baseHue, sat * 0.8, 0.65),
420
+ $d016ad53434219a1$var$hslToHex(split1, sat, 0.5),
421
+ $d016ad53434219a1$var$hslToHex(split2, sat, 0.5),
422
+ $d016ad53434219a1$var$hslToHex(split1, sat * 0.7, 0.7)
423
+ ];
424
+ }
425
+ case "analogous-accent":
426
+ {
427
+ // Tight cluster of 3 analogous hues + 1 distant accent
428
+ const step = 15 + this.rng() * 20; // 15-35° apart
429
+ const h1 = (baseHue - step + 360) % 360;
430
+ const h2 = (baseHue + step) % 360;
431
+ const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
432
+ const sat = 0.5 + this.rng() * 0.3;
433
+ return [
434
+ $d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
435
+ $d016ad53434219a1$var$hslToHex(h1, sat, 0.55),
436
+ $d016ad53434219a1$var$hslToHex(h2, sat, 0.45),
437
+ $d016ad53434219a1$var$hslToHex(accentHue, sat + 0.15, 0.5)
438
+ ];
439
+ }
440
+ case "limited-palette":
441
+ {
442
+ // Only 3 colors — like a risograph print
443
+ const h2 = (baseHue + 120 + this.rng() * 40) % 360;
444
+ const h3 = (baseHue + 220 + this.rng() * 40) % 360;
445
+ const sat = 0.6 + this.rng() * 0.2;
446
+ return [
447
+ $d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
448
+ $d016ad53434219a1$var$hslToHex(h2, sat, 0.5),
449
+ $d016ad53434219a1$var$hslToHex(h3, sat * 0.9, 0.55)
450
+ ];
451
+ }
280
452
  case "harmonious":
281
453
  default:
282
454
  return this.getColors();
@@ -297,6 +469,14 @@ class $d016ad53434219a1$export$ab958c550f521376 {
297
469
  "#f5f5f0",
298
470
  "#e8e8e0"
299
471
  ];
472
+ case "split-complementary":
473
+ case "analogous-accent":
474
+ return this.getBackgroundColors();
475
+ case "limited-palette":
476
+ return [
477
+ $d016ad53434219a1$var$hslToHex(this.seed % 360, 0.08, 0.94),
478
+ $d016ad53434219a1$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
479
+ ];
300
480
  case "neon":
301
481
  return [
302
482
  "#0a0a12",
@@ -423,15 +603,17 @@ function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
423
603
  accent: colors[colors.length - 1] || "#888888",
424
604
  all: colors
425
605
  };
426
- // Pick dominant as the color closest to the palette's average hue
606
+ // Pick dominant as the color with the highest chroma (saturation × distance from gray)
607
+ // This selects the most visually prominent color rather than the average
427
608
  const hsls = colors.map((c)=>$d016ad53434219a1$var$hexToHsl(c));
428
- const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
429
609
  let dominantIdx = 0;
430
- let minDist = 360;
610
+ let maxChroma = -1;
431
611
  for(let i = 0; i < hsls.length; i++){
432
- const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
433
- if (d < minDist) {
434
- minDist = d;
612
+ // Chroma approximation: saturation × how far lightness is from 50% (gray)
613
+ const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
614
+ const chroma = hsls[i][1] * lightnessVibrancy;
615
+ if (chroma > maxChroma) {
616
+ maxChroma = chroma;
435
617
  dominantIdx = i;
436
618
  }
437
619
  }
@@ -550,7 +732,8 @@ function $d016ad53434219a1$export$703ba40a4347f77a(base, layerRatio, hueShiftPer
550
732
  return {
551
733
  dominant: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.dominant, shift),
552
734
  secondary: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
553
- accent: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
735
+ accent: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
736
+ all: base.all.map((c)=>$d016ad53434219a1$export$1793a1bfbe4f6ff5(c, shift * 0.6))
554
737
  };
555
738
  }
556
739
 
@@ -1933,6 +2116,23 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
1933
2116
  ctx.fill();
1934
2117
  ctx.fillStyle = origFill;
1935
2118
  ctx.restore();
2119
+ // Pass 4: Organic edge erosion — irregular bites along the boundary
2120
+ if (rng && size > 20) {
2121
+ const erosionBites = 6 + Math.floor(rng() * 8);
2122
+ const edgeRadius = size * 0.45;
2123
+ ctx.save();
2124
+ ctx.globalCompositeOperation = "destination-out";
2125
+ ctx.globalAlpha = 0.6 + rng() * 0.3;
2126
+ for(let eb = 0; eb < erosionBites; eb++){
2127
+ const biteAngle = rng() * Math.PI * 2;
2128
+ const biteDist = edgeRadius * (0.85 + rng() * 0.25);
2129
+ const biteR = size * (0.02 + rng() * 0.04);
2130
+ ctx.beginPath();
2131
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2132
+ ctx.fill();
2133
+ }
2134
+ ctx.restore();
2135
+ }
1936
2136
  ctx.globalAlpha = savedAlpha;
1937
2137
  // Soft stroke on top — thinner than normal for delicacy
1938
2138
  ctx.globalAlpha *= 0.25;
@@ -2242,6 +2442,23 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2242
2442
  ctx.stroke();
2243
2443
  ctx.restore();
2244
2444
  }
2445
+ // Organic edge erosion — small irregular bites for rough paper feel
2446
+ if (rng && size > 20) {
2447
+ const erosionBites = 4 + Math.floor(rng() * 6);
2448
+ const edgeRadius = size * 0.42;
2449
+ ctx.save();
2450
+ ctx.globalCompositeOperation = "destination-out";
2451
+ ctx.globalAlpha = 0.5 + rng() * 0.3;
2452
+ for(let eb = 0; eb < erosionBites; eb++){
2453
+ const biteAngle = rng() * Math.PI * 2;
2454
+ const biteDist = edgeRadius * (0.9 + rng() * 0.2);
2455
+ const biteR = size * (0.015 + rng() * 0.03);
2456
+ ctx.beginPath();
2457
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2458
+ ctx.fill();
2459
+ }
2460
+ ctx.restore();
2461
+ }
2245
2462
  ctx.globalAlpha = savedAlphaHD;
2246
2463
  break;
2247
2464
  }
@@ -2253,12 +2470,20 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2253
2470
  }
2254
2471
  }
2255
2472
  function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2256
- 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;
2473
+ 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;
2257
2474
  ctx.save();
2258
2475
  ctx.translate(x, y);
2259
2476
  ctx.rotate(rotation * Math.PI / 180);
2260
- // Glow / shadow effect
2261
- if (glowRadius > 0) {
2477
+ // ── Drop shadow — soft colored shadow offset along light direction ──
2478
+ if (lightAngle !== undefined && size > 10) {
2479
+ const shadowDist = size * 0.035;
2480
+ const shadowBlurR = size * 0.06;
2481
+ ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
2482
+ ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
2483
+ ctx.shadowBlur = shadowBlurR;
2484
+ ctx.shadowColor = "rgba(0,0,0,0.12)";
2485
+ } else if (glowRadius > 0) {
2486
+ // Glow / shadow effect (legacy path)
2262
2487
  ctx.shadowBlur = glowRadius;
2263
2488
  ctx.shadowColor = glowColor || fillColor;
2264
2489
  ctx.shadowOffsetX = 0;
@@ -2280,8 +2505,39 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2280
2505
  });
2281
2506
  $c3de8257a8baa3b0$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
2282
2507
  }
2283
- // Reset shadow so patterns aren't double-glowed
2284
- if (glowRadius > 0) ctx.shadowBlur = 0;
2508
+ // Reset shadow so patterns and highlight aren't double-shadowed
2509
+ ctx.shadowBlur = 0;
2510
+ ctx.shadowOffsetX = 0;
2511
+ ctx.shadowOffsetY = 0;
2512
+ ctx.shadowColor = "transparent";
2513
+ // ── Specular highlight — tinted arc on the light-facing side ──
2514
+ if (lightAngle !== undefined && size > 15 && rng) {
2515
+ const hlRadius = size * 0.35;
2516
+ const hlDist = size * 0.15;
2517
+ const hlX = Math.cos(lightAngle) * hlDist;
2518
+ const hlY = Math.sin(lightAngle) * hlDist;
2519
+ const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2520
+ // Tint highlight warm/cool based on fill color for cohesion
2521
+ // Parse fill to detect warmth — fallback to white for non-parseable
2522
+ let hlBase = "255,255,255";
2523
+ if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
2524
+ const r = parseInt(fillColor.slice(1, 3), 16);
2525
+ const g = parseInt(fillColor.slice(3, 5), 16);
2526
+ const b = parseInt(fillColor.slice(5, 7), 16);
2527
+ // Blend toward white but keep a hint of the fill's warmth
2528
+ hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
2529
+ }
2530
+ hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
2531
+ hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
2532
+ hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
2533
+ const savedOp = ctx.globalCompositeOperation;
2534
+ ctx.globalCompositeOperation = "soft-light";
2535
+ ctx.fillStyle = hlGrad;
2536
+ ctx.beginPath();
2537
+ ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
2538
+ ctx.fill();
2539
+ ctx.globalCompositeOperation = savedOp;
2540
+ }
2285
2541
  // Layer additional patterns if specified
2286
2542
  if (patterns.length > 0) (0, $e4b03e131ed2a289$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
2287
2543
  baseSize: size,
@@ -3335,6 +3591,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3335
3591
  "watercolor",
3336
3592
  "fill-only"
3337
3593
  ],
3594
+ preferredCompositions: [
3595
+ "clustered",
3596
+ "flow-field",
3597
+ "radial"
3598
+ ],
3338
3599
  flowLineMultiplier: 2.5,
3339
3600
  heroShape: false,
3340
3601
  glowMultiplier: 0.5,
@@ -3356,6 +3617,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3356
3617
  "stroke-only",
3357
3618
  "incomplete"
3358
3619
  ],
3620
+ preferredCompositions: [
3621
+ "golden-spiral",
3622
+ "grid-subdivision"
3623
+ ],
3359
3624
  flowLineMultiplier: 0.3,
3360
3625
  heroShape: true,
3361
3626
  glowMultiplier: 0,
@@ -3377,6 +3642,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3377
3642
  "fill-only",
3378
3643
  "incomplete"
3379
3644
  ],
3645
+ preferredCompositions: [
3646
+ "flow-field",
3647
+ "golden-spiral",
3648
+ "spiral"
3649
+ ],
3380
3650
  flowLineMultiplier: 4,
3381
3651
  heroShape: false,
3382
3652
  glowMultiplier: 0.3,
@@ -3399,6 +3669,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3399
3669
  "double-stroke",
3400
3670
  "hatched"
3401
3671
  ],
3672
+ preferredCompositions: [
3673
+ "grid-subdivision",
3674
+ "radial"
3675
+ ],
3402
3676
  flowLineMultiplier: 0,
3403
3677
  heroShape: false,
3404
3678
  glowMultiplier: 0,
@@ -3420,6 +3694,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3420
3694
  "incomplete",
3421
3695
  "fill-only"
3422
3696
  ],
3697
+ preferredCompositions: [
3698
+ "golden-spiral",
3699
+ "radial",
3700
+ "spiral"
3701
+ ],
3423
3702
  flowLineMultiplier: 1.5,
3424
3703
  heroShape: true,
3425
3704
  glowMultiplier: 2,
@@ -3440,6 +3719,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3440
3719
  "fill-and-stroke",
3441
3720
  "double-stroke"
3442
3721
  ],
3722
+ preferredCompositions: [
3723
+ "grid-subdivision",
3724
+ "golden-spiral"
3725
+ ],
3443
3726
  flowLineMultiplier: 0,
3444
3727
  heroShape: true,
3445
3728
  glowMultiplier: 0,
@@ -3461,6 +3744,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3461
3744
  "double-stroke",
3462
3745
  "dashed"
3463
3746
  ],
3747
+ preferredCompositions: [
3748
+ "radial",
3749
+ "spiral",
3750
+ "clustered"
3751
+ ],
3464
3752
  flowLineMultiplier: 2,
3465
3753
  heroShape: true,
3466
3754
  glowMultiplier: 3,
@@ -3483,6 +3771,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3483
3771
  "stroke-only",
3484
3772
  "dashed"
3485
3773
  ],
3774
+ preferredCompositions: [
3775
+ "flow-field",
3776
+ "grid-subdivision",
3777
+ "clustered"
3778
+ ],
3486
3779
  flowLineMultiplier: 1.5,
3487
3780
  heroShape: false,
3488
3781
  glowMultiplier: 0,
@@ -3504,6 +3797,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3504
3797
  "watercolor",
3505
3798
  "fill-and-stroke"
3506
3799
  ],
3800
+ preferredCompositions: [
3801
+ "radial",
3802
+ "spiral",
3803
+ "golden-spiral"
3804
+ ],
3507
3805
  flowLineMultiplier: 3,
3508
3806
  heroShape: true,
3509
3807
  glowMultiplier: 2.5,
@@ -3525,6 +3823,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3525
3823
  "fill-only",
3526
3824
  "incomplete"
3527
3825
  ],
3826
+ preferredCompositions: [
3827
+ "golden-spiral",
3828
+ "flow-field",
3829
+ "radial"
3830
+ ],
3528
3831
  flowLineMultiplier: 0.5,
3529
3832
  heroShape: false,
3530
3833
  glowMultiplier: 0.3,
@@ -3546,6 +3849,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3546
3849
  "stroke-only",
3547
3850
  "dashed"
3548
3851
  ],
3852
+ preferredCompositions: [
3853
+ "grid-subdivision",
3854
+ "radial"
3855
+ ],
3549
3856
  flowLineMultiplier: 0,
3550
3857
  heroShape: false,
3551
3858
  glowMultiplier: 0,
@@ -3567,6 +3874,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3567
3874
  "fill-only",
3568
3875
  "double-stroke"
3569
3876
  ],
3877
+ preferredCompositions: [
3878
+ "grid-subdivision",
3879
+ "clustered"
3880
+ ],
3570
3881
  flowLineMultiplier: 0,
3571
3882
  heroShape: true,
3572
3883
  glowMultiplier: 0,
@@ -3588,6 +3899,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3588
3899
  "watercolor",
3589
3900
  "fill-only"
3590
3901
  ],
3902
+ preferredCompositions: [
3903
+ "radial",
3904
+ "golden-spiral",
3905
+ "flow-field"
3906
+ ],
3591
3907
  flowLineMultiplier: 1,
3592
3908
  heroShape: true,
3593
3909
  glowMultiplier: 1,
@@ -3609,6 +3925,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3609
3925
  "stroke-only",
3610
3926
  "fill-only"
3611
3927
  ],
3928
+ preferredCompositions: [
3929
+ "clustered",
3930
+ "grid-subdivision",
3931
+ "radial"
3932
+ ],
3612
3933
  flowLineMultiplier: 0,
3613
3934
  heroShape: false,
3614
3935
  glowMultiplier: 0.3,
@@ -3630,6 +3951,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3630
3951
  "fill-only",
3631
3952
  "incomplete"
3632
3953
  ],
3954
+ preferredCompositions: [
3955
+ "flow-field",
3956
+ "golden-spiral",
3957
+ "spiral"
3958
+ ],
3633
3959
  flowLineMultiplier: 3,
3634
3960
  heroShape: true,
3635
3961
  glowMultiplier: 0.2,
@@ -3651,6 +3977,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3651
3977
  "fill-only",
3652
3978
  "hatched"
3653
3979
  ],
3980
+ preferredCompositions: [
3981
+ "radial",
3982
+ "clustered",
3983
+ "flow-field"
3984
+ ],
3654
3985
  flowLineMultiplier: 0,
3655
3986
  heroShape: false,
3656
3987
  glowMultiplier: 0,
@@ -3673,6 +4004,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3673
4004
  "stroke-only",
3674
4005
  "incomplete"
3675
4006
  ],
4007
+ preferredCompositions: [
4008
+ "spiral",
4009
+ "radial",
4010
+ "golden-spiral"
4011
+ ],
3676
4012
  flowLineMultiplier: 2,
3677
4013
  heroShape: true,
3678
4014
  glowMultiplier: 2.5,
@@ -3696,6 +4032,12 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3696
4032
  ...b.preferredStyles
3697
4033
  ])
3698
4034
  ];
4035
+ const mergedCompositions = [
4036
+ ...new Set([
4037
+ ...a.preferredCompositions,
4038
+ ...b.preferredCompositions
4039
+ ])
4040
+ ];
3699
4041
  return {
3700
4042
  name: `${a.name}+${b.name}`,
3701
4043
  gridSize: Math.round($f89bc858f7202849$var$lerpNum(a.gridSize, b.gridSize, t)),
@@ -3707,6 +4049,7 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3707
4049
  backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3708
4050
  paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3709
4051
  preferredStyles: mergedStyles,
4052
+ preferredCompositions: mergedCompositions,
3710
4053
  flowLineMultiplier: $f89bc858f7202849$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3711
4054
  heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3712
4055
  glowMultiplier: $f89bc858f7202849$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
@@ -3740,12 +4083,14 @@ const $4f72c5a314eddf25$var$SACRED_SHAPES = [
3740
4083
  "torus",
3741
4084
  "eggOfLife"
3742
4085
  ];
3743
- const $4f72c5a314eddf25$var$COMPOSITION_MODES = [
4086
+ // ── Composition modes ───────────────────────────────────────────────
4087
+ const $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES = [
3744
4088
  "radial",
3745
4089
  "flow-field",
3746
4090
  "spiral",
3747
4091
  "grid-subdivision",
3748
- "clustered"
4092
+ "clustered",
4093
+ "golden-spiral"
3749
4094
  ];
3750
4095
  // ── Helper: get position based on composition mode ──────────────────
3751
4096
  function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
@@ -3803,6 +4148,21 @@ function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height,
3803
4148
  x: rng() * width,
3804
4149
  y: rng() * height
3805
4150
  };
4151
+ case "golden-spiral":
4152
+ {
4153
+ // Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
4154
+ const PHI = (1 + Math.sqrt(5)) / 2;
4155
+ const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
4156
+ const t = shapeIndex / totalShapes;
4157
+ const angle = shapeIndex * goldenAngle + rng() * 0.3;
4158
+ const maxR = Math.min(width, height) * 0.44;
4159
+ // Shapes spiral outward with sqrt distribution for even area coverage
4160
+ const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
4161
+ return {
4162
+ x: cx + Math.cos(angle) * r,
4163
+ y: cy + Math.sin(angle) * r
4164
+ };
4165
+ }
3806
4166
  }
3807
4167
  }
3808
4168
  // ── Helper: positional color from hierarchy ─────────────────────────
@@ -3826,7 +4186,69 @@ function $4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones) {
3826
4186
  }
3827
4187
  return false;
3828
4188
  }
3829
- // ── Helper: density check ───────────────────────────────────────────
4189
+ // ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
4190
+ class $4f72c5a314eddf25$var$SpatialGrid {
4191
+ cells;
4192
+ cellSize;
4193
+ constructor(cellSize){
4194
+ this.cells = new Map();
4195
+ this.cellSize = cellSize;
4196
+ }
4197
+ key(cx, cy) {
4198
+ return `${cx},${cy}`;
4199
+ }
4200
+ insert(item) {
4201
+ const cx = Math.floor(item.x / this.cellSize);
4202
+ const cy = Math.floor(item.y / this.cellSize);
4203
+ const k = this.key(cx, cy);
4204
+ const cell = this.cells.get(k);
4205
+ if (cell) cell.push(item);
4206
+ else this.cells.set(k, [
4207
+ item
4208
+ ]);
4209
+ }
4210
+ /** Count items within radius of (x, y) */ countNear(x, y, radius) {
4211
+ const r2 = radius * radius;
4212
+ const minCx = Math.floor((x - radius) / this.cellSize);
4213
+ const maxCx = Math.floor((x + radius) / this.cellSize);
4214
+ const minCy = Math.floor((y - radius) / this.cellSize);
4215
+ const maxCy = Math.floor((y + radius) / this.cellSize);
4216
+ let count = 0;
4217
+ for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
4218
+ const cell = this.cells.get(this.key(cx, cy));
4219
+ if (!cell) continue;
4220
+ for (const p of cell){
4221
+ const dx = x - p.x;
4222
+ const dy = y - p.y;
4223
+ if (dx * dx + dy * dy < r2) count++;
4224
+ }
4225
+ }
4226
+ return count;
4227
+ }
4228
+ /** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
4229
+ const minCx = Math.floor((x - searchRadius) / this.cellSize);
4230
+ const maxCx = Math.floor((x + searchRadius) / this.cellSize);
4231
+ const minCy = Math.floor((y - searchRadius) / this.cellSize);
4232
+ const maxCy = Math.floor((y + searchRadius) / this.cellSize);
4233
+ let nearest = null;
4234
+ let bestDist2 = Infinity;
4235
+ for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
4236
+ const cell = this.cells.get(this.key(cx, cy));
4237
+ if (!cell) continue;
4238
+ for (const p of cell){
4239
+ const dx = x - p.x;
4240
+ const dy = y - p.y;
4241
+ const d2 = dx * dx + dy * dy;
4242
+ if (d2 > 0 && d2 < bestDist2) {
4243
+ bestDist2 = d2;
4244
+ nearest = p;
4245
+ }
4246
+ }
4247
+ }
4248
+ return nearest;
4249
+ }
4250
+ }
4251
+ // ── Helper: density check (legacy wrapper) ──────────────────────────
3830
4252
  function $4f72c5a314eddf25$var$localDensity(x, y, positions, radius) {
3831
4253
  let count = 0;
3832
4254
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -4125,42 +4547,43 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4125
4547
  const patternOpacity = 0.02 + rng() * 0.04;
4126
4548
  const patternColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4127
4549
  if (bgPatternRoll < 0.2) {
4128
- // Dot grid
4550
+ // Dot grid — batched into a single path
4129
4551
  const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4130
4552
  const dotR = dotSpacing * 0.08;
4131
4553
  ctx.globalAlpha = patternOpacity;
4132
4554
  ctx.fillStyle = patternColor;
4555
+ ctx.beginPath();
4133
4556
  for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4134
- ctx.beginPath();
4557
+ ctx.moveTo(px + dotR, py);
4135
4558
  ctx.arc(px, py, dotR, 0, Math.PI * 2);
4136
- ctx.fill();
4137
4559
  }
4560
+ ctx.fill();
4138
4561
  } else if (bgPatternRoll < 0.4) {
4139
- // Diagonal lines
4562
+ // Diagonal lines — batched into a single path
4140
4563
  const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4141
4564
  ctx.globalAlpha = patternOpacity;
4142
4565
  ctx.strokeStyle = patternColor;
4143
4566
  ctx.lineWidth = 0.5 * scaleFactor;
4144
4567
  const diag = Math.hypot(width, height);
4568
+ ctx.beginPath();
4145
4569
  for(let d = -diag; d < diag; d += lineSpacing){
4146
- ctx.beginPath();
4147
4570
  ctx.moveTo(d, 0);
4148
4571
  ctx.lineTo(d + height, height);
4149
- ctx.stroke();
4150
4572
  }
4573
+ ctx.stroke();
4151
4574
  } else {
4152
- // Tessellation — hexagonal grid of tiny shapes
4575
+ // Tessellation — hexagonal grid, batched into a single path
4153
4576
  const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4154
4577
  const tessH = tessSize * Math.sqrt(3);
4155
4578
  ctx.globalAlpha = patternOpacity * 0.7;
4156
4579
  ctx.strokeStyle = patternColor;
4157
4580
  ctx.lineWidth = 0.4 * scaleFactor;
4581
+ ctx.beginPath();
4158
4582
  for(let row = 0; row * tessH < height + tessH; row++){
4159
4583
  const offsetX = row % 2 * tessSize * 0.75;
4160
4584
  for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4161
4585
  const hx = col * tessSize * 1.5 + offsetX;
4162
4586
  const hy = row * tessH;
4163
- ctx.beginPath();
4164
4587
  for(let s = 0; s < 6; s++){
4165
4588
  const angle = Math.PI / 3 * s - Math.PI / 6;
4166
4589
  const vx = hx + Math.cos(angle) * tessSize * 0.5;
@@ -4169,18 +4592,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4169
4592
  else ctx.lineTo(vx, vy);
4170
4593
  }
4171
4594
  ctx.closePath();
4172
- ctx.stroke();
4173
4595
  }
4174
4596
  }
4597
+ ctx.stroke();
4175
4598
  }
4176
4599
  ctx.restore();
4177
4600
  }
4178
4601
  ctx.globalCompositeOperation = "source-over";
4179
- // ── 2. Composition mode ────────────────────────────────────────
4180
- const compositionMode = $4f72c5a314eddf25$var$COMPOSITION_MODES[Math.floor(rng() * $4f72c5a314eddf25$var$COMPOSITION_MODES.length)];
4602
+ // ── 2. Composition mode — archetype-aware selection ──────────────
4603
+ const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES.length)];
4181
4604
  const symRoll = rng();
4182
4605
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
4183
- // ── 3. Focal points + void zones ───────────────────────────────
4606
+ // ── 3. Focal points + void zones (archetype-aware) ───────────────
4184
4607
  const THIRDS_POINTS = [
4185
4608
  {
4186
4609
  x: 1 / 3,
@@ -4213,9 +4636,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4213
4636
  y: height * (0.2 + rng() * 0.6),
4214
4637
  strength: 0.3 + rng() * 0.4
4215
4638
  });
4216
- const numVoids = Math.floor(rng() * 2) + 1;
4639
+ // Archetype-aware void zones: dense archetypes get fewer/no voids,
4640
+ // minimal archetypes get golden-ratio positioned voids
4641
+ const PHI = (1 + Math.sqrt(5)) / 2;
4642
+ const isMinimalArchetype = archetype.gridSize <= 3;
4643
+ const isDenseArchetype = archetype.gridSize >= 8;
4644
+ const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
4217
4645
  const voidZones = [];
4218
- for(let v = 0; v < numVoids; v++)voidZones.push({
4646
+ for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
4647
+ // Place voids at golden-ratio positions for intentional negative space
4648
+ const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
4649
+ const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
4650
+ voidZones.push({
4651
+ x: width * (gx + (rng() - 0.5) * 0.05),
4652
+ y: height * (gy + (rng() - 0.5) * 0.05),
4653
+ radius: Math.min(width, height) * (0.08 + rng() * 0.08)
4654
+ });
4655
+ } else voidZones.push({
4219
4656
  x: width * (0.15 + rng() * 0.7),
4220
4657
  y: height * (0.15 + rng() * 0.7),
4221
4658
  radius: Math.min(width, height) * (0.06 + rng() * 0.1)
@@ -4271,14 +4708,30 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4271
4708
  }
4272
4709
  }
4273
4710
  ctx.globalAlpha = 1;
4274
- // ── 4. Flow field seed values ──────────────────────────────────
4711
+ // ── 4. Flow field simplex noise for organic variation ─────────
4712
+ // Create a seeded simplex noise field (unique per hash)
4713
+ const noiseFieldRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 333));
4714
+ const simplexNoise = (0, $e4b03e131ed2a289$export$bbde7fbaaf9a8d66)(noiseFieldRng);
4715
+ const fbmNoise = (0, $e4b03e131ed2a289$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
4275
4716
  const fieldAngleBase = rng() * Math.PI * 2;
4276
- const fieldFreq = 0.5 + rng() * 2;
4717
+ const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
4277
4718
  function flowAngle(x, y) {
4278
- 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;
4719
+ // Sample FBM noise at the position, scaled by frequency
4720
+ const nx = x / width * fieldFreq;
4721
+ const ny = y / height * fieldFreq;
4722
+ return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
4723
+ }
4724
+ // Noise-based size modulation — shapes in "high noise" areas get scaled
4725
+ function noiseSizeModulation(x, y) {
4726
+ const n = simplexNoise(x / width * 3, y / height * 3);
4727
+ // Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
4728
+ return 0.7 + (n + 1) * 0.3;
4279
4729
  }
4280
4730
  // Track all placed shapes for density checks and connecting curves
4281
4731
  const shapePositions = [];
4732
+ // Spatial grid for O(1) density and nearest-neighbor lookups
4733
+ const densityCheckRadius = Math.min(width, height) * 0.08;
4734
+ const spatialGrid = new $4f72c5a314eddf25$var$SpatialGrid(densityCheckRadius);
4282
4735
  // Hero avoidance radius — shapes near the hero orient toward it
4283
4736
  let heroCenter = null;
4284
4737
  // ── 4b. Hero shape — a dominant focal element ───────────────────
@@ -4309,7 +4762,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4309
4762
  glowColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(heroStroke, 0.4),
4310
4763
  gradientFillEnd: (0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
4311
4764
  renderStyle: heroStyle,
4312
- rng: rng
4765
+ rng: rng,
4766
+ lightAngle: lightAngle,
4767
+ scaleFactor: scaleFactor
4313
4768
  });
4314
4769
  heroCenter = {
4315
4770
  x: heroFocal.x,
@@ -4322,9 +4777,14 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4322
4777
  size: heroSize,
4323
4778
  shape: heroShape
4324
4779
  });
4780
+ spatialGrid.insert({
4781
+ x: heroFocal.x,
4782
+ y: heroFocal.y,
4783
+ size: heroSize,
4784
+ shape: heroShape
4785
+ });
4325
4786
  }
4326
4787
  // ── 5. Shape layers ────────────────────────────────────────────
4327
- const densityCheckRadius = Math.min(width, height) * 0.08;
4328
4788
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4329
4789
  for(let layer = 0; layer < layers; layer++){
4330
4790
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
@@ -4363,12 +4823,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4363
4823
  if ($4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones)) {
4364
4824
  if (rng() < 0.85) continue;
4365
4825
  }
4366
- if ($4f72c5a314eddf25$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
4826
+ if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
4367
4827
  if (rng() < 0.6) continue;
4368
4828
  }
4369
4829
  // Power distribution for size — archetype controls the curve
4370
4830
  const sizeT = Math.pow(rng(), archetype.sizePower);
4371
- const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
4831
+ const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
4372
4832
  // Size fraction for affinity-aware shape selection
4373
4833
  const sizeFraction = size / adjustedMaxSize;
4374
4834
  // Palette-driven shape selection (replaces naive pickShape)
@@ -4423,17 +4883,11 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4423
4883
  let finalX = x;
4424
4884
  let finalY = y;
4425
4885
  if (shapePositions.length > 0 && rng() < 0.25) {
4426
- // Find nearest placed shape
4427
- let nearestDist = Infinity;
4428
- let nearestPos = null;
4429
- for (const sp of shapePositions){
4430
- const d = Math.hypot(x - sp.x, y - sp.y);
4431
- if (d < nearestDist && d > 0) {
4432
- nearestDist = d;
4433
- nearestPos = sp;
4434
- }
4435
- }
4886
+ // Use spatial grid for O(1) nearest-neighbor lookup
4887
+ const searchRadius = adjustedMaxSize * 3;
4888
+ const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
4436
4889
  if (nearestPos) {
4890
+ const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
4437
4891
  // Target distance: edges kissing (sum of half-sizes)
4438
4892
  const targetDist = (size + nearestPos.size) * 0.5;
4439
4893
  if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
@@ -4471,7 +4925,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4471
4925
  glowColor: hasGlow ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
4472
4926
  gradientFillEnd: gradientEnd,
4473
4927
  renderStyle: finalRenderStyle,
4474
- rng: rng
4928
+ rng: rng,
4929
+ lightAngle: lightAngle,
4930
+ scaleFactor: scaleFactor
4475
4931
  };
4476
4932
  if (shouldMirror) (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4477
4933
  ...shapeConfig,
@@ -4504,6 +4960,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4504
4960
  size: size,
4505
4961
  shape: shape
4506
4962
  });
4963
+ spatialGrid.insert({
4964
+ x: finalX,
4965
+ y: finalY,
4966
+ size: size,
4967
+ shape: shape
4968
+ });
4507
4969
  // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
4508
4970
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
4509
4971
  const echoCount = 2 + Math.floor(rng() * 2);
@@ -4532,6 +4994,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4532
4994
  size: echoSize,
4533
4995
  shape: shape
4534
4996
  });
4997
+ spatialGrid.insert({
4998
+ x: echoX,
4999
+ y: echoY,
5000
+ size: echoSize,
5001
+ shape: shape
5002
+ });
4535
5003
  }
4536
5004
  }
4537
5005
  // ── 5d. Recursive nesting ──────────────────────────────────
@@ -4596,12 +5064,119 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4596
5064
  size: member.size,
4597
5065
  shape: memberShape
4598
5066
  });
5067
+ spatialGrid.insert({
5068
+ x: mx,
5069
+ y: my,
5070
+ size: member.size,
5071
+ shape: memberShape
5072
+ });
5073
+ }
5074
+ }
5075
+ // ── 5f. Rhythm placement — deliberate geometric progressions ──
5076
+ // ~12% of medium-large shapes spawn a rhythmic sequence
5077
+ if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
5078
+ const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
5079
+ const rhythmAngle = rng() * Math.PI * 2;
5080
+ const rhythmSpacing = size * (0.8 + rng() * 0.6);
5081
+ const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
5082
+ const rhythmShape = shape; // same shape for visual rhythm
5083
+ let rhythmSize = size * 0.6;
5084
+ for(let r = 0; r < rhythmCount; r++){
5085
+ const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
5086
+ const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
5087
+ if (rx < 0 || rx > width || ry < 0 || ry > height) break;
5088
+ if ($4f72c5a314eddf25$var$isInVoidZone(rx, ry, voidZones)) break;
5089
+ rhythmSize *= rhythmDecay;
5090
+ if (rhythmSize < adjustedMinSize) break;
5091
+ const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
5092
+ ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
5093
+ const rhythmFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
5094
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
5095
+ fillColor: rhythmFill,
5096
+ strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.5),
5097
+ strokeWidth: strokeWidth * 0.7,
5098
+ size: rhythmSize,
5099
+ rotation: rotation + (r + 1) * 12,
5100
+ proportionType: "GOLDEN_RATIO",
5101
+ renderStyle: finalRenderStyle,
5102
+ rng: rng
5103
+ });
5104
+ shapePositions.push({
5105
+ x: rx,
5106
+ y: ry,
5107
+ size: rhythmSize,
5108
+ shape: rhythmShape
5109
+ });
5110
+ spatialGrid.insert({
5111
+ x: rx,
5112
+ y: ry,
5113
+ size: rhythmSize,
5114
+ shape: rhythmShape
5115
+ });
4599
5116
  }
4600
5117
  }
4601
5118
  }
4602
5119
  }
4603
5120
  // Reset blend mode for post-processing passes
4604
5121
  ctx.globalCompositeOperation = "source-over";
5122
+ // ── 5g. Layered masking / cutout portals ───────────────────────
5123
+ // ~18% of images get 1-3 portal windows that paint over foreground
5124
+ // with a tinted background wash, creating a "peek through" effect.
5125
+ if (rng() < 0.18 && shapePositions.length > 3) {
5126
+ const portalCount = 1 + Math.floor(rng() * 2);
5127
+ for(let p = 0; p < portalCount; p++){
5128
+ // Pick a position biased toward placed shapes
5129
+ const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
5130
+ const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
5131
+ const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
5132
+ const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
5133
+ // Pick a portal shape from the palette
5134
+ const portalShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
5135
+ const portalRotation = rng() * 360;
5136
+ const portalAlpha = 0.6 + rng() * 0.35;
5137
+ ctx.save();
5138
+ ctx.translate(portalX, portalY);
5139
+ ctx.rotate(portalRotation * Math.PI / 180);
5140
+ // Step 1: Clip to the portal shape and fill with background wash
5141
+ ctx.beginPath();
5142
+ (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
5143
+ ctx.clip();
5144
+ // Fill the clipped region with a radial gradient from background colors
5145
+ const portalColor = (0, $d016ad53434219a1$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
5146
+ const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
5147
+ portalGrad.addColorStop(0, portalColor);
5148
+ portalGrad.addColorStop(1, bgEnd);
5149
+ ctx.globalAlpha = portalAlpha;
5150
+ ctx.fillStyle = portalGrad;
5151
+ ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
5152
+ // Optional: subtle inner texture — a few tiny dots inside the portal
5153
+ if (rng() < 0.5) {
5154
+ const dotCount = 3 + Math.floor(rng() * 5);
5155
+ ctx.globalAlpha = portalAlpha * 0.3;
5156
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
5157
+ for(let d = 0; d < dotCount; d++){
5158
+ const dx = (rng() - 0.5) * portalSize * 1.4;
5159
+ const dy = (rng() - 0.5) * portalSize * 1.4;
5160
+ const dr = (1 + rng() * 3) * scaleFactor;
5161
+ ctx.beginPath();
5162
+ ctx.arc(dx, dy, dr, 0, Math.PI * 2);
5163
+ ctx.fill();
5164
+ }
5165
+ }
5166
+ ctx.restore();
5167
+ // Step 2: Draw a border ring around the portal (outside the clip)
5168
+ ctx.save();
5169
+ ctx.translate(portalX, portalY);
5170
+ ctx.rotate(portalRotation * Math.PI / 180);
5171
+ ctx.globalAlpha = 0.15 + rng() * 0.2;
5172
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
5173
+ ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
5174
+ ctx.beginPath();
5175
+ (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
5176
+ ctx.stroke();
5177
+ ctx.restore();
5178
+ }
5179
+ }
4605
5180
  // ── 6. Flow-line pass — variable color, branching, pressure ────
4606
5181
  const baseFlowLines = 6 + Math.floor(rng() * 10);
4607
5182
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
@@ -4625,6 +5200,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4625
5200
  fx += Math.cos(angle) * stepLen;
4626
5201
  fy += Math.sin(angle) * stepLen;
4627
5202
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
5203
+ // Skip segments that pass through void zones
5204
+ if ($4f72c5a314eddf25$var$isInVoidZone(fx, fy, voidZones)) {
5205
+ prevX = fx;
5206
+ prevY = fy;
5207
+ continue;
5208
+ }
4628
5209
  const t = s / steps;
4629
5210
  // Taper + pressure
4630
5211
  const taper = 1 - t * 0.8;
@@ -4722,30 +5303,60 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4722
5303
  }
4723
5304
  ctx.restore();
4724
5305
  }
4725
- // ── 7. Noise texture overlay ───────────────────────────────────
5306
+ // ── 7. Noise texture overlay — batched via ImageData ─────────────
4726
5307
  const noiseRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 777));
4727
5308
  const noiseDensity = Math.floor(width * height / 800);
4728
- for(let i = 0; i < noiseDensity; i++){
4729
- const nx = noiseRng() * width;
4730
- const ny = noiseRng() * height;
4731
- const brightness = noiseRng() > 0.5 ? 255 : 0;
4732
- const alpha = 0.01 + noiseRng() * 0.03;
4733
- ctx.globalAlpha = alpha;
4734
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
4735
- ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5309
+ try {
5310
+ const imageData = ctx.getImageData(0, 0, width, height);
5311
+ const data = imageData.data;
5312
+ const pixelScale = Math.max(1, Math.round(scaleFactor));
5313
+ for(let i = 0; i < noiseDensity; i++){
5314
+ const nx = Math.floor(noiseRng() * width);
5315
+ const ny = Math.floor(noiseRng() * height);
5316
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5317
+ const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
5318
+ // Write a small block of pixels for scale
5319
+ for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
5320
+ const idx = ((ny + dy) * width + (nx + dx)) * 4;
5321
+ // Alpha-blend the noise dot onto existing pixel data
5322
+ const srcA = alpha / 255;
5323
+ const invA = 1 - srcA;
5324
+ data[idx] = Math.round(data[idx] * invA + brightness * srcA);
5325
+ data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
5326
+ data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
5327
+ // Keep existing alpha
5328
+ }
5329
+ }
5330
+ ctx.putImageData(imageData, 0, 0);
5331
+ } catch {
5332
+ // Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
5333
+ for(let i = 0; i < noiseDensity; i++){
5334
+ const nx = noiseRng() * width;
5335
+ const ny = noiseRng() * height;
5336
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5337
+ const alpha = 0.01 + noiseRng() * 0.03;
5338
+ ctx.globalAlpha = alpha;
5339
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5340
+ ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5341
+ }
4736
5342
  }
4737
5343
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
4738
5344
  ctx.globalAlpha = 1;
4739
5345
  const vignetteStrength = 0.25 + rng() * 0.2;
4740
5346
  const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
5347
+ // Tint vignette based on background: warm sepia for light, cool blue for dark
5348
+ const isLightBg = bgLum > 0.5;
5349
+ const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
5350
+ : `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
4741
5351
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
4742
5352
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
4743
- vigGrad.addColorStop(1, `rgba(0,0,0,${vignetteStrength.toFixed(3)})`);
5353
+ vigGrad.addColorStop(1, vignetteColor);
4744
5354
  ctx.fillStyle = vigGrad;
4745
5355
  ctx.fillRect(0, 0, width, height);
4746
- // ── 9. Organic connecting curves ───────────────────────────────
5356
+ // ── 9. Organic connecting curves — proximity-aware ───────────────
4747
5357
  if (shapePositions.length > 1) {
4748
5358
  const numCurves = Math.floor(8 * (width * height) / 1048576);
5359
+ const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
4749
5360
  ctx.lineWidth = 0.8 * scaleFactor;
4750
5361
  for(let i = 0; i < numCurves; i++){
4751
5362
  const idxA = Math.floor(rng() * shapePositions.length);
@@ -4753,11 +5364,13 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4753
5364
  const idxB = (idxA + offset) % shapePositions.length;
4754
5365
  const a = shapePositions[idxA];
4755
5366
  const b = shapePositions[idxB];
4756
- const mx = (a.x + b.x) / 2;
4757
- const my = (a.y + b.y) / 2;
4758
5367
  const dx = b.x - a.x;
4759
5368
  const dy = b.y - a.y;
4760
5369
  const dist = Math.hypot(dx, dy);
5370
+ // Skip connections between distant shapes
5371
+ if (dist > maxCurveDist) continue;
5372
+ const mx = (a.x + b.x) / 2;
5373
+ const my = (a.y + b.y) / 2;
4761
5374
  const bulge = (rng() - 0.5) * dist * 0.4;
4762
5375
  const cpx = mx + -dy / (dist || 1) * bulge;
4763
5376
  const cpy = my + dx / (dist || 1) * bulge;
@@ -4813,13 +5426,211 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4813
5426
  ctx.restore();
4814
5427
  ctx.globalCompositeOperation = "source-over";
4815
5428
  }
4816
- // ── 11. Signature markunique geometric chop from hash prefix ──
5429
+ // 10d. Gradient mapmap luminance through a two-color gradient
5430
+ // Uses dominant→accent as the dark→light ramp for a cohesive tonal look
5431
+ if (rng() < 0.35) {
5432
+ const gmDark = colorHierarchy.dominant;
5433
+ const gmLight = colorHierarchy.accent;
5434
+ ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
5435
+ ctx.globalCompositeOperation = "color";
5436
+ // Paint a linear gradient from dark color (top) to light color (bottom)
5437
+ const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
5438
+ gmGrad.addColorStop(0, gmDark);
5439
+ gmGrad.addColorStop(1, gmLight);
5440
+ ctx.fillStyle = gmGrad;
5441
+ ctx.fillRect(0, 0, width, height);
5442
+ ctx.globalCompositeOperation = "source-over";
5443
+ }
5444
+ // ── 10e. Generative borders — archetype-driven decorative frames ──
5445
+ {
5446
+ ctx.save();
5447
+ ctx.globalAlpha = 1;
5448
+ ctx.globalCompositeOperation = "source-over";
5449
+ const borderRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 314));
5450
+ const borderPad = Math.min(width, height) * 0.025;
5451
+ const borderColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5452
+ const borderColorSolid = colorHierarchy.accent;
5453
+ const archName = archetype.name;
5454
+ if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
5455
+ // Clean ruled lines with corner ornaments
5456
+ ctx.strokeStyle = borderColor;
5457
+ ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
5458
+ ctx.globalAlpha = 0.18 + borderRng() * 0.1;
5459
+ // Outer rule
5460
+ ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
5461
+ // Inner rule (thinner, offset)
5462
+ const innerPad = borderPad * 1.8;
5463
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5464
+ ctx.globalAlpha *= 0.7;
5465
+ ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
5466
+ // Corner ornaments — small squares at each corner
5467
+ const ornSize = borderPad * 0.6;
5468
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(borderColorSolid, 0.12);
5469
+ const corners = [
5470
+ [
5471
+ borderPad,
5472
+ borderPad
5473
+ ],
5474
+ [
5475
+ width - borderPad - ornSize,
5476
+ borderPad
5477
+ ],
5478
+ [
5479
+ borderPad,
5480
+ height - borderPad - ornSize
5481
+ ],
5482
+ [
5483
+ width - borderPad - ornSize,
5484
+ height - borderPad - ornSize
5485
+ ]
5486
+ ];
5487
+ for (const [cx2, cy2] of corners){
5488
+ ctx.fillRect(cx2, cy2, ornSize, ornSize);
5489
+ // Diagonal cross inside ornament
5490
+ ctx.beginPath();
5491
+ ctx.moveTo(cx2, cy2);
5492
+ ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
5493
+ ctx.moveTo(cx2 + ornSize, cy2);
5494
+ ctx.lineTo(cx2, cy2 + ornSize);
5495
+ ctx.stroke();
5496
+ }
5497
+ } else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
5498
+ // Vine tendrils — organic curving lines along edges
5499
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
5500
+ ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
5501
+ ctx.globalAlpha = 0.12 + borderRng() * 0.08;
5502
+ ctx.lineCap = "round";
5503
+ const tendrilCount = 8 + Math.floor(borderRng() * 8);
5504
+ for(let t = 0; t < tendrilCount; t++){
5505
+ // Start from a random edge point
5506
+ const edge = Math.floor(borderRng() * 4);
5507
+ let tx, ty;
5508
+ if (edge === 0) {
5509
+ tx = borderRng() * width;
5510
+ ty = borderPad;
5511
+ } else if (edge === 1) {
5512
+ tx = borderRng() * width;
5513
+ ty = height - borderPad;
5514
+ } else if (edge === 2) {
5515
+ tx = borderPad;
5516
+ ty = borderRng() * height;
5517
+ } else {
5518
+ tx = width - borderPad;
5519
+ ty = borderRng() * height;
5520
+ }
5521
+ ctx.beginPath();
5522
+ ctx.moveTo(tx, ty);
5523
+ const segs = 3 + Math.floor(borderRng() * 4);
5524
+ for(let s = 0; s < segs; s++){
5525
+ const inward = borderPad * (1 + borderRng() * 2);
5526
+ // Curl inward from edge
5527
+ const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
5528
+ const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
5529
+ const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
5530
+ const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
5531
+ tx = cpx3;
5532
+ ty = cpy3;
5533
+ ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
5534
+ }
5535
+ ctx.stroke();
5536
+ // Small leaf/dot at tendril end
5537
+ if (borderRng() < 0.6) {
5538
+ ctx.beginPath();
5539
+ ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
5540
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
5541
+ ctx.fill();
5542
+ }
5543
+ }
5544
+ } else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
5545
+ // Star-studded arcs along edges
5546
+ ctx.globalAlpha = 0.1 + borderRng() * 0.08;
5547
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5548
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
5549
+ ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
5550
+ // Subtle arc along top and bottom
5551
+ ctx.beginPath();
5552
+ ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
5553
+ ctx.stroke();
5554
+ ctx.beginPath();
5555
+ ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
5556
+ ctx.stroke();
5557
+ // Scatter small stars along the border region
5558
+ const starCount = 15 + Math.floor(borderRng() * 15);
5559
+ for(let s = 0; s < starCount; s++){
5560
+ const edge = Math.floor(borderRng() * 4);
5561
+ let sx, sy;
5562
+ if (edge === 0) {
5563
+ sx = borderRng() * width;
5564
+ sy = borderPad * (0.5 + borderRng());
5565
+ } else if (edge === 1) {
5566
+ sx = borderRng() * width;
5567
+ sy = height - borderPad * (0.5 + borderRng());
5568
+ } else if (edge === 2) {
5569
+ sx = borderPad * (0.5 + borderRng());
5570
+ sy = borderRng() * height;
5571
+ } else {
5572
+ sx = width - borderPad * (0.5 + borderRng());
5573
+ sy = borderRng() * height;
5574
+ }
5575
+ const starR = (1 + borderRng() * 2.5) * scaleFactor;
5576
+ // 4-point star
5577
+ ctx.beginPath();
5578
+ for(let p = 0; p < 8; p++){
5579
+ const a = p / 8 * Math.PI * 2;
5580
+ const r = p % 2 === 0 ? starR : starR * 0.4;
5581
+ const px2 = sx + Math.cos(a) * r;
5582
+ const py2 = sy + Math.sin(a) * r;
5583
+ if (p === 0) ctx.moveTo(px2, py2);
5584
+ else ctx.lineTo(px2, py2);
5585
+ }
5586
+ ctx.closePath();
5587
+ ctx.fill();
5588
+ }
5589
+ } else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
5590
+ // Thin single rule — understated elegance
5591
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
5592
+ ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
5593
+ ctx.globalAlpha = 0.1 + borderRng() * 0.06;
5594
+ ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
5595
+ }
5596
+ // Other archetypes: no border (intentional — not every image needs one)
5597
+ ctx.restore();
5598
+ }
5599
+ // ── 11. Signature mark — placed in the least-dense corner ──────
4817
5600
  {
4818
5601
  const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));
4819
5602
  const sigSize = Math.min(width, height) * 0.025;
4820
- // Bottom-right corner with padding
4821
- const sigX = width - sigSize * 2.5;
4822
- const sigY = height - sigSize * 2.5;
5603
+ const sigMargin = sigSize * 2.5;
5604
+ // Find the corner with the lowest local density
5605
+ const cornerCandidates = [
5606
+ {
5607
+ x: sigMargin,
5608
+ y: sigMargin
5609
+ },
5610
+ {
5611
+ x: width - sigMargin,
5612
+ y: sigMargin
5613
+ },
5614
+ {
5615
+ x: sigMargin,
5616
+ y: height - sigMargin
5617
+ },
5618
+ {
5619
+ x: width - sigMargin,
5620
+ y: height - sigMargin
5621
+ }
5622
+ ];
5623
+ let bestCorner = cornerCandidates[3]; // default: bottom-right
5624
+ let minDensity = Infinity;
5625
+ for (const corner of cornerCandidates){
5626
+ const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
5627
+ if (density < minDensity) {
5628
+ minDensity = density;
5629
+ bestCorner = corner;
5630
+ }
5631
+ }
5632
+ const sigX = bestCorner.x;
5633
+ const sigY = bestCorner.y;
4823
5634
  const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
4824
5635
  const sigColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
4825
5636
  ctx.save();