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/browser.js CHANGED
@@ -580,15 +580,17 @@ function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
580
580
  accent: colors[colors.length - 1] || "#888888",
581
581
  all: colors
582
582
  };
583
- // 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
584
585
  const hsls = colors.map((c)=>$b5a262d09b87e373$var$hexToHsl(c));
585
- const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
586
586
  let dominantIdx = 0;
587
- let minDist = 360;
587
+ let maxChroma = -1;
588
588
  for(let i = 0; i < hsls.length; i++){
589
- const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
590
- if (d < minDist) {
591
- 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;
592
594
  dominantIdx = i;
593
595
  }
594
596
  }
@@ -2485,16 +2487,26 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2485
2487
  ctx.shadowOffsetX = 0;
2486
2488
  ctx.shadowOffsetY = 0;
2487
2489
  ctx.shadowColor = "transparent";
2488
- // ── Specular highlight — bright arc on the light-facing side ──
2490
+ // ── Specular highlight — tinted arc on the light-facing side ──
2489
2491
  if (lightAngle !== undefined && size > 15 && rng) {
2490
2492
  const hlRadius = size * 0.35;
2491
2493
  const hlDist = size * 0.15;
2492
2494
  const hlX = Math.cos(lightAngle) * hlDist;
2493
2495
  const hlY = Math.sin(lightAngle) * hlDist;
2494
2496
  const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2495
- hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
2496
- hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
2497
- hlGrad.addColorStop(1, "rgba(255,255,255,0)");
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)`);
2498
2510
  const savedOp = ctx.globalCompositeOperation;
2499
2511
  ctx.globalCompositeOperation = "soft-light";
2500
2512
  ctx.fillStyle = hlGrad;
@@ -3556,6 +3568,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3556
3568
  "watercolor",
3557
3569
  "fill-only"
3558
3570
  ],
3571
+ preferredCompositions: [
3572
+ "clustered",
3573
+ "flow-field",
3574
+ "radial"
3575
+ ],
3559
3576
  flowLineMultiplier: 2.5,
3560
3577
  heroShape: false,
3561
3578
  glowMultiplier: 0.5,
@@ -3577,6 +3594,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3577
3594
  "stroke-only",
3578
3595
  "incomplete"
3579
3596
  ],
3597
+ preferredCompositions: [
3598
+ "golden-spiral",
3599
+ "grid-subdivision"
3600
+ ],
3580
3601
  flowLineMultiplier: 0.3,
3581
3602
  heroShape: true,
3582
3603
  glowMultiplier: 0,
@@ -3598,6 +3619,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3598
3619
  "fill-only",
3599
3620
  "incomplete"
3600
3621
  ],
3622
+ preferredCompositions: [
3623
+ "flow-field",
3624
+ "golden-spiral",
3625
+ "spiral"
3626
+ ],
3601
3627
  flowLineMultiplier: 4,
3602
3628
  heroShape: false,
3603
3629
  glowMultiplier: 0.3,
@@ -3620,6 +3646,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3620
3646
  "double-stroke",
3621
3647
  "hatched"
3622
3648
  ],
3649
+ preferredCompositions: [
3650
+ "grid-subdivision",
3651
+ "radial"
3652
+ ],
3623
3653
  flowLineMultiplier: 0,
3624
3654
  heroShape: false,
3625
3655
  glowMultiplier: 0,
@@ -3641,6 +3671,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3641
3671
  "incomplete",
3642
3672
  "fill-only"
3643
3673
  ],
3674
+ preferredCompositions: [
3675
+ "golden-spiral",
3676
+ "radial",
3677
+ "spiral"
3678
+ ],
3644
3679
  flowLineMultiplier: 1.5,
3645
3680
  heroShape: true,
3646
3681
  glowMultiplier: 2,
@@ -3661,6 +3696,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3661
3696
  "fill-and-stroke",
3662
3697
  "double-stroke"
3663
3698
  ],
3699
+ preferredCompositions: [
3700
+ "grid-subdivision",
3701
+ "golden-spiral"
3702
+ ],
3664
3703
  flowLineMultiplier: 0,
3665
3704
  heroShape: true,
3666
3705
  glowMultiplier: 0,
@@ -3682,6 +3721,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3682
3721
  "double-stroke",
3683
3722
  "dashed"
3684
3723
  ],
3724
+ preferredCompositions: [
3725
+ "radial",
3726
+ "spiral",
3727
+ "clustered"
3728
+ ],
3685
3729
  flowLineMultiplier: 2,
3686
3730
  heroShape: true,
3687
3731
  glowMultiplier: 3,
@@ -3704,6 +3748,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3704
3748
  "stroke-only",
3705
3749
  "dashed"
3706
3750
  ],
3751
+ preferredCompositions: [
3752
+ "flow-field",
3753
+ "grid-subdivision",
3754
+ "clustered"
3755
+ ],
3707
3756
  flowLineMultiplier: 1.5,
3708
3757
  heroShape: false,
3709
3758
  glowMultiplier: 0,
@@ -3725,6 +3774,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3725
3774
  "watercolor",
3726
3775
  "fill-and-stroke"
3727
3776
  ],
3777
+ preferredCompositions: [
3778
+ "radial",
3779
+ "spiral",
3780
+ "golden-spiral"
3781
+ ],
3728
3782
  flowLineMultiplier: 3,
3729
3783
  heroShape: true,
3730
3784
  glowMultiplier: 2.5,
@@ -3746,6 +3800,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3746
3800
  "fill-only",
3747
3801
  "incomplete"
3748
3802
  ],
3803
+ preferredCompositions: [
3804
+ "golden-spiral",
3805
+ "flow-field",
3806
+ "radial"
3807
+ ],
3749
3808
  flowLineMultiplier: 0.5,
3750
3809
  heroShape: false,
3751
3810
  glowMultiplier: 0.3,
@@ -3767,6 +3826,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3767
3826
  "stroke-only",
3768
3827
  "dashed"
3769
3828
  ],
3829
+ preferredCompositions: [
3830
+ "grid-subdivision",
3831
+ "radial"
3832
+ ],
3770
3833
  flowLineMultiplier: 0,
3771
3834
  heroShape: false,
3772
3835
  glowMultiplier: 0,
@@ -3788,6 +3851,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3788
3851
  "fill-only",
3789
3852
  "double-stroke"
3790
3853
  ],
3854
+ preferredCompositions: [
3855
+ "grid-subdivision",
3856
+ "clustered"
3857
+ ],
3791
3858
  flowLineMultiplier: 0,
3792
3859
  heroShape: true,
3793
3860
  glowMultiplier: 0,
@@ -3809,6 +3876,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3809
3876
  "watercolor",
3810
3877
  "fill-only"
3811
3878
  ],
3879
+ preferredCompositions: [
3880
+ "radial",
3881
+ "golden-spiral",
3882
+ "flow-field"
3883
+ ],
3812
3884
  flowLineMultiplier: 1,
3813
3885
  heroShape: true,
3814
3886
  glowMultiplier: 1,
@@ -3830,6 +3902,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3830
3902
  "stroke-only",
3831
3903
  "fill-only"
3832
3904
  ],
3905
+ preferredCompositions: [
3906
+ "clustered",
3907
+ "grid-subdivision",
3908
+ "radial"
3909
+ ],
3833
3910
  flowLineMultiplier: 0,
3834
3911
  heroShape: false,
3835
3912
  glowMultiplier: 0.3,
@@ -3851,6 +3928,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3851
3928
  "fill-only",
3852
3929
  "incomplete"
3853
3930
  ],
3931
+ preferredCompositions: [
3932
+ "flow-field",
3933
+ "golden-spiral",
3934
+ "spiral"
3935
+ ],
3854
3936
  flowLineMultiplier: 3,
3855
3937
  heroShape: true,
3856
3938
  glowMultiplier: 0.2,
@@ -3872,6 +3954,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3872
3954
  "fill-only",
3873
3955
  "hatched"
3874
3956
  ],
3957
+ preferredCompositions: [
3958
+ "radial",
3959
+ "clustered",
3960
+ "flow-field"
3961
+ ],
3875
3962
  flowLineMultiplier: 0,
3876
3963
  heroShape: false,
3877
3964
  glowMultiplier: 0,
@@ -3894,6 +3981,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3894
3981
  "stroke-only",
3895
3982
  "incomplete"
3896
3983
  ],
3984
+ preferredCompositions: [
3985
+ "spiral",
3986
+ "radial",
3987
+ "golden-spiral"
3988
+ ],
3897
3989
  flowLineMultiplier: 2,
3898
3990
  heroShape: true,
3899
3991
  glowMultiplier: 2.5,
@@ -3917,6 +4009,12 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3917
4009
  ...b.preferredStyles
3918
4010
  ])
3919
4011
  ];
4012
+ const mergedCompositions = [
4013
+ ...new Set([
4014
+ ...a.preferredCompositions,
4015
+ ...b.preferredCompositions
4016
+ ])
4017
+ ];
3920
4018
  return {
3921
4019
  name: `${a.name}+${b.name}`,
3922
4020
  gridSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.gridSize, b.gridSize, t)),
@@ -3928,6 +4026,7 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3928
4026
  backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3929
4027
  paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3930
4028
  preferredStyles: mergedStyles,
4029
+ preferredCompositions: mergedCompositions,
3931
4030
  flowLineMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3932
4031
  heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3933
4032
  glowMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
@@ -3961,7 +4060,8 @@ const $1f63dc64b5593c73$var$SACRED_SHAPES = [
3961
4060
  "torus",
3962
4061
  "eggOfLife"
3963
4062
  ];
3964
- const $1f63dc64b5593c73$var$COMPOSITION_MODES = [
4063
+ // ── Composition modes ───────────────────────────────────────────────
4064
+ const $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES = [
3965
4065
  "radial",
3966
4066
  "flow-field",
3967
4067
  "spiral",
@@ -4063,7 +4163,67 @@ function $1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones) {
4063
4163
  }
4064
4164
  return false;
4065
4165
  }
4066
- // ── 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) ──────────────────────────
4067
4227
  function $1f63dc64b5593c73$var$localDensity(x, y, positions, radius) {
4068
4228
  let count = 0;
4069
4229
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -4362,42 +4522,43 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4362
4522
  const patternOpacity = 0.02 + rng() * 0.04;
4363
4523
  const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4364
4524
  if (bgPatternRoll < 0.2) {
4365
- // Dot grid
4525
+ // Dot grid — batched into a single path
4366
4526
  const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4367
4527
  const dotR = dotSpacing * 0.08;
4368
4528
  ctx.globalAlpha = patternOpacity;
4369
4529
  ctx.fillStyle = patternColor;
4530
+ ctx.beginPath();
4370
4531
  for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4371
- ctx.beginPath();
4532
+ ctx.moveTo(px + dotR, py);
4372
4533
  ctx.arc(px, py, dotR, 0, Math.PI * 2);
4373
- ctx.fill();
4374
4534
  }
4535
+ ctx.fill();
4375
4536
  } else if (bgPatternRoll < 0.4) {
4376
- // Diagonal lines
4537
+ // Diagonal lines — batched into a single path
4377
4538
  const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4378
4539
  ctx.globalAlpha = patternOpacity;
4379
4540
  ctx.strokeStyle = patternColor;
4380
4541
  ctx.lineWidth = 0.5 * scaleFactor;
4381
4542
  const diag = Math.hypot(width, height);
4543
+ ctx.beginPath();
4382
4544
  for(let d = -diag; d < diag; d += lineSpacing){
4383
- ctx.beginPath();
4384
4545
  ctx.moveTo(d, 0);
4385
4546
  ctx.lineTo(d + height, height);
4386
- ctx.stroke();
4387
4547
  }
4548
+ ctx.stroke();
4388
4549
  } else {
4389
- // Tessellation — hexagonal grid of tiny shapes
4550
+ // Tessellation — hexagonal grid, batched into a single path
4390
4551
  const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4391
4552
  const tessH = tessSize * Math.sqrt(3);
4392
4553
  ctx.globalAlpha = patternOpacity * 0.7;
4393
4554
  ctx.strokeStyle = patternColor;
4394
4555
  ctx.lineWidth = 0.4 * scaleFactor;
4556
+ ctx.beginPath();
4395
4557
  for(let row = 0; row * tessH < height + tessH; row++){
4396
4558
  const offsetX = row % 2 * tessSize * 0.75;
4397
4559
  for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4398
4560
  const hx = col * tessSize * 1.5 + offsetX;
4399
4561
  const hy = row * tessH;
4400
- ctx.beginPath();
4401
4562
  for(let s = 0; s < 6; s++){
4402
4563
  const angle = Math.PI / 3 * s - Math.PI / 6;
4403
4564
  const vx = hx + Math.cos(angle) * tessSize * 0.5;
@@ -4406,18 +4567,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4406
4567
  else ctx.lineTo(vx, vy);
4407
4568
  }
4408
4569
  ctx.closePath();
4409
- ctx.stroke();
4410
4570
  }
4411
4571
  }
4572
+ ctx.stroke();
4412
4573
  }
4413
4574
  ctx.restore();
4414
4575
  }
4415
4576
  ctx.globalCompositeOperation = "source-over";
4416
- // ── 2. Composition mode ────────────────────────────────────────
4417
- 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)];
4418
4579
  const symRoll = rng();
4419
4580
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
4420
- // ── 3. Focal points + void zones ───────────────────────────────
4581
+ // ── 3. Focal points + void zones (archetype-aware) ───────────────
4421
4582
  const THIRDS_POINTS = [
4422
4583
  {
4423
4584
  x: 1 / 3,
@@ -4450,9 +4611,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4450
4611
  y: height * (0.2 + rng() * 0.6),
4451
4612
  strength: 0.3 + rng() * 0.4
4452
4613
  });
4453
- 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;
4454
4620
  const voidZones = [];
4455
- 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({
4456
4631
  x: width * (0.15 + rng() * 0.7),
4457
4632
  y: height * (0.15 + rng() * 0.7),
4458
4633
  radius: Math.min(width, height) * (0.06 + rng() * 0.1)
@@ -4529,6 +4704,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4529
4704
  }
4530
4705
  // Track all placed shapes for density checks and connecting curves
4531
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);
4532
4710
  // Hero avoidance radius — shapes near the hero orient toward it
4533
4711
  let heroCenter = null;
4534
4712
  // ── 4b. Hero shape — a dominant focal element ───────────────────
@@ -4574,9 +4752,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4574
4752
  size: heroSize,
4575
4753
  shape: heroShape
4576
4754
  });
4755
+ spatialGrid.insert({
4756
+ x: heroFocal.x,
4757
+ y: heroFocal.y,
4758
+ size: heroSize,
4759
+ shape: heroShape
4760
+ });
4577
4761
  }
4578
4762
  // ── 5. Shape layers ────────────────────────────────────────────
4579
- const densityCheckRadius = Math.min(width, height) * 0.08;
4580
4763
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4581
4764
  for(let layer = 0; layer < layers; layer++){
4582
4765
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
@@ -4615,7 +4798,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4615
4798
  if ($1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones)) {
4616
4799
  if (rng() < 0.85) continue;
4617
4800
  }
4618
- if ($1f63dc64b5593c73$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
4801
+ if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
4619
4802
  if (rng() < 0.6) continue;
4620
4803
  }
4621
4804
  // Power distribution for size — archetype controls the curve
@@ -4675,17 +4858,11 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4675
4858
  let finalX = x;
4676
4859
  let finalY = y;
4677
4860
  if (shapePositions.length > 0 && rng() < 0.25) {
4678
- // Find nearest placed shape
4679
- let nearestDist = Infinity;
4680
- let nearestPos = null;
4681
- for (const sp of shapePositions){
4682
- const d = Math.hypot(x - sp.x, y - sp.y);
4683
- if (d < nearestDist && d > 0) {
4684
- nearestDist = d;
4685
- nearestPos = sp;
4686
- }
4687
- }
4861
+ // Use spatial grid for O(1) nearest-neighbor lookup
4862
+ const searchRadius = adjustedMaxSize * 3;
4863
+ const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
4688
4864
  if (nearestPos) {
4865
+ const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
4689
4866
  // Target distance: edges kissing (sum of half-sizes)
4690
4867
  const targetDist = (size + nearestPos.size) * 0.5;
4691
4868
  if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
@@ -4758,6 +4935,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4758
4935
  size: size,
4759
4936
  shape: shape
4760
4937
  });
4938
+ spatialGrid.insert({
4939
+ x: finalX,
4940
+ y: finalY,
4941
+ size: size,
4942
+ shape: shape
4943
+ });
4761
4944
  // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
4762
4945
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
4763
4946
  const echoCount = 2 + Math.floor(rng() * 2);
@@ -4786,6 +4969,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4786
4969
  size: echoSize,
4787
4970
  shape: shape
4788
4971
  });
4972
+ spatialGrid.insert({
4973
+ x: echoX,
4974
+ y: echoY,
4975
+ size: echoSize,
4976
+ shape: shape
4977
+ });
4789
4978
  }
4790
4979
  }
4791
4980
  // ── 5d. Recursive nesting ──────────────────────────────────
@@ -4850,13 +5039,62 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4850
5039
  size: member.size,
4851
5040
  shape: memberShape
4852
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
+ });
4853
5091
  }
4854
5092
  }
4855
5093
  }
4856
5094
  }
4857
5095
  // Reset blend mode for post-processing passes
4858
5096
  ctx.globalCompositeOperation = "source-over";
4859
- // ── 5f. Layered masking / cutout portals ───────────────────────
5097
+ // ── 5g. Layered masking / cutout portals ───────────────────────
4860
5098
  // ~18% of images get 1-3 portal windows that paint over foreground
4861
5099
  // with a tinted background wash, creating a "peek through" effect.
4862
5100
  if (rng() < 0.18 && shapePositions.length > 3) {
@@ -4937,6 +5175,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4937
5175
  fx += Math.cos(angle) * stepLen;
4938
5176
  fy += Math.sin(angle) * stepLen;
4939
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
+ }
4940
5184
  const t = s / steps;
4941
5185
  // Taper + pressure
4942
5186
  const taper = 1 - t * 0.8;
@@ -5034,30 +5278,60 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5034
5278
  }
5035
5279
  ctx.restore();
5036
5280
  }
5037
- // ── 7. Noise texture overlay ───────────────────────────────────
5281
+ // ── 7. Noise texture overlay — batched via ImageData ─────────────
5038
5282
  const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
5039
5283
  const noiseDensity = Math.floor(width * height / 800);
5040
- for(let i = 0; i < noiseDensity; i++){
5041
- const nx = noiseRng() * width;
5042
- const ny = noiseRng() * height;
5043
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5044
- const alpha = 0.01 + noiseRng() * 0.03;
5045
- ctx.globalAlpha = alpha;
5046
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5047
- 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
+ }
5048
5317
  }
5049
5318
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
5050
5319
  ctx.globalAlpha = 1;
5051
5320
  const vignetteStrength = 0.25 + rng() * 0.2;
5052
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
5053
5326
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
5054
5327
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
5055
- vigGrad.addColorStop(1, `rgba(0,0,0,${vignetteStrength.toFixed(3)})`);
5328
+ vigGrad.addColorStop(1, vignetteColor);
5056
5329
  ctx.fillStyle = vigGrad;
5057
5330
  ctx.fillRect(0, 0, width, height);
5058
- // ── 9. Organic connecting curves ───────────────────────────────
5331
+ // ── 9. Organic connecting curves — proximity-aware ───────────────
5059
5332
  if (shapePositions.length > 1) {
5060
5333
  const numCurves = Math.floor(8 * (width * height) / 1048576);
5334
+ const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
5061
5335
  ctx.lineWidth = 0.8 * scaleFactor;
5062
5336
  for(let i = 0; i < numCurves; i++){
5063
5337
  const idxA = Math.floor(rng() * shapePositions.length);
@@ -5065,11 +5339,13 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5065
5339
  const idxB = (idxA + offset) % shapePositions.length;
5066
5340
  const a = shapePositions[idxA];
5067
5341
  const b = shapePositions[idxB];
5068
- const mx = (a.x + b.x) / 2;
5069
- const my = (a.y + b.y) / 2;
5070
5342
  const dx = b.x - a.x;
5071
5343
  const dy = b.y - a.y;
5072
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;
5073
5349
  const bulge = (rng() - 0.5) * dist * 0.4;
5074
5350
  const cpx = mx + -dy / (dist || 1) * bulge;
5075
5351
  const cpy = my + dx / (dist || 1) * bulge;
@@ -5295,13 +5571,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5295
5571
  // Other archetypes: no border (intentional — not every image needs one)
5296
5572
  ctx.restore();
5297
5573
  }
5298
- // ── 11. Signature mark — unique geometric chop from hash prefix ──
5574
+ // ── 11. Signature mark — placed in the least-dense corner ──────
5299
5575
  {
5300
5576
  const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
5301
5577
  const sigSize = Math.min(width, height) * 0.025;
5302
- // Bottom-right corner with padding
5303
- const sigX = width - sigSize * 2.5;
5304
- 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;
5305
5609
  const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
5306
5610
  const sigColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
5307
5611
  ctx.save();