git-hash-art 0.10.1 → 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
@@ -603,15 +603,17 @@ function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
603
603
  accent: colors[colors.length - 1] || "#888888",
604
604
  all: colors
605
605
  };
606
- // 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
607
608
  const hsls = colors.map((c)=>$d016ad53434219a1$var$hexToHsl(c));
608
- const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
609
609
  let dominantIdx = 0;
610
- let minDist = 360;
610
+ let maxChroma = -1;
611
611
  for(let i = 0; i < hsls.length; i++){
612
- const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
613
- if (d < minDist) {
614
- 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;
615
617
  dominantIdx = i;
616
618
  }
617
619
  }
@@ -2508,16 +2510,26 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2508
2510
  ctx.shadowOffsetX = 0;
2509
2511
  ctx.shadowOffsetY = 0;
2510
2512
  ctx.shadowColor = "transparent";
2511
- // ── Specular highlight — bright arc on the light-facing side ──
2513
+ // ── Specular highlight — tinted arc on the light-facing side ──
2512
2514
  if (lightAngle !== undefined && size > 15 && rng) {
2513
2515
  const hlRadius = size * 0.35;
2514
2516
  const hlDist = size * 0.15;
2515
2517
  const hlX = Math.cos(lightAngle) * hlDist;
2516
2518
  const hlY = Math.sin(lightAngle) * hlDist;
2517
2519
  const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2518
- hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
2519
- hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
2520
- hlGrad.addColorStop(1, "rgba(255,255,255,0)");
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)`);
2521
2533
  const savedOp = ctx.globalCompositeOperation;
2522
2534
  ctx.globalCompositeOperation = "soft-light";
2523
2535
  ctx.fillStyle = hlGrad;
@@ -3579,6 +3591,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3579
3591
  "watercolor",
3580
3592
  "fill-only"
3581
3593
  ],
3594
+ preferredCompositions: [
3595
+ "clustered",
3596
+ "flow-field",
3597
+ "radial"
3598
+ ],
3582
3599
  flowLineMultiplier: 2.5,
3583
3600
  heroShape: false,
3584
3601
  glowMultiplier: 0.5,
@@ -3600,6 +3617,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3600
3617
  "stroke-only",
3601
3618
  "incomplete"
3602
3619
  ],
3620
+ preferredCompositions: [
3621
+ "golden-spiral",
3622
+ "grid-subdivision"
3623
+ ],
3603
3624
  flowLineMultiplier: 0.3,
3604
3625
  heroShape: true,
3605
3626
  glowMultiplier: 0,
@@ -3621,6 +3642,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3621
3642
  "fill-only",
3622
3643
  "incomplete"
3623
3644
  ],
3645
+ preferredCompositions: [
3646
+ "flow-field",
3647
+ "golden-spiral",
3648
+ "spiral"
3649
+ ],
3624
3650
  flowLineMultiplier: 4,
3625
3651
  heroShape: false,
3626
3652
  glowMultiplier: 0.3,
@@ -3643,6 +3669,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3643
3669
  "double-stroke",
3644
3670
  "hatched"
3645
3671
  ],
3672
+ preferredCompositions: [
3673
+ "grid-subdivision",
3674
+ "radial"
3675
+ ],
3646
3676
  flowLineMultiplier: 0,
3647
3677
  heroShape: false,
3648
3678
  glowMultiplier: 0,
@@ -3664,6 +3694,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3664
3694
  "incomplete",
3665
3695
  "fill-only"
3666
3696
  ],
3697
+ preferredCompositions: [
3698
+ "golden-spiral",
3699
+ "radial",
3700
+ "spiral"
3701
+ ],
3667
3702
  flowLineMultiplier: 1.5,
3668
3703
  heroShape: true,
3669
3704
  glowMultiplier: 2,
@@ -3684,6 +3719,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3684
3719
  "fill-and-stroke",
3685
3720
  "double-stroke"
3686
3721
  ],
3722
+ preferredCompositions: [
3723
+ "grid-subdivision",
3724
+ "golden-spiral"
3725
+ ],
3687
3726
  flowLineMultiplier: 0,
3688
3727
  heroShape: true,
3689
3728
  glowMultiplier: 0,
@@ -3705,6 +3744,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3705
3744
  "double-stroke",
3706
3745
  "dashed"
3707
3746
  ],
3747
+ preferredCompositions: [
3748
+ "radial",
3749
+ "spiral",
3750
+ "clustered"
3751
+ ],
3708
3752
  flowLineMultiplier: 2,
3709
3753
  heroShape: true,
3710
3754
  glowMultiplier: 3,
@@ -3727,6 +3771,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3727
3771
  "stroke-only",
3728
3772
  "dashed"
3729
3773
  ],
3774
+ preferredCompositions: [
3775
+ "flow-field",
3776
+ "grid-subdivision",
3777
+ "clustered"
3778
+ ],
3730
3779
  flowLineMultiplier: 1.5,
3731
3780
  heroShape: false,
3732
3781
  glowMultiplier: 0,
@@ -3748,6 +3797,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3748
3797
  "watercolor",
3749
3798
  "fill-and-stroke"
3750
3799
  ],
3800
+ preferredCompositions: [
3801
+ "radial",
3802
+ "spiral",
3803
+ "golden-spiral"
3804
+ ],
3751
3805
  flowLineMultiplier: 3,
3752
3806
  heroShape: true,
3753
3807
  glowMultiplier: 2.5,
@@ -3769,6 +3823,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3769
3823
  "fill-only",
3770
3824
  "incomplete"
3771
3825
  ],
3826
+ preferredCompositions: [
3827
+ "golden-spiral",
3828
+ "flow-field",
3829
+ "radial"
3830
+ ],
3772
3831
  flowLineMultiplier: 0.5,
3773
3832
  heroShape: false,
3774
3833
  glowMultiplier: 0.3,
@@ -3790,6 +3849,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3790
3849
  "stroke-only",
3791
3850
  "dashed"
3792
3851
  ],
3852
+ preferredCompositions: [
3853
+ "grid-subdivision",
3854
+ "radial"
3855
+ ],
3793
3856
  flowLineMultiplier: 0,
3794
3857
  heroShape: false,
3795
3858
  glowMultiplier: 0,
@@ -3811,6 +3874,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3811
3874
  "fill-only",
3812
3875
  "double-stroke"
3813
3876
  ],
3877
+ preferredCompositions: [
3878
+ "grid-subdivision",
3879
+ "clustered"
3880
+ ],
3814
3881
  flowLineMultiplier: 0,
3815
3882
  heroShape: true,
3816
3883
  glowMultiplier: 0,
@@ -3832,6 +3899,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3832
3899
  "watercolor",
3833
3900
  "fill-only"
3834
3901
  ],
3902
+ preferredCompositions: [
3903
+ "radial",
3904
+ "golden-spiral",
3905
+ "flow-field"
3906
+ ],
3835
3907
  flowLineMultiplier: 1,
3836
3908
  heroShape: true,
3837
3909
  glowMultiplier: 1,
@@ -3853,6 +3925,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3853
3925
  "stroke-only",
3854
3926
  "fill-only"
3855
3927
  ],
3928
+ preferredCompositions: [
3929
+ "clustered",
3930
+ "grid-subdivision",
3931
+ "radial"
3932
+ ],
3856
3933
  flowLineMultiplier: 0,
3857
3934
  heroShape: false,
3858
3935
  glowMultiplier: 0.3,
@@ -3874,6 +3951,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3874
3951
  "fill-only",
3875
3952
  "incomplete"
3876
3953
  ],
3954
+ preferredCompositions: [
3955
+ "flow-field",
3956
+ "golden-spiral",
3957
+ "spiral"
3958
+ ],
3877
3959
  flowLineMultiplier: 3,
3878
3960
  heroShape: true,
3879
3961
  glowMultiplier: 0.2,
@@ -3895,6 +3977,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3895
3977
  "fill-only",
3896
3978
  "hatched"
3897
3979
  ],
3980
+ preferredCompositions: [
3981
+ "radial",
3982
+ "clustered",
3983
+ "flow-field"
3984
+ ],
3898
3985
  flowLineMultiplier: 0,
3899
3986
  heroShape: false,
3900
3987
  glowMultiplier: 0,
@@ -3917,6 +4004,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3917
4004
  "stroke-only",
3918
4005
  "incomplete"
3919
4006
  ],
4007
+ preferredCompositions: [
4008
+ "spiral",
4009
+ "radial",
4010
+ "golden-spiral"
4011
+ ],
3920
4012
  flowLineMultiplier: 2,
3921
4013
  heroShape: true,
3922
4014
  glowMultiplier: 2.5,
@@ -3940,6 +4032,12 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3940
4032
  ...b.preferredStyles
3941
4033
  ])
3942
4034
  ];
4035
+ const mergedCompositions = [
4036
+ ...new Set([
4037
+ ...a.preferredCompositions,
4038
+ ...b.preferredCompositions
4039
+ ])
4040
+ ];
3943
4041
  return {
3944
4042
  name: `${a.name}+${b.name}`,
3945
4043
  gridSize: Math.round($f89bc858f7202849$var$lerpNum(a.gridSize, b.gridSize, t)),
@@ -3951,6 +4049,7 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3951
4049
  backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3952
4050
  paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3953
4051
  preferredStyles: mergedStyles,
4052
+ preferredCompositions: mergedCompositions,
3954
4053
  flowLineMultiplier: $f89bc858f7202849$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3955
4054
  heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3956
4055
  glowMultiplier: $f89bc858f7202849$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
@@ -3984,7 +4083,8 @@ const $4f72c5a314eddf25$var$SACRED_SHAPES = [
3984
4083
  "torus",
3985
4084
  "eggOfLife"
3986
4085
  ];
3987
- const $4f72c5a314eddf25$var$COMPOSITION_MODES = [
4086
+ // ── Composition modes ───────────────────────────────────────────────
4087
+ const $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES = [
3988
4088
  "radial",
3989
4089
  "flow-field",
3990
4090
  "spiral",
@@ -4086,7 +4186,69 @@ function $4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones) {
4086
4186
  }
4087
4187
  return false;
4088
4188
  }
4089
- // ── 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) ──────────────────────────
4090
4252
  function $4f72c5a314eddf25$var$localDensity(x, y, positions, radius) {
4091
4253
  let count = 0;
4092
4254
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -4385,42 +4547,43 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4385
4547
  const patternOpacity = 0.02 + rng() * 0.04;
4386
4548
  const patternColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4387
4549
  if (bgPatternRoll < 0.2) {
4388
- // Dot grid
4550
+ // Dot grid — batched into a single path
4389
4551
  const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4390
4552
  const dotR = dotSpacing * 0.08;
4391
4553
  ctx.globalAlpha = patternOpacity;
4392
4554
  ctx.fillStyle = patternColor;
4555
+ ctx.beginPath();
4393
4556
  for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4394
- ctx.beginPath();
4557
+ ctx.moveTo(px + dotR, py);
4395
4558
  ctx.arc(px, py, dotR, 0, Math.PI * 2);
4396
- ctx.fill();
4397
4559
  }
4560
+ ctx.fill();
4398
4561
  } else if (bgPatternRoll < 0.4) {
4399
- // Diagonal lines
4562
+ // Diagonal lines — batched into a single path
4400
4563
  const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4401
4564
  ctx.globalAlpha = patternOpacity;
4402
4565
  ctx.strokeStyle = patternColor;
4403
4566
  ctx.lineWidth = 0.5 * scaleFactor;
4404
4567
  const diag = Math.hypot(width, height);
4568
+ ctx.beginPath();
4405
4569
  for(let d = -diag; d < diag; d += lineSpacing){
4406
- ctx.beginPath();
4407
4570
  ctx.moveTo(d, 0);
4408
4571
  ctx.lineTo(d + height, height);
4409
- ctx.stroke();
4410
4572
  }
4573
+ ctx.stroke();
4411
4574
  } else {
4412
- // Tessellation — hexagonal grid of tiny shapes
4575
+ // Tessellation — hexagonal grid, batched into a single path
4413
4576
  const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4414
4577
  const tessH = tessSize * Math.sqrt(3);
4415
4578
  ctx.globalAlpha = patternOpacity * 0.7;
4416
4579
  ctx.strokeStyle = patternColor;
4417
4580
  ctx.lineWidth = 0.4 * scaleFactor;
4581
+ ctx.beginPath();
4418
4582
  for(let row = 0; row * tessH < height + tessH; row++){
4419
4583
  const offsetX = row % 2 * tessSize * 0.75;
4420
4584
  for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4421
4585
  const hx = col * tessSize * 1.5 + offsetX;
4422
4586
  const hy = row * tessH;
4423
- ctx.beginPath();
4424
4587
  for(let s = 0; s < 6; s++){
4425
4588
  const angle = Math.PI / 3 * s - Math.PI / 6;
4426
4589
  const vx = hx + Math.cos(angle) * tessSize * 0.5;
@@ -4429,18 +4592,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4429
4592
  else ctx.lineTo(vx, vy);
4430
4593
  }
4431
4594
  ctx.closePath();
4432
- ctx.stroke();
4433
4595
  }
4434
4596
  }
4597
+ ctx.stroke();
4435
4598
  }
4436
4599
  ctx.restore();
4437
4600
  }
4438
4601
  ctx.globalCompositeOperation = "source-over";
4439
- // ── 2. Composition mode ────────────────────────────────────────
4440
- 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)];
4441
4604
  const symRoll = rng();
4442
4605
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
4443
- // ── 3. Focal points + void zones ───────────────────────────────
4606
+ // ── 3. Focal points + void zones (archetype-aware) ───────────────
4444
4607
  const THIRDS_POINTS = [
4445
4608
  {
4446
4609
  x: 1 / 3,
@@ -4473,9 +4636,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4473
4636
  y: height * (0.2 + rng() * 0.6),
4474
4637
  strength: 0.3 + rng() * 0.4
4475
4638
  });
4476
- 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;
4477
4645
  const voidZones = [];
4478
- 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({
4479
4656
  x: width * (0.15 + rng() * 0.7),
4480
4657
  y: height * (0.15 + rng() * 0.7),
4481
4658
  radius: Math.min(width, height) * (0.06 + rng() * 0.1)
@@ -4552,6 +4729,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4552
4729
  }
4553
4730
  // Track all placed shapes for density checks and connecting curves
4554
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);
4555
4735
  // Hero avoidance radius — shapes near the hero orient toward it
4556
4736
  let heroCenter = null;
4557
4737
  // ── 4b. Hero shape — a dominant focal element ───────────────────
@@ -4597,9 +4777,14 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4597
4777
  size: heroSize,
4598
4778
  shape: heroShape
4599
4779
  });
4780
+ spatialGrid.insert({
4781
+ x: heroFocal.x,
4782
+ y: heroFocal.y,
4783
+ size: heroSize,
4784
+ shape: heroShape
4785
+ });
4600
4786
  }
4601
4787
  // ── 5. Shape layers ────────────────────────────────────────────
4602
- const densityCheckRadius = Math.min(width, height) * 0.08;
4603
4788
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4604
4789
  for(let layer = 0; layer < layers; layer++){
4605
4790
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
@@ -4638,7 +4823,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4638
4823
  if ($4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones)) {
4639
4824
  if (rng() < 0.85) continue;
4640
4825
  }
4641
- if ($4f72c5a314eddf25$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
4826
+ if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
4642
4827
  if (rng() < 0.6) continue;
4643
4828
  }
4644
4829
  // Power distribution for size — archetype controls the curve
@@ -4698,17 +4883,11 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4698
4883
  let finalX = x;
4699
4884
  let finalY = y;
4700
4885
  if (shapePositions.length > 0 && rng() < 0.25) {
4701
- // Find nearest placed shape
4702
- let nearestDist = Infinity;
4703
- let nearestPos = null;
4704
- for (const sp of shapePositions){
4705
- const d = Math.hypot(x - sp.x, y - sp.y);
4706
- if (d < nearestDist && d > 0) {
4707
- nearestDist = d;
4708
- nearestPos = sp;
4709
- }
4710
- }
4886
+ // Use spatial grid for O(1) nearest-neighbor lookup
4887
+ const searchRadius = adjustedMaxSize * 3;
4888
+ const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
4711
4889
  if (nearestPos) {
4890
+ const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
4712
4891
  // Target distance: edges kissing (sum of half-sizes)
4713
4892
  const targetDist = (size + nearestPos.size) * 0.5;
4714
4893
  if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
@@ -4781,6 +4960,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4781
4960
  size: size,
4782
4961
  shape: shape
4783
4962
  });
4963
+ spatialGrid.insert({
4964
+ x: finalX,
4965
+ y: finalY,
4966
+ size: size,
4967
+ shape: shape
4968
+ });
4784
4969
  // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
4785
4970
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
4786
4971
  const echoCount = 2 + Math.floor(rng() * 2);
@@ -4809,6 +4994,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4809
4994
  size: echoSize,
4810
4995
  shape: shape
4811
4996
  });
4997
+ spatialGrid.insert({
4998
+ x: echoX,
4999
+ y: echoY,
5000
+ size: echoSize,
5001
+ shape: shape
5002
+ });
4812
5003
  }
4813
5004
  }
4814
5005
  // ── 5d. Recursive nesting ──────────────────────────────────
@@ -4873,13 +5064,62 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4873
5064
  size: member.size,
4874
5065
  shape: memberShape
4875
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
+ });
4876
5116
  }
4877
5117
  }
4878
5118
  }
4879
5119
  }
4880
5120
  // Reset blend mode for post-processing passes
4881
5121
  ctx.globalCompositeOperation = "source-over";
4882
- // ── 5f. Layered masking / cutout portals ───────────────────────
5122
+ // ── 5g. Layered masking / cutout portals ───────────────────────
4883
5123
  // ~18% of images get 1-3 portal windows that paint over foreground
4884
5124
  // with a tinted background wash, creating a "peek through" effect.
4885
5125
  if (rng() < 0.18 && shapePositions.length > 3) {
@@ -4960,6 +5200,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4960
5200
  fx += Math.cos(angle) * stepLen;
4961
5201
  fy += Math.sin(angle) * stepLen;
4962
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
+ }
4963
5209
  const t = s / steps;
4964
5210
  // Taper + pressure
4965
5211
  const taper = 1 - t * 0.8;
@@ -5057,30 +5303,60 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5057
5303
  }
5058
5304
  ctx.restore();
5059
5305
  }
5060
- // ── 7. Noise texture overlay ───────────────────────────────────
5306
+ // ── 7. Noise texture overlay — batched via ImageData ─────────────
5061
5307
  const noiseRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 777));
5062
5308
  const noiseDensity = Math.floor(width * height / 800);
5063
- for(let i = 0; i < noiseDensity; i++){
5064
- const nx = noiseRng() * width;
5065
- const ny = noiseRng() * height;
5066
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5067
- const alpha = 0.01 + noiseRng() * 0.03;
5068
- ctx.globalAlpha = alpha;
5069
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5070
- 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
+ }
5071
5342
  }
5072
5343
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
5073
5344
  ctx.globalAlpha = 1;
5074
5345
  const vignetteStrength = 0.25 + rng() * 0.2;
5075
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
5076
5351
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
5077
5352
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
5078
- vigGrad.addColorStop(1, `rgba(0,0,0,${vignetteStrength.toFixed(3)})`);
5353
+ vigGrad.addColorStop(1, vignetteColor);
5079
5354
  ctx.fillStyle = vigGrad;
5080
5355
  ctx.fillRect(0, 0, width, height);
5081
- // ── 9. Organic connecting curves ───────────────────────────────
5356
+ // ── 9. Organic connecting curves — proximity-aware ───────────────
5082
5357
  if (shapePositions.length > 1) {
5083
5358
  const numCurves = Math.floor(8 * (width * height) / 1048576);
5359
+ const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
5084
5360
  ctx.lineWidth = 0.8 * scaleFactor;
5085
5361
  for(let i = 0; i < numCurves; i++){
5086
5362
  const idxA = Math.floor(rng() * shapePositions.length);
@@ -5088,11 +5364,13 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5088
5364
  const idxB = (idxA + offset) % shapePositions.length;
5089
5365
  const a = shapePositions[idxA];
5090
5366
  const b = shapePositions[idxB];
5091
- const mx = (a.x + b.x) / 2;
5092
- const my = (a.y + b.y) / 2;
5093
5367
  const dx = b.x - a.x;
5094
5368
  const dy = b.y - a.y;
5095
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;
5096
5374
  const bulge = (rng() - 0.5) * dist * 0.4;
5097
5375
  const cpx = mx + -dy / (dist || 1) * bulge;
5098
5376
  const cpy = my + dx / (dist || 1) * bulge;
@@ -5318,13 +5596,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5318
5596
  // Other archetypes: no border (intentional — not every image needs one)
5319
5597
  ctx.restore();
5320
5598
  }
5321
- // ── 11. Signature mark — unique geometric chop from hash prefix ──
5599
+ // ── 11. Signature mark — placed in the least-dense corner ──────
5322
5600
  {
5323
5601
  const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));
5324
5602
  const sigSize = Math.min(width, height) * 0.025;
5325
- // Bottom-right corner with padding
5326
- const sigX = width - sigSize * 2.5;
5327
- 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;
5328
5634
  const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
5329
5635
  const sigColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
5330
5636
  ctx.save();