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/module.js CHANGED
@@ -589,15 +589,17 @@ function $9d614e7d77fc2947$export$fabac4600b87056(colors, rng) {
589
589
  accent: colors[colors.length - 1] || "#888888",
590
590
  all: colors
591
591
  };
592
- // Pick dominant as the color closest to the palette's average hue
592
+ // Pick dominant as the color with the highest chroma (saturation × distance from gray)
593
+ // This selects the most visually prominent color rather than the average
593
594
  const hsls = colors.map((c)=>$9d614e7d77fc2947$var$hexToHsl(c));
594
- const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
595
595
  let dominantIdx = 0;
596
- let minDist = 360;
596
+ let maxChroma = -1;
597
597
  for(let i = 0; i < hsls.length; i++){
598
- const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
599
- if (d < minDist) {
600
- minDist = d;
598
+ // Chroma approximation: saturation × how far lightness is from 50% (gray)
599
+ const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
600
+ const chroma = hsls[i][1] * lightnessVibrancy;
601
+ if (chroma > maxChroma) {
602
+ maxChroma = chroma;
601
603
  dominantIdx = i;
602
604
  }
603
605
  }
@@ -2494,16 +2496,26 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2494
2496
  ctx.shadowOffsetX = 0;
2495
2497
  ctx.shadowOffsetY = 0;
2496
2498
  ctx.shadowColor = "transparent";
2497
- // ── Specular highlight — bright arc on the light-facing side ──
2499
+ // ── Specular highlight — tinted arc on the light-facing side ──
2498
2500
  if (lightAngle !== undefined && size > 15 && rng) {
2499
2501
  const hlRadius = size * 0.35;
2500
2502
  const hlDist = size * 0.15;
2501
2503
  const hlX = Math.cos(lightAngle) * hlDist;
2502
2504
  const hlY = Math.sin(lightAngle) * hlDist;
2503
2505
  const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2504
- hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
2505
- hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
2506
- hlGrad.addColorStop(1, "rgba(255,255,255,0)");
2506
+ // Tint highlight warm/cool based on fill color for cohesion
2507
+ // Parse fill to detect warmth — fallback to white for non-parseable
2508
+ let hlBase = "255,255,255";
2509
+ if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
2510
+ const r = parseInt(fillColor.slice(1, 3), 16);
2511
+ const g = parseInt(fillColor.slice(3, 5), 16);
2512
+ const b = parseInt(fillColor.slice(5, 7), 16);
2513
+ // Blend toward white but keep a hint of the fill's warmth
2514
+ hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
2515
+ }
2516
+ hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
2517
+ hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
2518
+ hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
2507
2519
  const savedOp = ctx.globalCompositeOperation;
2508
2520
  ctx.globalCompositeOperation = "soft-light";
2509
2521
  ctx.fillStyle = hlGrad;
@@ -3565,6 +3577,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3565
3577
  "watercolor",
3566
3578
  "fill-only"
3567
3579
  ],
3580
+ preferredCompositions: [
3581
+ "clustered",
3582
+ "flow-field",
3583
+ "radial"
3584
+ ],
3568
3585
  flowLineMultiplier: 2.5,
3569
3586
  heroShape: false,
3570
3587
  glowMultiplier: 0.5,
@@ -3586,6 +3603,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3586
3603
  "stroke-only",
3587
3604
  "incomplete"
3588
3605
  ],
3606
+ preferredCompositions: [
3607
+ "golden-spiral",
3608
+ "grid-subdivision"
3609
+ ],
3589
3610
  flowLineMultiplier: 0.3,
3590
3611
  heroShape: true,
3591
3612
  glowMultiplier: 0,
@@ -3607,6 +3628,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3607
3628
  "fill-only",
3608
3629
  "incomplete"
3609
3630
  ],
3631
+ preferredCompositions: [
3632
+ "flow-field",
3633
+ "golden-spiral",
3634
+ "spiral"
3635
+ ],
3610
3636
  flowLineMultiplier: 4,
3611
3637
  heroShape: false,
3612
3638
  glowMultiplier: 0.3,
@@ -3629,6 +3655,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3629
3655
  "double-stroke",
3630
3656
  "hatched"
3631
3657
  ],
3658
+ preferredCompositions: [
3659
+ "grid-subdivision",
3660
+ "radial"
3661
+ ],
3632
3662
  flowLineMultiplier: 0,
3633
3663
  heroShape: false,
3634
3664
  glowMultiplier: 0,
@@ -3650,6 +3680,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3650
3680
  "incomplete",
3651
3681
  "fill-only"
3652
3682
  ],
3683
+ preferredCompositions: [
3684
+ "golden-spiral",
3685
+ "radial",
3686
+ "spiral"
3687
+ ],
3653
3688
  flowLineMultiplier: 1.5,
3654
3689
  heroShape: true,
3655
3690
  glowMultiplier: 2,
@@ -3670,6 +3705,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3670
3705
  "fill-and-stroke",
3671
3706
  "double-stroke"
3672
3707
  ],
3708
+ preferredCompositions: [
3709
+ "grid-subdivision",
3710
+ "golden-spiral"
3711
+ ],
3673
3712
  flowLineMultiplier: 0,
3674
3713
  heroShape: true,
3675
3714
  glowMultiplier: 0,
@@ -3691,6 +3730,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3691
3730
  "double-stroke",
3692
3731
  "dashed"
3693
3732
  ],
3733
+ preferredCompositions: [
3734
+ "radial",
3735
+ "spiral",
3736
+ "clustered"
3737
+ ],
3694
3738
  flowLineMultiplier: 2,
3695
3739
  heroShape: true,
3696
3740
  glowMultiplier: 3,
@@ -3713,6 +3757,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3713
3757
  "stroke-only",
3714
3758
  "dashed"
3715
3759
  ],
3760
+ preferredCompositions: [
3761
+ "flow-field",
3762
+ "grid-subdivision",
3763
+ "clustered"
3764
+ ],
3716
3765
  flowLineMultiplier: 1.5,
3717
3766
  heroShape: false,
3718
3767
  glowMultiplier: 0,
@@ -3734,6 +3783,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3734
3783
  "watercolor",
3735
3784
  "fill-and-stroke"
3736
3785
  ],
3786
+ preferredCompositions: [
3787
+ "radial",
3788
+ "spiral",
3789
+ "golden-spiral"
3790
+ ],
3737
3791
  flowLineMultiplier: 3,
3738
3792
  heroShape: true,
3739
3793
  glowMultiplier: 2.5,
@@ -3755,6 +3809,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3755
3809
  "fill-only",
3756
3810
  "incomplete"
3757
3811
  ],
3812
+ preferredCompositions: [
3813
+ "golden-spiral",
3814
+ "flow-field",
3815
+ "radial"
3816
+ ],
3758
3817
  flowLineMultiplier: 0.5,
3759
3818
  heroShape: false,
3760
3819
  glowMultiplier: 0.3,
@@ -3776,6 +3835,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3776
3835
  "stroke-only",
3777
3836
  "dashed"
3778
3837
  ],
3838
+ preferredCompositions: [
3839
+ "grid-subdivision",
3840
+ "radial"
3841
+ ],
3779
3842
  flowLineMultiplier: 0,
3780
3843
  heroShape: false,
3781
3844
  glowMultiplier: 0,
@@ -3797,6 +3860,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3797
3860
  "fill-only",
3798
3861
  "double-stroke"
3799
3862
  ],
3863
+ preferredCompositions: [
3864
+ "grid-subdivision",
3865
+ "clustered"
3866
+ ],
3800
3867
  flowLineMultiplier: 0,
3801
3868
  heroShape: true,
3802
3869
  glowMultiplier: 0,
@@ -3818,6 +3885,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3818
3885
  "watercolor",
3819
3886
  "fill-only"
3820
3887
  ],
3888
+ preferredCompositions: [
3889
+ "radial",
3890
+ "golden-spiral",
3891
+ "flow-field"
3892
+ ],
3821
3893
  flowLineMultiplier: 1,
3822
3894
  heroShape: true,
3823
3895
  glowMultiplier: 1,
@@ -3839,6 +3911,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3839
3911
  "stroke-only",
3840
3912
  "fill-only"
3841
3913
  ],
3914
+ preferredCompositions: [
3915
+ "clustered",
3916
+ "grid-subdivision",
3917
+ "radial"
3918
+ ],
3842
3919
  flowLineMultiplier: 0,
3843
3920
  heroShape: false,
3844
3921
  glowMultiplier: 0.3,
@@ -3860,6 +3937,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3860
3937
  "fill-only",
3861
3938
  "incomplete"
3862
3939
  ],
3940
+ preferredCompositions: [
3941
+ "flow-field",
3942
+ "golden-spiral",
3943
+ "spiral"
3944
+ ],
3863
3945
  flowLineMultiplier: 3,
3864
3946
  heroShape: true,
3865
3947
  glowMultiplier: 0.2,
@@ -3881,6 +3963,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3881
3963
  "fill-only",
3882
3964
  "hatched"
3883
3965
  ],
3966
+ preferredCompositions: [
3967
+ "radial",
3968
+ "clustered",
3969
+ "flow-field"
3970
+ ],
3884
3971
  flowLineMultiplier: 0,
3885
3972
  heroShape: false,
3886
3973
  glowMultiplier: 0,
@@ -3903,6 +3990,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3903
3990
  "stroke-only",
3904
3991
  "incomplete"
3905
3992
  ],
3993
+ preferredCompositions: [
3994
+ "spiral",
3995
+ "radial",
3996
+ "golden-spiral"
3997
+ ],
3906
3998
  flowLineMultiplier: 2,
3907
3999
  heroShape: true,
3908
4000
  glowMultiplier: 2.5,
@@ -3926,6 +4018,12 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3926
4018
  ...b.preferredStyles
3927
4019
  ])
3928
4020
  ];
4021
+ const mergedCompositions = [
4022
+ ...new Set([
4023
+ ...a.preferredCompositions,
4024
+ ...b.preferredCompositions
4025
+ ])
4026
+ ];
3929
4027
  return {
3930
4028
  name: `${a.name}+${b.name}`,
3931
4029
  gridSize: Math.round($3faa2521b78398cf$var$lerpNum(a.gridSize, b.gridSize, t)),
@@ -3937,6 +4035,7 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3937
4035
  backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3938
4036
  paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3939
4037
  preferredStyles: mergedStyles,
4038
+ preferredCompositions: mergedCompositions,
3940
4039
  flowLineMultiplier: $3faa2521b78398cf$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3941
4040
  heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3942
4041
  glowMultiplier: $3faa2521b78398cf$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
@@ -3970,7 +4069,8 @@ const $b623126c6e9cbb71$var$SACRED_SHAPES = [
3970
4069
  "torus",
3971
4070
  "eggOfLife"
3972
4071
  ];
3973
- const $b623126c6e9cbb71$var$COMPOSITION_MODES = [
4072
+ // ── Composition modes ───────────────────────────────────────────────
4073
+ const $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES = [
3974
4074
  "radial",
3975
4075
  "flow-field",
3976
4076
  "spiral",
@@ -4072,7 +4172,69 @@ function $b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones) {
4072
4172
  }
4073
4173
  return false;
4074
4174
  }
4075
- // ── Helper: density check ───────────────────────────────────────────
4175
+ // ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
4176
+ class $b623126c6e9cbb71$var$SpatialGrid {
4177
+ cells;
4178
+ cellSize;
4179
+ constructor(cellSize){
4180
+ this.cells = new Map();
4181
+ this.cellSize = cellSize;
4182
+ }
4183
+ key(cx, cy) {
4184
+ return `${cx},${cy}`;
4185
+ }
4186
+ insert(item) {
4187
+ const cx = Math.floor(item.x / this.cellSize);
4188
+ const cy = Math.floor(item.y / this.cellSize);
4189
+ const k = this.key(cx, cy);
4190
+ const cell = this.cells.get(k);
4191
+ if (cell) cell.push(item);
4192
+ else this.cells.set(k, [
4193
+ item
4194
+ ]);
4195
+ }
4196
+ /** Count items within radius of (x, y) */ countNear(x, y, radius) {
4197
+ const r2 = radius * radius;
4198
+ const minCx = Math.floor((x - radius) / this.cellSize);
4199
+ const maxCx = Math.floor((x + radius) / this.cellSize);
4200
+ const minCy = Math.floor((y - radius) / this.cellSize);
4201
+ const maxCy = Math.floor((y + radius) / this.cellSize);
4202
+ let count = 0;
4203
+ for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
4204
+ const cell = this.cells.get(this.key(cx, cy));
4205
+ if (!cell) continue;
4206
+ for (const p of cell){
4207
+ const dx = x - p.x;
4208
+ const dy = y - p.y;
4209
+ if (dx * dx + dy * dy < r2) count++;
4210
+ }
4211
+ }
4212
+ return count;
4213
+ }
4214
+ /** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
4215
+ const minCx = Math.floor((x - searchRadius) / this.cellSize);
4216
+ const maxCx = Math.floor((x + searchRadius) / this.cellSize);
4217
+ const minCy = Math.floor((y - searchRadius) / this.cellSize);
4218
+ const maxCy = Math.floor((y + searchRadius) / this.cellSize);
4219
+ let nearest = null;
4220
+ let bestDist2 = Infinity;
4221
+ for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
4222
+ const cell = this.cells.get(this.key(cx, cy));
4223
+ if (!cell) continue;
4224
+ for (const p of cell){
4225
+ const dx = x - p.x;
4226
+ const dy = y - p.y;
4227
+ const d2 = dx * dx + dy * dy;
4228
+ if (d2 > 0 && d2 < bestDist2) {
4229
+ bestDist2 = d2;
4230
+ nearest = p;
4231
+ }
4232
+ }
4233
+ }
4234
+ return nearest;
4235
+ }
4236
+ }
4237
+ // ── Helper: density check (legacy wrapper) ──────────────────────────
4076
4238
  function $b623126c6e9cbb71$var$localDensity(x, y, positions, radius) {
4077
4239
  let count = 0;
4078
4240
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -4371,42 +4533,43 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4371
4533
  const patternOpacity = 0.02 + rng() * 0.04;
4372
4534
  const patternColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4373
4535
  if (bgPatternRoll < 0.2) {
4374
- // Dot grid
4536
+ // Dot grid — batched into a single path
4375
4537
  const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4376
4538
  const dotR = dotSpacing * 0.08;
4377
4539
  ctx.globalAlpha = patternOpacity;
4378
4540
  ctx.fillStyle = patternColor;
4541
+ ctx.beginPath();
4379
4542
  for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4380
- ctx.beginPath();
4543
+ ctx.moveTo(px + dotR, py);
4381
4544
  ctx.arc(px, py, dotR, 0, Math.PI * 2);
4382
- ctx.fill();
4383
4545
  }
4546
+ ctx.fill();
4384
4547
  } else if (bgPatternRoll < 0.4) {
4385
- // Diagonal lines
4548
+ // Diagonal lines — batched into a single path
4386
4549
  const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4387
4550
  ctx.globalAlpha = patternOpacity;
4388
4551
  ctx.strokeStyle = patternColor;
4389
4552
  ctx.lineWidth = 0.5 * scaleFactor;
4390
4553
  const diag = Math.hypot(width, height);
4554
+ ctx.beginPath();
4391
4555
  for(let d = -diag; d < diag; d += lineSpacing){
4392
- ctx.beginPath();
4393
4556
  ctx.moveTo(d, 0);
4394
4557
  ctx.lineTo(d + height, height);
4395
- ctx.stroke();
4396
4558
  }
4559
+ ctx.stroke();
4397
4560
  } else {
4398
- // Tessellation — hexagonal grid of tiny shapes
4561
+ // Tessellation — hexagonal grid, batched into a single path
4399
4562
  const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4400
4563
  const tessH = tessSize * Math.sqrt(3);
4401
4564
  ctx.globalAlpha = patternOpacity * 0.7;
4402
4565
  ctx.strokeStyle = patternColor;
4403
4566
  ctx.lineWidth = 0.4 * scaleFactor;
4567
+ ctx.beginPath();
4404
4568
  for(let row = 0; row * tessH < height + tessH; row++){
4405
4569
  const offsetX = row % 2 * tessSize * 0.75;
4406
4570
  for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4407
4571
  const hx = col * tessSize * 1.5 + offsetX;
4408
4572
  const hy = row * tessH;
4409
- ctx.beginPath();
4410
4573
  for(let s = 0; s < 6; s++){
4411
4574
  const angle = Math.PI / 3 * s - Math.PI / 6;
4412
4575
  const vx = hx + Math.cos(angle) * tessSize * 0.5;
@@ -4415,18 +4578,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4415
4578
  else ctx.lineTo(vx, vy);
4416
4579
  }
4417
4580
  ctx.closePath();
4418
- ctx.stroke();
4419
4581
  }
4420
4582
  }
4583
+ ctx.stroke();
4421
4584
  }
4422
4585
  ctx.restore();
4423
4586
  }
4424
4587
  ctx.globalCompositeOperation = "source-over";
4425
- // ── 2. Composition mode ────────────────────────────────────────
4426
- const compositionMode = $b623126c6e9cbb71$var$COMPOSITION_MODES[Math.floor(rng() * $b623126c6e9cbb71$var$COMPOSITION_MODES.length)];
4588
+ // ── 2. Composition mode — archetype-aware selection ──────────────
4589
+ const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES.length)];
4427
4590
  const symRoll = rng();
4428
4591
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
4429
- // ── 3. Focal points + void zones ───────────────────────────────
4592
+ // ── 3. Focal points + void zones (archetype-aware) ───────────────
4430
4593
  const THIRDS_POINTS = [
4431
4594
  {
4432
4595
  x: 1 / 3,
@@ -4459,9 +4622,23 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4459
4622
  y: height * (0.2 + rng() * 0.6),
4460
4623
  strength: 0.3 + rng() * 0.4
4461
4624
  });
4462
- const numVoids = Math.floor(rng() * 2) + 1;
4625
+ // Archetype-aware void zones: dense archetypes get fewer/no voids,
4626
+ // minimal archetypes get golden-ratio positioned voids
4627
+ const PHI = (1 + Math.sqrt(5)) / 2;
4628
+ const isMinimalArchetype = archetype.gridSize <= 3;
4629
+ const isDenseArchetype = archetype.gridSize >= 8;
4630
+ const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
4463
4631
  const voidZones = [];
4464
- for(let v = 0; v < numVoids; v++)voidZones.push({
4632
+ for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
4633
+ // Place voids at golden-ratio positions for intentional negative space
4634
+ const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
4635
+ const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
4636
+ voidZones.push({
4637
+ x: width * (gx + (rng() - 0.5) * 0.05),
4638
+ y: height * (gy + (rng() - 0.5) * 0.05),
4639
+ radius: Math.min(width, height) * (0.08 + rng() * 0.08)
4640
+ });
4641
+ } else voidZones.push({
4465
4642
  x: width * (0.15 + rng() * 0.7),
4466
4643
  y: height * (0.15 + rng() * 0.7),
4467
4644
  radius: Math.min(width, height) * (0.06 + rng() * 0.1)
@@ -4538,6 +4715,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4538
4715
  }
4539
4716
  // Track all placed shapes for density checks and connecting curves
4540
4717
  const shapePositions = [];
4718
+ // Spatial grid for O(1) density and nearest-neighbor lookups
4719
+ const densityCheckRadius = Math.min(width, height) * 0.08;
4720
+ const spatialGrid = new $b623126c6e9cbb71$var$SpatialGrid(densityCheckRadius);
4541
4721
  // Hero avoidance radius — shapes near the hero orient toward it
4542
4722
  let heroCenter = null;
4543
4723
  // ── 4b. Hero shape — a dominant focal element ───────────────────
@@ -4583,9 +4763,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4583
4763
  size: heroSize,
4584
4764
  shape: heroShape
4585
4765
  });
4766
+ spatialGrid.insert({
4767
+ x: heroFocal.x,
4768
+ y: heroFocal.y,
4769
+ size: heroSize,
4770
+ shape: heroShape
4771
+ });
4586
4772
  }
4587
4773
  // ── 5. Shape layers ────────────────────────────────────────────
4588
- const densityCheckRadius = Math.min(width, height) * 0.08;
4589
4774
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4590
4775
  for(let layer = 0; layer < layers; layer++){
4591
4776
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
@@ -4624,7 +4809,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4624
4809
  if ($b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones)) {
4625
4810
  if (rng() < 0.85) continue;
4626
4811
  }
4627
- if ($b623126c6e9cbb71$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
4812
+ if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
4628
4813
  if (rng() < 0.6) continue;
4629
4814
  }
4630
4815
  // Power distribution for size — archetype controls the curve
@@ -4684,17 +4869,11 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4684
4869
  let finalX = x;
4685
4870
  let finalY = y;
4686
4871
  if (shapePositions.length > 0 && rng() < 0.25) {
4687
- // Find nearest placed shape
4688
- let nearestDist = Infinity;
4689
- let nearestPos = null;
4690
- for (const sp of shapePositions){
4691
- const d = Math.hypot(x - sp.x, y - sp.y);
4692
- if (d < nearestDist && d > 0) {
4693
- nearestDist = d;
4694
- nearestPos = sp;
4695
- }
4696
- }
4872
+ // Use spatial grid for O(1) nearest-neighbor lookup
4873
+ const searchRadius = adjustedMaxSize * 3;
4874
+ const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
4697
4875
  if (nearestPos) {
4876
+ const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
4698
4877
  // Target distance: edges kissing (sum of half-sizes)
4699
4878
  const targetDist = (size + nearestPos.size) * 0.5;
4700
4879
  if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
@@ -4767,6 +4946,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4767
4946
  size: size,
4768
4947
  shape: shape
4769
4948
  });
4949
+ spatialGrid.insert({
4950
+ x: finalX,
4951
+ y: finalY,
4952
+ size: size,
4953
+ shape: shape
4954
+ });
4770
4955
  // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
4771
4956
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
4772
4957
  const echoCount = 2 + Math.floor(rng() * 2);
@@ -4795,6 +4980,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4795
4980
  size: echoSize,
4796
4981
  shape: shape
4797
4982
  });
4983
+ spatialGrid.insert({
4984
+ x: echoX,
4985
+ y: echoY,
4986
+ size: echoSize,
4987
+ shape: shape
4988
+ });
4798
4989
  }
4799
4990
  }
4800
4991
  // ── 5d. Recursive nesting ──────────────────────────────────
@@ -4859,13 +5050,62 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4859
5050
  size: member.size,
4860
5051
  shape: memberShape
4861
5052
  });
5053
+ spatialGrid.insert({
5054
+ x: mx,
5055
+ y: my,
5056
+ size: member.size,
5057
+ shape: memberShape
5058
+ });
5059
+ }
5060
+ }
5061
+ // ── 5f. Rhythm placement — deliberate geometric progressions ──
5062
+ // ~12% of medium-large shapes spawn a rhythmic sequence
5063
+ if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
5064
+ const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
5065
+ const rhythmAngle = rng() * Math.PI * 2;
5066
+ const rhythmSpacing = size * (0.8 + rng() * 0.6);
5067
+ const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
5068
+ const rhythmShape = shape; // same shape for visual rhythm
5069
+ let rhythmSize = size * 0.6;
5070
+ for(let r = 0; r < rhythmCount; r++){
5071
+ const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
5072
+ const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
5073
+ if (rx < 0 || rx > width || ry < 0 || ry > height) break;
5074
+ if ($b623126c6e9cbb71$var$isInVoidZone(rx, ry, voidZones)) break;
5075
+ rhythmSize *= rhythmDecay;
5076
+ if (rhythmSize < adjustedMinSize) break;
5077
+ const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
5078
+ ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
5079
+ const rhythmFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
5080
+ (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
5081
+ fillColor: rhythmFill,
5082
+ strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.5),
5083
+ strokeWidth: strokeWidth * 0.7,
5084
+ size: rhythmSize,
5085
+ rotation: rotation + (r + 1) * 12,
5086
+ proportionType: "GOLDEN_RATIO",
5087
+ renderStyle: finalRenderStyle,
5088
+ rng: rng
5089
+ });
5090
+ shapePositions.push({
5091
+ x: rx,
5092
+ y: ry,
5093
+ size: rhythmSize,
5094
+ shape: rhythmShape
5095
+ });
5096
+ spatialGrid.insert({
5097
+ x: rx,
5098
+ y: ry,
5099
+ size: rhythmSize,
5100
+ shape: rhythmShape
5101
+ });
4862
5102
  }
4863
5103
  }
4864
5104
  }
4865
5105
  }
4866
5106
  // Reset blend mode for post-processing passes
4867
5107
  ctx.globalCompositeOperation = "source-over";
4868
- // ── 5f. Layered masking / cutout portals ───────────────────────
5108
+ // ── 5g. Layered masking / cutout portals ───────────────────────
4869
5109
  // ~18% of images get 1-3 portal windows that paint over foreground
4870
5110
  // with a tinted background wash, creating a "peek through" effect.
4871
5111
  if (rng() < 0.18 && shapePositions.length > 3) {
@@ -4946,6 +5186,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4946
5186
  fx += Math.cos(angle) * stepLen;
4947
5187
  fy += Math.sin(angle) * stepLen;
4948
5188
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
5189
+ // Skip segments that pass through void zones
5190
+ if ($b623126c6e9cbb71$var$isInVoidZone(fx, fy, voidZones)) {
5191
+ prevX = fx;
5192
+ prevY = fy;
5193
+ continue;
5194
+ }
4949
5195
  const t = s / steps;
4950
5196
  // Taper + pressure
4951
5197
  const taper = 1 - t * 0.8;
@@ -5043,30 +5289,60 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5043
5289
  }
5044
5290
  ctx.restore();
5045
5291
  }
5046
- // ── 7. Noise texture overlay ───────────────────────────────────
5292
+ // ── 7. Noise texture overlay — batched via ImageData ─────────────
5047
5293
  const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
5048
5294
  const noiseDensity = Math.floor(width * height / 800);
5049
- for(let i = 0; i < noiseDensity; i++){
5050
- const nx = noiseRng() * width;
5051
- const ny = noiseRng() * height;
5052
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5053
- const alpha = 0.01 + noiseRng() * 0.03;
5054
- ctx.globalAlpha = alpha;
5055
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5056
- ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5295
+ try {
5296
+ const imageData = ctx.getImageData(0, 0, width, height);
5297
+ const data = imageData.data;
5298
+ const pixelScale = Math.max(1, Math.round(scaleFactor));
5299
+ for(let i = 0; i < noiseDensity; i++){
5300
+ const nx = Math.floor(noiseRng() * width);
5301
+ const ny = Math.floor(noiseRng() * height);
5302
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5303
+ const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
5304
+ // Write a small block of pixels for scale
5305
+ for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
5306
+ const idx = ((ny + dy) * width + (nx + dx)) * 4;
5307
+ // Alpha-blend the noise dot onto existing pixel data
5308
+ const srcA = alpha / 255;
5309
+ const invA = 1 - srcA;
5310
+ data[idx] = Math.round(data[idx] * invA + brightness * srcA);
5311
+ data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
5312
+ data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
5313
+ // Keep existing alpha
5314
+ }
5315
+ }
5316
+ ctx.putImageData(imageData, 0, 0);
5317
+ } catch {
5318
+ // Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
5319
+ for(let i = 0; i < noiseDensity; i++){
5320
+ const nx = noiseRng() * width;
5321
+ const ny = noiseRng() * height;
5322
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5323
+ const alpha = 0.01 + noiseRng() * 0.03;
5324
+ ctx.globalAlpha = alpha;
5325
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5326
+ ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5327
+ }
5057
5328
  }
5058
5329
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
5059
5330
  ctx.globalAlpha = 1;
5060
5331
  const vignetteStrength = 0.25 + rng() * 0.2;
5061
5332
  const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
5333
+ // Tint vignette based on background: warm sepia for light, cool blue for dark
5334
+ const isLightBg = bgLum > 0.5;
5335
+ const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
5336
+ : `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
5062
5337
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
5063
5338
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
5064
- vigGrad.addColorStop(1, `rgba(0,0,0,${vignetteStrength.toFixed(3)})`);
5339
+ vigGrad.addColorStop(1, vignetteColor);
5065
5340
  ctx.fillStyle = vigGrad;
5066
5341
  ctx.fillRect(0, 0, width, height);
5067
- // ── 9. Organic connecting curves ───────────────────────────────
5342
+ // ── 9. Organic connecting curves — proximity-aware ───────────────
5068
5343
  if (shapePositions.length > 1) {
5069
5344
  const numCurves = Math.floor(8 * (width * height) / 1048576);
5345
+ const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
5070
5346
  ctx.lineWidth = 0.8 * scaleFactor;
5071
5347
  for(let i = 0; i < numCurves; i++){
5072
5348
  const idxA = Math.floor(rng() * shapePositions.length);
@@ -5074,11 +5350,13 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5074
5350
  const idxB = (idxA + offset) % shapePositions.length;
5075
5351
  const a = shapePositions[idxA];
5076
5352
  const b = shapePositions[idxB];
5077
- const mx = (a.x + b.x) / 2;
5078
- const my = (a.y + b.y) / 2;
5079
5353
  const dx = b.x - a.x;
5080
5354
  const dy = b.y - a.y;
5081
5355
  const dist = Math.hypot(dx, dy);
5356
+ // Skip connections between distant shapes
5357
+ if (dist > maxCurveDist) continue;
5358
+ const mx = (a.x + b.x) / 2;
5359
+ const my = (a.y + b.y) / 2;
5082
5360
  const bulge = (rng() - 0.5) * dist * 0.4;
5083
5361
  const cpx = mx + -dy / (dist || 1) * bulge;
5084
5362
  const cpy = my + dx / (dist || 1) * bulge;
@@ -5304,13 +5582,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5304
5582
  // Other archetypes: no border (intentional — not every image needs one)
5305
5583
  ctx.restore();
5306
5584
  }
5307
- // ── 11. Signature mark — unique geometric chop from hash prefix ──
5585
+ // ── 11. Signature mark — placed in the least-dense corner ──────
5308
5586
  {
5309
5587
  const sigRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
5310
5588
  const sigSize = Math.min(width, height) * 0.025;
5311
- // Bottom-right corner with padding
5312
- const sigX = width - sigSize * 2.5;
5313
- const sigY = height - sigSize * 2.5;
5589
+ const sigMargin = sigSize * 2.5;
5590
+ // Find the corner with the lowest local density
5591
+ const cornerCandidates = [
5592
+ {
5593
+ x: sigMargin,
5594
+ y: sigMargin
5595
+ },
5596
+ {
5597
+ x: width - sigMargin,
5598
+ y: sigMargin
5599
+ },
5600
+ {
5601
+ x: sigMargin,
5602
+ y: height - sigMargin
5603
+ },
5604
+ {
5605
+ x: width - sigMargin,
5606
+ y: height - sigMargin
5607
+ }
5608
+ ];
5609
+ let bestCorner = cornerCandidates[3]; // default: bottom-right
5610
+ let minDensity = Infinity;
5611
+ for (const corner of cornerCandidates){
5612
+ const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
5613
+ if (density < minDensity) {
5614
+ minDensity = density;
5615
+ bestCorner = corner;
5616
+ }
5617
+ }
5618
+ const sigX = bestCorner.x;
5619
+ const sigY = bestCorner.y;
5314
5620
  const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
5315
5621
  const sigColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
5316
5622
  ctx.save();