git-hash-art 0.9.0 → 0.10.1

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
@@ -62,6 +62,136 @@ const $461134e0b6ce0619$export$bb9e4790bc99ae59 = {
62
62
  PI: Math.PI,
63
63
  PHI: (1 + Math.sqrt(5)) / 2
64
64
  };
65
+ function $461134e0b6ce0619$export$bbde7fbaaf9a8d66(rng) {
66
+ // Build a deterministic permutation table (256 entries, doubled)
67
+ const perm = new Uint8Array(512);
68
+ const p = new Uint8Array(256);
69
+ for(let i = 0; i < 256; i++)p[i] = i;
70
+ // Fisher-Yates shuffle with our seeded RNG
71
+ for(let i = 255; i > 0; i--){
72
+ const j = Math.floor(rng() * (i + 1));
73
+ const tmp = p[i];
74
+ p[i] = p[j];
75
+ p[j] = tmp;
76
+ }
77
+ for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
78
+ // 12 gradient vectors for 2D simplex
79
+ const GRAD2 = [
80
+ [
81
+ 1,
82
+ 1
83
+ ],
84
+ [
85
+ -1,
86
+ 1
87
+ ],
88
+ [
89
+ 1,
90
+ -1
91
+ ],
92
+ [
93
+ -1,
94
+ -1
95
+ ],
96
+ [
97
+ 1,
98
+ 0
99
+ ],
100
+ [
101
+ -1,
102
+ 0
103
+ ],
104
+ [
105
+ 0,
106
+ 1
107
+ ],
108
+ [
109
+ 0,
110
+ -1
111
+ ],
112
+ [
113
+ 1,
114
+ 1
115
+ ],
116
+ [
117
+ -1,
118
+ 1
119
+ ],
120
+ [
121
+ 1,
122
+ -1
123
+ ],
124
+ [
125
+ -1,
126
+ -1
127
+ ]
128
+ ];
129
+ const F2 = 0.5 * (Math.sqrt(3) - 1);
130
+ const G2 = (3 - Math.sqrt(3)) / 6;
131
+ function dot2(g, x, y) {
132
+ return g[0] * x + g[1] * y;
133
+ }
134
+ return function noise2D(xin, yin) {
135
+ const s = (xin + yin) * F2;
136
+ const i = Math.floor(xin + s);
137
+ const j = Math.floor(yin + s);
138
+ const t = (i + j) * G2;
139
+ const X0 = i - t;
140
+ const Y0 = j - t;
141
+ const x0 = xin - X0;
142
+ const y0 = yin - Y0;
143
+ let i1, j1;
144
+ if (x0 > y0) {
145
+ i1 = 1;
146
+ j1 = 0;
147
+ } else {
148
+ i1 = 0;
149
+ j1 = 1;
150
+ }
151
+ const x1 = x0 - i1 + G2;
152
+ const y1 = y0 - j1 + G2;
153
+ const x2 = x0 - 1 + 2 * G2;
154
+ const y2 = y0 - 1 + 2 * G2;
155
+ const ii = i & 255;
156
+ const jj = j & 255;
157
+ let n0 = 0, n1 = 0, n2 = 0;
158
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
159
+ if (t0 >= 0) {
160
+ t0 *= t0;
161
+ const gi0 = perm[ii + perm[jj]] % 12;
162
+ n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
163
+ }
164
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
165
+ if (t1 >= 0) {
166
+ t1 *= t1;
167
+ const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
168
+ n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
169
+ }
170
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
171
+ if (t2 >= 0) {
172
+ t2 *= t2;
173
+ const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
174
+ n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
175
+ }
176
+ // Scale to approximately [-1, 1]
177
+ return 70 * (n0 + n1 + n2);
178
+ };
179
+ }
180
+ function $461134e0b6ce0619$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
181
+ return function fbm(x, y) {
182
+ let value = 0;
183
+ let amplitude = 1;
184
+ let frequency = 1;
185
+ let maxAmp = 0;
186
+ for(let i = 0; i < octaves; i++){
187
+ value += noise(x * frequency, y * frequency) * amplitude;
188
+ maxAmp += amplitude;
189
+ amplitude *= gain;
190
+ frequency *= lacunarity;
191
+ }
192
+ return value / maxAmp;
193
+ };
194
+ }
65
195
  class $461134e0b6ce0619$export$da2372f11bc66b3f {
66
196
  static getProportionalSize(baseSize, proportion) {
67
197
  return baseSize * proportion;
@@ -263,6 +393,48 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
263
393
  $9d614e7d77fc2947$var$hslToHex(baseHue, 0.7, 0.35)
264
394
  ];
265
395
  }
396
+ case "split-complementary":
397
+ {
398
+ // Base hue + two colors flanking the complement (±30°)
399
+ const comp = (baseHue + 180) % 360;
400
+ const split1 = (comp - 30 + 360) % 360;
401
+ const split2 = (comp + 30) % 360;
402
+ const sat = 0.55 + this.rng() * 0.25;
403
+ return [
404
+ $9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
405
+ $9d614e7d77fc2947$var$hslToHex(baseHue, sat * 0.8, 0.65),
406
+ $9d614e7d77fc2947$var$hslToHex(split1, sat, 0.5),
407
+ $9d614e7d77fc2947$var$hslToHex(split2, sat, 0.5),
408
+ $9d614e7d77fc2947$var$hslToHex(split1, sat * 0.7, 0.7)
409
+ ];
410
+ }
411
+ case "analogous-accent":
412
+ {
413
+ // Tight cluster of 3 analogous hues + 1 distant accent
414
+ const step = 15 + this.rng() * 20; // 15-35° apart
415
+ const h1 = (baseHue - step + 360) % 360;
416
+ const h2 = (baseHue + step) % 360;
417
+ const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
418
+ const sat = 0.5 + this.rng() * 0.3;
419
+ return [
420
+ $9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
421
+ $9d614e7d77fc2947$var$hslToHex(h1, sat, 0.55),
422
+ $9d614e7d77fc2947$var$hslToHex(h2, sat, 0.45),
423
+ $9d614e7d77fc2947$var$hslToHex(accentHue, sat + 0.15, 0.5)
424
+ ];
425
+ }
426
+ case "limited-palette":
427
+ {
428
+ // Only 3 colors — like a risograph print
429
+ const h2 = (baseHue + 120 + this.rng() * 40) % 360;
430
+ const h3 = (baseHue + 220 + this.rng() * 40) % 360;
431
+ const sat = 0.6 + this.rng() * 0.2;
432
+ return [
433
+ $9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
434
+ $9d614e7d77fc2947$var$hslToHex(h2, sat, 0.5),
435
+ $9d614e7d77fc2947$var$hslToHex(h3, sat * 0.9, 0.55)
436
+ ];
437
+ }
266
438
  case "harmonious":
267
439
  default:
268
440
  return this.getColors();
@@ -283,6 +455,14 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
283
455
  "#f5f5f0",
284
456
  "#e8e8e0"
285
457
  ];
458
+ case "split-complementary":
459
+ case "analogous-accent":
460
+ return this.getBackgroundColors();
461
+ case "limited-palette":
462
+ return [
463
+ $9d614e7d77fc2947$var$hslToHex(this.seed % 360, 0.08, 0.94),
464
+ $9d614e7d77fc2947$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
465
+ ];
286
466
  case "neon":
287
467
  return [
288
468
  "#0a0a12",
@@ -527,6 +707,19 @@ function $9d614e7d77fc2947$export$6d1620b367f86f7a(rng) {
527
707
  intensity: intensity
528
708
  };
529
709
  }
710
+ function $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(hex, degrees) {
711
+ const [h, s, l] = $9d614e7d77fc2947$var$hexToHsl(hex);
712
+ return $9d614e7d77fc2947$var$hslToHex((h + degrees + 360) % 360, s, l);
713
+ }
714
+ function $9d614e7d77fc2947$export$703ba40a4347f77a(base, layerRatio, hueShiftPerLayer) {
715
+ const shift = layerRatio * hueShiftPerLayer;
716
+ return {
717
+ dominant: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.dominant, shift),
718
+ secondary: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
719
+ accent: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
720
+ all: base.all.map((c)=>$9d614e7d77fc2947$export$1793a1bfbe4f6ff5(c, shift * 0.6))
721
+ };
722
+ }
530
723
 
531
724
 
532
725
 
@@ -1810,7 +2003,8 @@ const $9beb8f41637c29fd$var$RENDER_STYLES = [
1810
2003
  "noise-grain",
1811
2004
  "wood-grain",
1812
2005
  "marble-vein",
1813
- "fabric-weave"
2006
+ "fabric-weave",
2007
+ "hand-drawn"
1814
2008
  ];
1815
2009
  function $9beb8f41637c29fd$export$9fd4e64b2acd410e(rng) {
1816
2010
  return $9beb8f41637c29fd$var$RENDER_STYLES[Math.floor(rng() * $9beb8f41637c29fd$var$RENDER_STYLES.length)];
@@ -1906,6 +2100,23 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
1906
2100
  ctx.fill();
1907
2101
  ctx.fillStyle = origFill;
1908
2102
  ctx.restore();
2103
+ // Pass 4: Organic edge erosion — irregular bites along the boundary
2104
+ if (rng && size > 20) {
2105
+ const erosionBites = 6 + Math.floor(rng() * 8);
2106
+ const edgeRadius = size * 0.45;
2107
+ ctx.save();
2108
+ ctx.globalCompositeOperation = "destination-out";
2109
+ ctx.globalAlpha = 0.6 + rng() * 0.3;
2110
+ for(let eb = 0; eb < erosionBites; eb++){
2111
+ const biteAngle = rng() * Math.PI * 2;
2112
+ const biteDist = edgeRadius * (0.85 + rng() * 0.25);
2113
+ const biteR = size * (0.02 + rng() * 0.04);
2114
+ ctx.beginPath();
2115
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2116
+ ctx.fill();
2117
+ }
2118
+ ctx.restore();
2119
+ }
1909
2120
  ctx.globalAlpha = savedAlpha;
1910
2121
  // Soft stroke on top — thinner than normal for delicacy
1911
2122
  ctx.globalAlpha *= 0.25;
@@ -2191,6 +2402,50 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
2191
2402
  ctx.globalAlpha /= 0.3;
2192
2403
  break;
2193
2404
  }
2405
+ case "hand-drawn":
2406
+ {
2407
+ // Wobbly hand-drawn edge treatment — fill normally, then redraw
2408
+ // the outline with perturbed control points for a sketchy feel
2409
+ const savedAlphaHD = ctx.globalAlpha;
2410
+ ctx.globalAlpha = savedAlphaHD * 0.85;
2411
+ ctx.fill();
2412
+ ctx.globalAlpha = savedAlphaHD;
2413
+ // Draw 2-3 slightly offset wobbly strokes for a sketchy look
2414
+ const wobblePasses = 2 + (rng ? Math.floor(rng() * 2) : 0);
2415
+ ctx.lineWidth = strokeWidth * 0.8;
2416
+ for(let wp = 0; wp < wobblePasses; wp++){
2417
+ ctx.globalAlpha = savedAlphaHD * (0.4 - wp * 0.1);
2418
+ ctx.save();
2419
+ // Slight random offset per pass
2420
+ const wobbleX = rng ? (rng() - 0.5) * size * 0.02 : 0;
2421
+ const wobbleY = rng ? (rng() - 0.5) * size * 0.02 : 0;
2422
+ ctx.translate(wobbleX, wobbleY);
2423
+ // Slightly different scale per pass for edge variation
2424
+ const wobbleScale = 1 + (rng ? (rng() - 0.5) * 0.03 : 0);
2425
+ ctx.scale(wobbleScale, wobbleScale);
2426
+ ctx.stroke();
2427
+ ctx.restore();
2428
+ }
2429
+ // Organic edge erosion — small irregular bites for rough paper feel
2430
+ if (rng && size > 20) {
2431
+ const erosionBites = 4 + Math.floor(rng() * 6);
2432
+ const edgeRadius = size * 0.42;
2433
+ ctx.save();
2434
+ ctx.globalCompositeOperation = "destination-out";
2435
+ ctx.globalAlpha = 0.5 + rng() * 0.3;
2436
+ for(let eb = 0; eb < erosionBites; eb++){
2437
+ const biteAngle = rng() * Math.PI * 2;
2438
+ const biteDist = edgeRadius * (0.9 + rng() * 0.2);
2439
+ const biteR = size * (0.015 + rng() * 0.03);
2440
+ ctx.beginPath();
2441
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2442
+ ctx.fill();
2443
+ }
2444
+ ctx.restore();
2445
+ }
2446
+ ctx.globalAlpha = savedAlphaHD;
2447
+ break;
2448
+ }
2194
2449
  case "fill-and-stroke":
2195
2450
  default:
2196
2451
  ctx.fill();
@@ -2199,12 +2454,20 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
2199
2454
  }
2200
2455
  }
2201
2456
  function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2202
- const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, patterns: patterns = [], proportionType: proportionType = "GOLDEN_RATIO", baseOpacity: baseOpacity = 0.6, opacityReduction: opacityReduction = 0.1, glowRadius: glowRadius = 0, glowColor: glowColor, gradientFillEnd: gradientFillEnd, renderStyle: renderStyle = "fill-and-stroke", rng: rng } = config;
2457
+ const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, patterns: patterns = [], proportionType: proportionType = "GOLDEN_RATIO", baseOpacity: baseOpacity = 0.6, opacityReduction: opacityReduction = 0.1, glowRadius: glowRadius = 0, glowColor: glowColor, gradientFillEnd: gradientFillEnd, renderStyle: renderStyle = "fill-and-stroke", rng: rng, lightAngle: lightAngle, scaleFactor: scaleFactor = 1 } = config;
2203
2458
  ctx.save();
2204
2459
  ctx.translate(x, y);
2205
2460
  ctx.rotate(rotation * Math.PI / 180);
2206
- // Glow / shadow effect
2207
- if (glowRadius > 0) {
2461
+ // ── Drop shadow — soft colored shadow offset along light direction ──
2462
+ if (lightAngle !== undefined && size > 10) {
2463
+ const shadowDist = size * 0.035;
2464
+ const shadowBlurR = size * 0.06;
2465
+ ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
2466
+ ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
2467
+ ctx.shadowBlur = shadowBlurR;
2468
+ ctx.shadowColor = "rgba(0,0,0,0.12)";
2469
+ } else if (glowRadius > 0) {
2470
+ // Glow / shadow effect (legacy path)
2208
2471
  ctx.shadowBlur = glowRadius;
2209
2472
  ctx.shadowColor = glowColor || fillColor;
2210
2473
  ctx.shadowOffsetX = 0;
@@ -2226,8 +2489,29 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2226
2489
  });
2227
2490
  $9beb8f41637c29fd$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
2228
2491
  }
2229
- // Reset shadow so patterns aren't double-glowed
2230
- if (glowRadius > 0) ctx.shadowBlur = 0;
2492
+ // Reset shadow so patterns and highlight aren't double-shadowed
2493
+ ctx.shadowBlur = 0;
2494
+ ctx.shadowOffsetX = 0;
2495
+ ctx.shadowOffsetY = 0;
2496
+ ctx.shadowColor = "transparent";
2497
+ // ── Specular highlight — bright arc on the light-facing side ──
2498
+ if (lightAngle !== undefined && size > 15 && rng) {
2499
+ const hlRadius = size * 0.35;
2500
+ const hlDist = size * 0.15;
2501
+ const hlX = Math.cos(lightAngle) * hlDist;
2502
+ const hlY = Math.sin(lightAngle) * hlDist;
2503
+ 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)");
2507
+ const savedOp = ctx.globalCompositeOperation;
2508
+ ctx.globalCompositeOperation = "soft-light";
2509
+ ctx.fillStyle = hlGrad;
2510
+ ctx.beginPath();
2511
+ ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
2512
+ ctx.fill();
2513
+ ctx.globalCompositeOperation = savedOp;
2514
+ }
2231
2515
  // Layer additional patterns if specified
2232
2516
  if (patterns.length > 0) (0, $461134e0b6ce0619$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
2233
2517
  baseSize: size,
@@ -2326,7 +2610,8 @@ const $24064302523652b1$export$4343b39fe47bd82c = {
2326
2610
  bestStyles: [
2327
2611
  "fill-only",
2328
2612
  "watercolor",
2329
- "fill-and-stroke"
2613
+ "fill-and-stroke",
2614
+ "hand-drawn"
2330
2615
  ]
2331
2616
  },
2332
2617
  square: {
@@ -2363,7 +2648,8 @@ const $24064302523652b1$export$4343b39fe47bd82c = {
2363
2648
  bestStyles: [
2364
2649
  "fill-and-stroke",
2365
2650
  "fill-only",
2366
- "watercolor"
2651
+ "watercolor",
2652
+ "hand-drawn"
2367
2653
  ]
2368
2654
  },
2369
2655
  hexagon: {
@@ -2743,7 +3029,8 @@ const $24064302523652b1$export$4343b39fe47bd82c = {
2743
3029
  bestStyles: [
2744
3030
  "fill-only",
2745
3031
  "watercolor",
2746
- "fill-and-stroke"
3032
+ "fill-and-stroke",
3033
+ "hand-drawn"
2747
3034
  ]
2748
3035
  },
2749
3036
  ngon: {
@@ -3623,8 +3910,51 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
3623
3910
  invertForeground: false
3624
3911
  }
3625
3912
  ];
3913
+ /**
3914
+ * Linearly interpolate between two archetype numeric parameters.
3915
+ */ function $3faa2521b78398cf$var$lerpNum(a, b, t) {
3916
+ return a + (b - a) * t;
3917
+ }
3918
+ /**
3919
+ * Blend two archetypes by interpolating their numeric parameters
3920
+ * and merging their style arrays.
3921
+ */ function $3faa2521b78398cf$var$blendArchetypes(a, b, t) {
3922
+ // Merge preferred styles — unique union, primary archetype first
3923
+ const mergedStyles = [
3924
+ ...new Set([
3925
+ ...a.preferredStyles,
3926
+ ...b.preferredStyles
3927
+ ])
3928
+ ];
3929
+ return {
3930
+ name: `${a.name}+${b.name}`,
3931
+ gridSize: Math.round($3faa2521b78398cf$var$lerpNum(a.gridSize, b.gridSize, t)),
3932
+ layers: Math.round($3faa2521b78398cf$var$lerpNum(a.layers, b.layers, t)),
3933
+ baseOpacity: $3faa2521b78398cf$var$lerpNum(a.baseOpacity, b.baseOpacity, t),
3934
+ opacityReduction: $3faa2521b78398cf$var$lerpNum(a.opacityReduction, b.opacityReduction, t),
3935
+ minShapeSize: Math.round($3faa2521b78398cf$var$lerpNum(a.minShapeSize, b.minShapeSize, t)),
3936
+ maxShapeSize: Math.round($3faa2521b78398cf$var$lerpNum(a.maxShapeSize, b.maxShapeSize, t)),
3937
+ backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3938
+ paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3939
+ preferredStyles: mergedStyles,
3940
+ flowLineMultiplier: $3faa2521b78398cf$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3941
+ heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3942
+ glowMultiplier: $3faa2521b78398cf$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
3943
+ sizePower: $3faa2521b78398cf$var$lerpNum(a.sizePower, b.sizePower, t),
3944
+ invertForeground: t < 0.5 ? a.invertForeground : b.invertForeground
3945
+ };
3946
+ }
3626
3947
  function $3faa2521b78398cf$export$f1142fd7da4d6590(rng) {
3627
- return $3faa2521b78398cf$var$ARCHETYPES[Math.floor(rng() * $3faa2521b78398cf$var$ARCHETYPES.length)];
3948
+ const primary = $3faa2521b78398cf$var$ARCHETYPES[Math.floor(rng() * $3faa2521b78398cf$var$ARCHETYPES.length)];
3949
+ // ~15% chance of blending with a second archetype
3950
+ if (rng() < 0.15) {
3951
+ const secondary = $3faa2521b78398cf$var$ARCHETYPES[Math.floor(rng() * $3faa2521b78398cf$var$ARCHETYPES.length)];
3952
+ if (secondary.name !== primary.name) {
3953
+ const blendT = 0.25 + rng() * 0.25; // 25-50% blend toward secondary
3954
+ return $3faa2521b78398cf$var$blendArchetypes(primary, secondary, blendT);
3955
+ }
3956
+ }
3957
+ return primary;
3628
3958
  }
3629
3959
 
3630
3960
 
@@ -3645,7 +3975,8 @@ const $b623126c6e9cbb71$var$COMPOSITION_MODES = [
3645
3975
  "flow-field",
3646
3976
  "spiral",
3647
3977
  "grid-subdivision",
3648
- "clustered"
3978
+ "clustered",
3979
+ "golden-spiral"
3649
3980
  ];
3650
3981
  // ── Helper: get position based on composition mode ──────────────────
3651
3982
  function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
@@ -3703,6 +4034,21 @@ function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height,
3703
4034
  x: rng() * width,
3704
4035
  y: rng() * height
3705
4036
  };
4037
+ case "golden-spiral":
4038
+ {
4039
+ // Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
4040
+ const PHI = (1 + Math.sqrt(5)) / 2;
4041
+ const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
4042
+ const t = shapeIndex / totalShapes;
4043
+ const angle = shapeIndex * goldenAngle + rng() * 0.3;
4044
+ const maxR = Math.min(width, height) * 0.44;
4045
+ // Shapes spiral outward with sqrt distribution for even area coverage
4046
+ const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
4047
+ return {
4048
+ x: cx + Math.cos(angle) * r,
4049
+ y: cy + Math.sin(angle) * r
4050
+ };
4051
+ }
3706
4052
  }
3707
4053
  }
3708
4054
  // ── Helper: positional color from hierarchy ─────────────────────────
@@ -3960,6 +4306,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
3960
4306
  const colorGrade = (0, $9d614e7d77fc2947$export$6d1620b367f86f7a)(rng);
3961
4307
  // ── 0e. Light direction — consistent shadow angle ──────────────
3962
4308
  const lightAngle = rng() * Math.PI * 2;
4309
+ // ── 0f. Palette evolution — hue drift direction across layers ──
4310
+ const paletteHueShift = (rng() - 0.5) * 40; // -20° to +20° total drift
3963
4311
  const scaleFactor = Math.min(width, height) / 1024;
3964
4312
  const adjustedMinSize = minShapeSize * scaleFactor;
3965
4313
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -4134,11 +4482,59 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4134
4482
  ry + (nearest.y - ry) * pull
4135
4483
  ];
4136
4484
  }
4137
- // ── 4. Flow field seed values ──────────────────────────────────
4485
+ // ── 3b. Void zone decoration intentional negative space ────
4486
+ for (const zone of voidZones){
4487
+ // Subtle halo ring around void zones
4488
+ ctx.globalAlpha = 0.04 + rng() * 0.04;
4489
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
4490
+ ctx.lineWidth = 1.5 * scaleFactor;
4491
+ ctx.beginPath();
4492
+ ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
4493
+ ctx.stroke();
4494
+ // ~50% chance: scatter tiny dots inside the void
4495
+ if (rng() < 0.5) {
4496
+ const dotCount = 3 + Math.floor(rng() * 6);
4497
+ ctx.globalAlpha = 0.06 + rng() * 0.04;
4498
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
4499
+ for(let d = 0; d < dotCount; d++){
4500
+ const angle = rng() * Math.PI * 2;
4501
+ const dist = rng() * zone.radius * 0.7;
4502
+ const dotR = (1 + rng() * 3) * scaleFactor;
4503
+ ctx.beginPath();
4504
+ ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
4505
+ ctx.fill();
4506
+ }
4507
+ }
4508
+ // ~30% chance: thin concentric ring inside
4509
+ if (rng() < 0.3) {
4510
+ ctx.globalAlpha = 0.03 + rng() * 0.03;
4511
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4512
+ ctx.lineWidth = 0.5 * scaleFactor;
4513
+ const innerR = zone.radius * (0.4 + rng() * 0.3);
4514
+ ctx.beginPath();
4515
+ ctx.arc(zone.x, zone.y, innerR, 0, Math.PI * 2);
4516
+ ctx.stroke();
4517
+ }
4518
+ }
4519
+ ctx.globalAlpha = 1;
4520
+ // ── 4. Flow field — simplex noise for organic variation ─────────
4521
+ // Create a seeded simplex noise field (unique per hash)
4522
+ const noiseFieldRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 333));
4523
+ const simplexNoise = (0, $461134e0b6ce0619$export$bbde7fbaaf9a8d66)(noiseFieldRng);
4524
+ const fbmNoise = (0, $461134e0b6ce0619$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
4138
4525
  const fieldAngleBase = rng() * Math.PI * 2;
4139
- const fieldFreq = 0.5 + rng() * 2;
4526
+ const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
4140
4527
  function flowAngle(x, y) {
4141
- return fieldAngleBase + Math.sin(x / width * fieldFreq * Math.PI * 2) * Math.PI * 0.5 + Math.cos(y / height * fieldFreq * Math.PI * 2) * Math.PI * 0.5;
4528
+ // Sample FBM noise at the position, scaled by frequency
4529
+ const nx = x / width * fieldFreq;
4530
+ const ny = y / height * fieldFreq;
4531
+ return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
4532
+ }
4533
+ // Noise-based size modulation — shapes in "high noise" areas get scaled
4534
+ function noiseSizeModulation(x, y) {
4535
+ const n = simplexNoise(x / width * 3, y / height * 3);
4536
+ // Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
4537
+ return 0.7 + (n + 1) * 0.3;
4142
4538
  }
4143
4539
  // Track all placed shapes for density checks and connecting curves
4144
4540
  const shapePositions = [];
@@ -4172,7 +4568,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4172
4568
  glowColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(heroStroke, 0.4),
4173
4569
  gradientFillEnd: (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
4174
4570
  renderStyle: heroStyle,
4175
- rng: rng
4571
+ rng: rng,
4572
+ lightAngle: lightAngle,
4573
+ scaleFactor: scaleFactor
4176
4574
  });
4177
4575
  heroCenter = {
4178
4576
  x: heroFocal.x,
@@ -4206,6 +4604,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4206
4604
  const dofFactor = 1 - layerRatio * 0.5; // 1.0 for front layer, 0.5 for back
4207
4605
  const dofStrokeScale = 0.4 + dofFactor * 0.6; // strokes thin out with depth
4208
4606
  const dofContrastReduction = layerRatio * 0.2; // colors fade toward bg
4607
+ // Color palette evolution — hue-rotate the hierarchy per layer
4608
+ const layerHierarchy = (0, $9d614e7d77fc2947$export$703ba40a4347f77a)(colorHierarchy, layerRatio, paletteHueShift);
4609
+ // Focal depth: shapes near focal points get more detail
4610
+ const focalDetailBoost = (px, py)=>{
4611
+ let minFocalDist = Infinity;
4612
+ for (const fp of focalPoints){
4613
+ const d = Math.hypot(px - fp.x, py - fp.y);
4614
+ if (d < minFocalDist) minFocalDist = d;
4615
+ }
4616
+ const maxDist = Math.hypot(width, height) * 0.5;
4617
+ return Math.max(0, 1 - minFocalDist / maxDist); // 1.0 at focal, 0.0 at edges
4618
+ };
4209
4619
  for(let i = 0; i < numShapes; i++){
4210
4620
  // Position from composition mode, then focal bias
4211
4621
  const rawPos = $b623126c6e9cbb71$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
@@ -4219,7 +4629,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4219
4629
  }
4220
4630
  // Power distribution for size — archetype controls the curve
4221
4631
  const sizeT = Math.pow(rng(), archetype.sizePower);
4222
- const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
4632
+ const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
4223
4633
  // Size fraction for affinity-aware shape selection
4224
4634
  const sizeFraction = size / adjustedMaxSize;
4225
4635
  // Palette-driven shape selection (replaces naive pickShape)
@@ -4236,9 +4646,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4236
4646
  rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
4237
4647
  }
4238
4648
  }
4239
- // Positional color from hierarchy + jitter
4240
- let fillBase = $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
4241
- const strokeBase = (0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4649
+ // Positional color from hierarchy + jitter (using evolved layer palette)
4650
+ let fillBase = $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, layerHierarchy, rng);
4651
+ const strokeBase = (0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(layerHierarchy, rng);
4242
4652
  // Desaturate colors on later layers for depth
4243
4653
  if (atmosphericDesat > 0) fillBase = (0, $9d614e7d77fc2947$export$fb75607d98509d9)(fillBase, atmosphericDesat);
4244
4654
  // Temperature contrast: shift foreground shapes opposite to background
@@ -4322,7 +4732,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4322
4732
  glowColor: hasGlow ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
4323
4733
  gradientFillEnd: gradientEnd,
4324
4734
  renderStyle: finalRenderStyle,
4325
- rng: rng
4735
+ rng: rng,
4736
+ lightAngle: lightAngle,
4737
+ scaleFactor: scaleFactor
4326
4738
  };
4327
4739
  if (shouldMirror) (0, $9beb8f41637c29fd$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4328
4740
  ...shapeConfig,
@@ -4330,6 +4742,25 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4330
4742
  mirrorGap: size * (0.1 + rng() * 0.3)
4331
4743
  });
4332
4744
  else (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
4745
+ // ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
4746
+ if (rng() < 0.2 && size > adjustedMinSize * 2) {
4747
+ const glazePasses = 2 + Math.floor(rng() * 2);
4748
+ for(let g = 0; g < glazePasses; g++){
4749
+ const glazeScale = 1 - (g + 1) * 0.12; // progressively smaller
4750
+ const glazeAlpha = 0.08 + g * 0.04; // progressively more opaque toward center
4751
+ ctx.globalAlpha = glazeAlpha;
4752
+ (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
4753
+ fillColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
4754
+ strokeColor: "rgba(0,0,0,0)",
4755
+ strokeWidth: 0,
4756
+ size: size * glazeScale,
4757
+ rotation: rotation,
4758
+ proportionType: "GOLDEN_RATIO",
4759
+ renderStyle: "fill-only",
4760
+ rng: rng
4761
+ });
4762
+ }
4763
+ }
4333
4764
  shapePositions.push({
4334
4765
  x: finalX,
4335
4766
  y: finalY,
@@ -4367,7 +4798,10 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4367
4798
  }
4368
4799
  }
4369
4800
  // ── 5d. Recursive nesting ──────────────────────────────────
4370
- if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
4801
+ // Focal depth: shapes near focal points get more detail
4802
+ const focalProximity = focalDetailBoost(finalX, finalY);
4803
+ const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
4804
+ if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
4371
4805
  const innerCount = 1 + Math.floor(rng() * 3);
4372
4806
  for(let n = 0; n < innerCount; n++){
4373
4807
  // Pick inner shape from palette affinities
@@ -4392,7 +4826,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4392
4826
  }
4393
4827
  }
4394
4828
  // ── 5e. Shape constellations — pre-composed groups ─────────
4395
- if (size > adjustedMaxSize * 0.35 && rng() < 0.12) {
4829
+ const constellationChance = 0.12 + focalProximity * 0.1; // 12-22% near focal
4830
+ if (size > adjustedMaxSize * 0.35 && rng() < constellationChance) {
4396
4831
  const constellation = $b623126c6e9cbb71$var$CONSTELLATIONS[Math.floor(rng() * $b623126c6e9cbb71$var$CONSTELLATIONS.length)];
4397
4832
  const members = constellation.build(rng, size);
4398
4833
  const groupRotation = rng() * Math.PI * 2;
@@ -4430,6 +4865,64 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4430
4865
  }
4431
4866
  // Reset blend mode for post-processing passes
4432
4867
  ctx.globalCompositeOperation = "source-over";
4868
+ // ── 5f. Layered masking / cutout portals ───────────────────────
4869
+ // ~18% of images get 1-3 portal windows that paint over foreground
4870
+ // with a tinted background wash, creating a "peek through" effect.
4871
+ if (rng() < 0.18 && shapePositions.length > 3) {
4872
+ const portalCount = 1 + Math.floor(rng() * 2);
4873
+ for(let p = 0; p < portalCount; p++){
4874
+ // Pick a position biased toward placed shapes
4875
+ const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
4876
+ const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
4877
+ const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
4878
+ const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
4879
+ // Pick a portal shape from the palette
4880
+ const portalShape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
4881
+ const portalRotation = rng() * 360;
4882
+ const portalAlpha = 0.6 + rng() * 0.35;
4883
+ ctx.save();
4884
+ ctx.translate(portalX, portalY);
4885
+ ctx.rotate(portalRotation * Math.PI / 180);
4886
+ // Step 1: Clip to the portal shape and fill with background wash
4887
+ ctx.beginPath();
4888
+ (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
4889
+ ctx.clip();
4890
+ // Fill the clipped region with a radial gradient from background colors
4891
+ const portalColor = (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
4892
+ const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
4893
+ portalGrad.addColorStop(0, portalColor);
4894
+ portalGrad.addColorStop(1, bgEnd);
4895
+ ctx.globalAlpha = portalAlpha;
4896
+ ctx.fillStyle = portalGrad;
4897
+ ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
4898
+ // Optional: subtle inner texture — a few tiny dots inside the portal
4899
+ if (rng() < 0.5) {
4900
+ const dotCount = 3 + Math.floor(rng() * 5);
4901
+ ctx.globalAlpha = portalAlpha * 0.3;
4902
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
4903
+ for(let d = 0; d < dotCount; d++){
4904
+ const dx = (rng() - 0.5) * portalSize * 1.4;
4905
+ const dy = (rng() - 0.5) * portalSize * 1.4;
4906
+ const dr = (1 + rng() * 3) * scaleFactor;
4907
+ ctx.beginPath();
4908
+ ctx.arc(dx, dy, dr, 0, Math.PI * 2);
4909
+ ctx.fill();
4910
+ }
4911
+ }
4912
+ ctx.restore();
4913
+ // Step 2: Draw a border ring around the portal (outside the clip)
4914
+ ctx.save();
4915
+ ctx.translate(portalX, portalY);
4916
+ ctx.rotate(portalRotation * Math.PI / 180);
4917
+ ctx.globalAlpha = 0.15 + rng() * 0.2;
4918
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
4919
+ ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
4920
+ ctx.beginPath();
4921
+ (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
4922
+ ctx.stroke();
4923
+ ctx.restore();
4924
+ }
4925
+ }
4433
4926
  // ── 6. Flow-line pass — variable color, branching, pressure ────
4434
4927
  const baseFlowLines = 6 + Math.floor(rng() * 10);
4435
4928
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
@@ -4496,7 +4989,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4496
4989
  prevY = fy;
4497
4990
  }
4498
4991
  }
4499
- // ── 6b. Apply symmetry mirroring ─────────────────────────────────
4992
+ // ── 6b. Motion/energy lines short directional bursts ─────────
4993
+ const energyArchetypes = [
4994
+ "dense-chaotic",
4995
+ "cosmic",
4996
+ "neon-glow",
4997
+ "bold-graphic"
4998
+ ];
4999
+ const hasEnergyLines = energyArchetypes.some((a)=>archetype.name.includes(a)) || rng() < 0.25;
5000
+ if (hasEnergyLines && shapePositions.length > 0) {
5001
+ const energyCount = 5 + Math.floor(rng() * 10);
5002
+ ctx.lineCap = "round";
5003
+ for(let e = 0; e < energyCount; e++){
5004
+ // Pick a random shape to radiate from
5005
+ const source = shapePositions[Math.floor(rng() * shapePositions.length)];
5006
+ const burstCount = 2 + Math.floor(rng() * 4);
5007
+ const baseAngle = flowAngle(source.x, source.y);
5008
+ for(let b = 0; b < burstCount; b++){
5009
+ const angle = baseAngle + (rng() - 0.5) * 1.2;
5010
+ const lineLen = (source.size * 0.3 + rng() * source.size * 0.5) * scaleFactor * 0.3;
5011
+ const startDist = source.size * 0.5;
5012
+ const sx = source.x + Math.cos(angle) * startDist;
5013
+ const sy = source.y + Math.sin(angle) * startDist;
5014
+ const ex = sx + Math.cos(angle) * lineLen;
5015
+ const ey = sy + Math.sin(angle) * lineLen;
5016
+ ctx.globalAlpha = 0.04 + rng() * 0.06;
5017
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
5018
+ ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
5019
+ ctx.beginPath();
5020
+ ctx.moveTo(sx, sy);
5021
+ ctx.lineTo(ex, ey);
5022
+ ctx.stroke();
5023
+ }
5024
+ }
5025
+ }
5026
+ // ── 6c. Apply symmetry mirroring ─────────────────────────────────
4500
5027
  if (symmetryMode !== "none") {
4501
5028
  const canvas = ctx.canvas;
4502
5029
  ctx.save();
@@ -4607,6 +5134,214 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4607
5134
  ctx.restore();
4608
5135
  ctx.globalCompositeOperation = "source-over";
4609
5136
  }
5137
+ // 10d. Gradient map — map luminance through a two-color gradient
5138
+ // Uses dominant→accent as the dark→light ramp for a cohesive tonal look
5139
+ if (rng() < 0.35) {
5140
+ const gmDark = colorHierarchy.dominant;
5141
+ const gmLight = colorHierarchy.accent;
5142
+ ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
5143
+ ctx.globalCompositeOperation = "color";
5144
+ // Paint a linear gradient from dark color (top) to light color (bottom)
5145
+ const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
5146
+ gmGrad.addColorStop(0, gmDark);
5147
+ gmGrad.addColorStop(1, gmLight);
5148
+ ctx.fillStyle = gmGrad;
5149
+ ctx.fillRect(0, 0, width, height);
5150
+ ctx.globalCompositeOperation = "source-over";
5151
+ }
5152
+ // ── 10e. Generative borders — archetype-driven decorative frames ──
5153
+ {
5154
+ ctx.save();
5155
+ ctx.globalAlpha = 1;
5156
+ ctx.globalCompositeOperation = "source-over";
5157
+ const borderRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 314));
5158
+ const borderPad = Math.min(width, height) * 0.025;
5159
+ const borderColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5160
+ const borderColorSolid = colorHierarchy.accent;
5161
+ const archName = archetype.name;
5162
+ if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
5163
+ // Clean ruled lines with corner ornaments
5164
+ ctx.strokeStyle = borderColor;
5165
+ ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
5166
+ ctx.globalAlpha = 0.18 + borderRng() * 0.1;
5167
+ // Outer rule
5168
+ ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
5169
+ // Inner rule (thinner, offset)
5170
+ const innerPad = borderPad * 1.8;
5171
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5172
+ ctx.globalAlpha *= 0.7;
5173
+ ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
5174
+ // Corner ornaments — small squares at each corner
5175
+ const ornSize = borderPad * 0.6;
5176
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(borderColorSolid, 0.12);
5177
+ const corners = [
5178
+ [
5179
+ borderPad,
5180
+ borderPad
5181
+ ],
5182
+ [
5183
+ width - borderPad - ornSize,
5184
+ borderPad
5185
+ ],
5186
+ [
5187
+ borderPad,
5188
+ height - borderPad - ornSize
5189
+ ],
5190
+ [
5191
+ width - borderPad - ornSize,
5192
+ height - borderPad - ornSize
5193
+ ]
5194
+ ];
5195
+ for (const [cx2, cy2] of corners){
5196
+ ctx.fillRect(cx2, cy2, ornSize, ornSize);
5197
+ // Diagonal cross inside ornament
5198
+ ctx.beginPath();
5199
+ ctx.moveTo(cx2, cy2);
5200
+ ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
5201
+ ctx.moveTo(cx2 + ornSize, cy2);
5202
+ ctx.lineTo(cx2, cy2 + ornSize);
5203
+ ctx.stroke();
5204
+ }
5205
+ } else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
5206
+ // Vine tendrils — organic curving lines along edges
5207
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
5208
+ ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
5209
+ ctx.globalAlpha = 0.12 + borderRng() * 0.08;
5210
+ ctx.lineCap = "round";
5211
+ const tendrilCount = 8 + Math.floor(borderRng() * 8);
5212
+ for(let t = 0; t < tendrilCount; t++){
5213
+ // Start from a random edge point
5214
+ const edge = Math.floor(borderRng() * 4);
5215
+ let tx, ty;
5216
+ if (edge === 0) {
5217
+ tx = borderRng() * width;
5218
+ ty = borderPad;
5219
+ } else if (edge === 1) {
5220
+ tx = borderRng() * width;
5221
+ ty = height - borderPad;
5222
+ } else if (edge === 2) {
5223
+ tx = borderPad;
5224
+ ty = borderRng() * height;
5225
+ } else {
5226
+ tx = width - borderPad;
5227
+ ty = borderRng() * height;
5228
+ }
5229
+ ctx.beginPath();
5230
+ ctx.moveTo(tx, ty);
5231
+ const segs = 3 + Math.floor(borderRng() * 4);
5232
+ for(let s = 0; s < segs; s++){
5233
+ const inward = borderPad * (1 + borderRng() * 2);
5234
+ // Curl inward from edge
5235
+ const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
5236
+ const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
5237
+ const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
5238
+ const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
5239
+ tx = cpx3;
5240
+ ty = cpy3;
5241
+ ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
5242
+ }
5243
+ ctx.stroke();
5244
+ // Small leaf/dot at tendril end
5245
+ if (borderRng() < 0.6) {
5246
+ ctx.beginPath();
5247
+ ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
5248
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
5249
+ ctx.fill();
5250
+ }
5251
+ }
5252
+ } else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
5253
+ // Star-studded arcs along edges
5254
+ ctx.globalAlpha = 0.1 + borderRng() * 0.08;
5255
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5256
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
5257
+ ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
5258
+ // Subtle arc along top and bottom
5259
+ ctx.beginPath();
5260
+ ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
5261
+ ctx.stroke();
5262
+ ctx.beginPath();
5263
+ ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
5264
+ ctx.stroke();
5265
+ // Scatter small stars along the border region
5266
+ const starCount = 15 + Math.floor(borderRng() * 15);
5267
+ for(let s = 0; s < starCount; s++){
5268
+ const edge = Math.floor(borderRng() * 4);
5269
+ let sx, sy;
5270
+ if (edge === 0) {
5271
+ sx = borderRng() * width;
5272
+ sy = borderPad * (0.5 + borderRng());
5273
+ } else if (edge === 1) {
5274
+ sx = borderRng() * width;
5275
+ sy = height - borderPad * (0.5 + borderRng());
5276
+ } else if (edge === 2) {
5277
+ sx = borderPad * (0.5 + borderRng());
5278
+ sy = borderRng() * height;
5279
+ } else {
5280
+ sx = width - borderPad * (0.5 + borderRng());
5281
+ sy = borderRng() * height;
5282
+ }
5283
+ const starR = (1 + borderRng() * 2.5) * scaleFactor;
5284
+ // 4-point star
5285
+ ctx.beginPath();
5286
+ for(let p = 0; p < 8; p++){
5287
+ const a = p / 8 * Math.PI * 2;
5288
+ const r = p % 2 === 0 ? starR : starR * 0.4;
5289
+ const px2 = sx + Math.cos(a) * r;
5290
+ const py2 = sy + Math.sin(a) * r;
5291
+ if (p === 0) ctx.moveTo(px2, py2);
5292
+ else ctx.lineTo(px2, py2);
5293
+ }
5294
+ ctx.closePath();
5295
+ ctx.fill();
5296
+ }
5297
+ } else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
5298
+ // Thin single rule — understated elegance
5299
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
5300
+ ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
5301
+ ctx.globalAlpha = 0.1 + borderRng() * 0.06;
5302
+ ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
5303
+ }
5304
+ // Other archetypes: no border (intentional — not every image needs one)
5305
+ ctx.restore();
5306
+ }
5307
+ // ── 11. Signature mark — unique geometric chop from hash prefix ──
5308
+ {
5309
+ const sigRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
5310
+ 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;
5314
+ const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
5315
+ const sigColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
5316
+ ctx.save();
5317
+ ctx.globalAlpha = 0.12 + sigRng() * 0.08;
5318
+ ctx.translate(sigX, sigY);
5319
+ ctx.strokeStyle = sigColor;
5320
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.06);
5321
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5322
+ // Outer ring
5323
+ ctx.beginPath();
5324
+ ctx.arc(0, 0, sigSize, 0, Math.PI * 2);
5325
+ ctx.stroke();
5326
+ ctx.fill();
5327
+ // Inner geometric pattern — unique per hash
5328
+ ctx.beginPath();
5329
+ for(let s = 0; s < sigSegments; s++){
5330
+ const angle1 = sigRng() * Math.PI * 2;
5331
+ const angle2 = sigRng() * Math.PI * 2;
5332
+ const r1 = sigSize * (0.2 + sigRng() * 0.6);
5333
+ const r2 = sigSize * (0.2 + sigRng() * 0.6);
5334
+ ctx.moveTo(Math.cos(angle1) * r1, Math.sin(angle1) * r1);
5335
+ ctx.lineTo(Math.cos(angle2) * r2, Math.sin(angle2) * r2);
5336
+ }
5337
+ ctx.stroke();
5338
+ // Center dot
5339
+ ctx.beginPath();
5340
+ ctx.arc(0, 0, sigSize * 0.12, 0, Math.PI * 2);
5341
+ ctx.fillStyle = sigColor;
5342
+ ctx.fill();
5343
+ ctx.restore();
5344
+ }
4610
5345
  ctx.globalAlpha = 1;
4611
5346
  }
4612
5347