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/main.js CHANGED
@@ -76,6 +76,136 @@ const $e4b03e131ed2a289$export$bb9e4790bc99ae59 = {
76
76
  PI: Math.PI,
77
77
  PHI: (1 + Math.sqrt(5)) / 2
78
78
  };
79
+ function $e4b03e131ed2a289$export$bbde7fbaaf9a8d66(rng) {
80
+ // Build a deterministic permutation table (256 entries, doubled)
81
+ const perm = new Uint8Array(512);
82
+ const p = new Uint8Array(256);
83
+ for(let i = 0; i < 256; i++)p[i] = i;
84
+ // Fisher-Yates shuffle with our seeded RNG
85
+ for(let i = 255; i > 0; i--){
86
+ const j = Math.floor(rng() * (i + 1));
87
+ const tmp = p[i];
88
+ p[i] = p[j];
89
+ p[j] = tmp;
90
+ }
91
+ for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
92
+ // 12 gradient vectors for 2D simplex
93
+ const GRAD2 = [
94
+ [
95
+ 1,
96
+ 1
97
+ ],
98
+ [
99
+ -1,
100
+ 1
101
+ ],
102
+ [
103
+ 1,
104
+ -1
105
+ ],
106
+ [
107
+ -1,
108
+ -1
109
+ ],
110
+ [
111
+ 1,
112
+ 0
113
+ ],
114
+ [
115
+ -1,
116
+ 0
117
+ ],
118
+ [
119
+ 0,
120
+ 1
121
+ ],
122
+ [
123
+ 0,
124
+ -1
125
+ ],
126
+ [
127
+ 1,
128
+ 1
129
+ ],
130
+ [
131
+ -1,
132
+ 1
133
+ ],
134
+ [
135
+ 1,
136
+ -1
137
+ ],
138
+ [
139
+ -1,
140
+ -1
141
+ ]
142
+ ];
143
+ const F2 = 0.5 * (Math.sqrt(3) - 1);
144
+ const G2 = (3 - Math.sqrt(3)) / 6;
145
+ function dot2(g, x, y) {
146
+ return g[0] * x + g[1] * y;
147
+ }
148
+ return function noise2D(xin, yin) {
149
+ const s = (xin + yin) * F2;
150
+ const i = Math.floor(xin + s);
151
+ const j = Math.floor(yin + s);
152
+ const t = (i + j) * G2;
153
+ const X0 = i - t;
154
+ const Y0 = j - t;
155
+ const x0 = xin - X0;
156
+ const y0 = yin - Y0;
157
+ let i1, j1;
158
+ if (x0 > y0) {
159
+ i1 = 1;
160
+ j1 = 0;
161
+ } else {
162
+ i1 = 0;
163
+ j1 = 1;
164
+ }
165
+ const x1 = x0 - i1 + G2;
166
+ const y1 = y0 - j1 + G2;
167
+ const x2 = x0 - 1 + 2 * G2;
168
+ const y2 = y0 - 1 + 2 * G2;
169
+ const ii = i & 255;
170
+ const jj = j & 255;
171
+ let n0 = 0, n1 = 0, n2 = 0;
172
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
173
+ if (t0 >= 0) {
174
+ t0 *= t0;
175
+ const gi0 = perm[ii + perm[jj]] % 12;
176
+ n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
177
+ }
178
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
179
+ if (t1 >= 0) {
180
+ t1 *= t1;
181
+ const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
182
+ n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
183
+ }
184
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
185
+ if (t2 >= 0) {
186
+ t2 *= t2;
187
+ const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
188
+ n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
189
+ }
190
+ // Scale to approximately [-1, 1]
191
+ return 70 * (n0 + n1 + n2);
192
+ };
193
+ }
194
+ function $e4b03e131ed2a289$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
195
+ return function fbm(x, y) {
196
+ let value = 0;
197
+ let amplitude = 1;
198
+ let frequency = 1;
199
+ let maxAmp = 0;
200
+ for(let i = 0; i < octaves; i++){
201
+ value += noise(x * frequency, y * frequency) * amplitude;
202
+ maxAmp += amplitude;
203
+ amplitude *= gain;
204
+ frequency *= lacunarity;
205
+ }
206
+ return value / maxAmp;
207
+ };
208
+ }
79
209
  class $e4b03e131ed2a289$export$da2372f11bc66b3f {
80
210
  static getProportionalSize(baseSize, proportion) {
81
211
  return baseSize * proportion;
@@ -277,6 +407,48 @@ class $d016ad53434219a1$export$ab958c550f521376 {
277
407
  $d016ad53434219a1$var$hslToHex(baseHue, 0.7, 0.35)
278
408
  ];
279
409
  }
410
+ case "split-complementary":
411
+ {
412
+ // Base hue + two colors flanking the complement (±30°)
413
+ const comp = (baseHue + 180) % 360;
414
+ const split1 = (comp - 30 + 360) % 360;
415
+ const split2 = (comp + 30) % 360;
416
+ const sat = 0.55 + this.rng() * 0.25;
417
+ return [
418
+ $d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
419
+ $d016ad53434219a1$var$hslToHex(baseHue, sat * 0.8, 0.65),
420
+ $d016ad53434219a1$var$hslToHex(split1, sat, 0.5),
421
+ $d016ad53434219a1$var$hslToHex(split2, sat, 0.5),
422
+ $d016ad53434219a1$var$hslToHex(split1, sat * 0.7, 0.7)
423
+ ];
424
+ }
425
+ case "analogous-accent":
426
+ {
427
+ // Tight cluster of 3 analogous hues + 1 distant accent
428
+ const step = 15 + this.rng() * 20; // 15-35° apart
429
+ const h1 = (baseHue - step + 360) % 360;
430
+ const h2 = (baseHue + step) % 360;
431
+ const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
432
+ const sat = 0.5 + this.rng() * 0.3;
433
+ return [
434
+ $d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
435
+ $d016ad53434219a1$var$hslToHex(h1, sat, 0.55),
436
+ $d016ad53434219a1$var$hslToHex(h2, sat, 0.45),
437
+ $d016ad53434219a1$var$hslToHex(accentHue, sat + 0.15, 0.5)
438
+ ];
439
+ }
440
+ case "limited-palette":
441
+ {
442
+ // Only 3 colors — like a risograph print
443
+ const h2 = (baseHue + 120 + this.rng() * 40) % 360;
444
+ const h3 = (baseHue + 220 + this.rng() * 40) % 360;
445
+ const sat = 0.6 + this.rng() * 0.2;
446
+ return [
447
+ $d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
448
+ $d016ad53434219a1$var$hslToHex(h2, sat, 0.5),
449
+ $d016ad53434219a1$var$hslToHex(h3, sat * 0.9, 0.55)
450
+ ];
451
+ }
280
452
  case "harmonious":
281
453
  default:
282
454
  return this.getColors();
@@ -297,6 +469,14 @@ class $d016ad53434219a1$export$ab958c550f521376 {
297
469
  "#f5f5f0",
298
470
  "#e8e8e0"
299
471
  ];
472
+ case "split-complementary":
473
+ case "analogous-accent":
474
+ return this.getBackgroundColors();
475
+ case "limited-palette":
476
+ return [
477
+ $d016ad53434219a1$var$hslToHex(this.seed % 360, 0.08, 0.94),
478
+ $d016ad53434219a1$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
479
+ ];
300
480
  case "neon":
301
481
  return [
302
482
  "#0a0a12",
@@ -541,6 +721,19 @@ function $d016ad53434219a1$export$6d1620b367f86f7a(rng) {
541
721
  intensity: intensity
542
722
  };
543
723
  }
724
+ function $d016ad53434219a1$export$1793a1bfbe4f6ff5(hex, degrees) {
725
+ const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
726
+ return $d016ad53434219a1$var$hslToHex((h + degrees + 360) % 360, s, l);
727
+ }
728
+ function $d016ad53434219a1$export$703ba40a4347f77a(base, layerRatio, hueShiftPerLayer) {
729
+ const shift = layerRatio * hueShiftPerLayer;
730
+ return {
731
+ dominant: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.dominant, shift),
732
+ secondary: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
733
+ accent: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
734
+ all: base.all.map((c)=>$d016ad53434219a1$export$1793a1bfbe4f6ff5(c, shift * 0.6))
735
+ };
736
+ }
544
737
 
545
738
 
546
739
 
@@ -1824,7 +2017,8 @@ const $c3de8257a8baa3b0$var$RENDER_STYLES = [
1824
2017
  "noise-grain",
1825
2018
  "wood-grain",
1826
2019
  "marble-vein",
1827
- "fabric-weave"
2020
+ "fabric-weave",
2021
+ "hand-drawn"
1828
2022
  ];
1829
2023
  function $c3de8257a8baa3b0$export$9fd4e64b2acd410e(rng) {
1830
2024
  return $c3de8257a8baa3b0$var$RENDER_STYLES[Math.floor(rng() * $c3de8257a8baa3b0$var$RENDER_STYLES.length)];
@@ -1920,6 +2114,23 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
1920
2114
  ctx.fill();
1921
2115
  ctx.fillStyle = origFill;
1922
2116
  ctx.restore();
2117
+ // Pass 4: Organic edge erosion — irregular bites along the boundary
2118
+ if (rng && size > 20) {
2119
+ const erosionBites = 6 + Math.floor(rng() * 8);
2120
+ const edgeRadius = size * 0.45;
2121
+ ctx.save();
2122
+ ctx.globalCompositeOperation = "destination-out";
2123
+ ctx.globalAlpha = 0.6 + rng() * 0.3;
2124
+ for(let eb = 0; eb < erosionBites; eb++){
2125
+ const biteAngle = rng() * Math.PI * 2;
2126
+ const biteDist = edgeRadius * (0.85 + rng() * 0.25);
2127
+ const biteR = size * (0.02 + rng() * 0.04);
2128
+ ctx.beginPath();
2129
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2130
+ ctx.fill();
2131
+ }
2132
+ ctx.restore();
2133
+ }
1923
2134
  ctx.globalAlpha = savedAlpha;
1924
2135
  // Soft stroke on top — thinner than normal for delicacy
1925
2136
  ctx.globalAlpha *= 0.25;
@@ -2205,6 +2416,50 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2205
2416
  ctx.globalAlpha /= 0.3;
2206
2417
  break;
2207
2418
  }
2419
+ case "hand-drawn":
2420
+ {
2421
+ // Wobbly hand-drawn edge treatment — fill normally, then redraw
2422
+ // the outline with perturbed control points for a sketchy feel
2423
+ const savedAlphaHD = ctx.globalAlpha;
2424
+ ctx.globalAlpha = savedAlphaHD * 0.85;
2425
+ ctx.fill();
2426
+ ctx.globalAlpha = savedAlphaHD;
2427
+ // Draw 2-3 slightly offset wobbly strokes for a sketchy look
2428
+ const wobblePasses = 2 + (rng ? Math.floor(rng() * 2) : 0);
2429
+ ctx.lineWidth = strokeWidth * 0.8;
2430
+ for(let wp = 0; wp < wobblePasses; wp++){
2431
+ ctx.globalAlpha = savedAlphaHD * (0.4 - wp * 0.1);
2432
+ ctx.save();
2433
+ // Slight random offset per pass
2434
+ const wobbleX = rng ? (rng() - 0.5) * size * 0.02 : 0;
2435
+ const wobbleY = rng ? (rng() - 0.5) * size * 0.02 : 0;
2436
+ ctx.translate(wobbleX, wobbleY);
2437
+ // Slightly different scale per pass for edge variation
2438
+ const wobbleScale = 1 + (rng ? (rng() - 0.5) * 0.03 : 0);
2439
+ ctx.scale(wobbleScale, wobbleScale);
2440
+ ctx.stroke();
2441
+ ctx.restore();
2442
+ }
2443
+ // Organic edge erosion — small irregular bites for rough paper feel
2444
+ if (rng && size > 20) {
2445
+ const erosionBites = 4 + Math.floor(rng() * 6);
2446
+ const edgeRadius = size * 0.42;
2447
+ ctx.save();
2448
+ ctx.globalCompositeOperation = "destination-out";
2449
+ ctx.globalAlpha = 0.5 + rng() * 0.3;
2450
+ for(let eb = 0; eb < erosionBites; eb++){
2451
+ const biteAngle = rng() * Math.PI * 2;
2452
+ const biteDist = edgeRadius * (0.9 + rng() * 0.2);
2453
+ const biteR = size * (0.015 + rng() * 0.03);
2454
+ ctx.beginPath();
2455
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2456
+ ctx.fill();
2457
+ }
2458
+ ctx.restore();
2459
+ }
2460
+ ctx.globalAlpha = savedAlphaHD;
2461
+ break;
2462
+ }
2208
2463
  case "fill-and-stroke":
2209
2464
  default:
2210
2465
  ctx.fill();
@@ -2213,12 +2468,20 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2213
2468
  }
2214
2469
  }
2215
2470
  function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2216
- 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;
2471
+ 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;
2217
2472
  ctx.save();
2218
2473
  ctx.translate(x, y);
2219
2474
  ctx.rotate(rotation * Math.PI / 180);
2220
- // Glow / shadow effect
2221
- if (glowRadius > 0) {
2475
+ // ── Drop shadow — soft colored shadow offset along light direction ──
2476
+ if (lightAngle !== undefined && size > 10) {
2477
+ const shadowDist = size * 0.035;
2478
+ const shadowBlurR = size * 0.06;
2479
+ ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
2480
+ ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
2481
+ ctx.shadowBlur = shadowBlurR;
2482
+ ctx.shadowColor = "rgba(0,0,0,0.12)";
2483
+ } else if (glowRadius > 0) {
2484
+ // Glow / shadow effect (legacy path)
2222
2485
  ctx.shadowBlur = glowRadius;
2223
2486
  ctx.shadowColor = glowColor || fillColor;
2224
2487
  ctx.shadowOffsetX = 0;
@@ -2240,8 +2503,29 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2240
2503
  });
2241
2504
  $c3de8257a8baa3b0$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
2242
2505
  }
2243
- // Reset shadow so patterns aren't double-glowed
2244
- if (glowRadius > 0) ctx.shadowBlur = 0;
2506
+ // Reset shadow so patterns and highlight aren't double-shadowed
2507
+ ctx.shadowBlur = 0;
2508
+ ctx.shadowOffsetX = 0;
2509
+ ctx.shadowOffsetY = 0;
2510
+ ctx.shadowColor = "transparent";
2511
+ // ── Specular highlight — bright arc on the light-facing side ──
2512
+ if (lightAngle !== undefined && size > 15 && rng) {
2513
+ const hlRadius = size * 0.35;
2514
+ const hlDist = size * 0.15;
2515
+ const hlX = Math.cos(lightAngle) * hlDist;
2516
+ const hlY = Math.sin(lightAngle) * hlDist;
2517
+ const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2518
+ hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
2519
+ hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
2520
+ hlGrad.addColorStop(1, "rgba(255,255,255,0)");
2521
+ const savedOp = ctx.globalCompositeOperation;
2522
+ ctx.globalCompositeOperation = "soft-light";
2523
+ ctx.fillStyle = hlGrad;
2524
+ ctx.beginPath();
2525
+ ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
2526
+ ctx.fill();
2527
+ ctx.globalCompositeOperation = savedOp;
2528
+ }
2245
2529
  // Layer additional patterns if specified
2246
2530
  if (patterns.length > 0) (0, $e4b03e131ed2a289$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
2247
2531
  baseSize: size,
@@ -2340,7 +2624,8 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
2340
2624
  bestStyles: [
2341
2625
  "fill-only",
2342
2626
  "watercolor",
2343
- "fill-and-stroke"
2627
+ "fill-and-stroke",
2628
+ "hand-drawn"
2344
2629
  ]
2345
2630
  },
2346
2631
  square: {
@@ -2377,7 +2662,8 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
2377
2662
  bestStyles: [
2378
2663
  "fill-and-stroke",
2379
2664
  "fill-only",
2380
- "watercolor"
2665
+ "watercolor",
2666
+ "hand-drawn"
2381
2667
  ]
2382
2668
  },
2383
2669
  hexagon: {
@@ -2757,7 +3043,8 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
2757
3043
  bestStyles: [
2758
3044
  "fill-only",
2759
3045
  "watercolor",
2760
- "fill-and-stroke"
3046
+ "fill-and-stroke",
3047
+ "hand-drawn"
2761
3048
  ]
2762
3049
  },
2763
3050
  ngon: {
@@ -3637,8 +3924,51 @@ const $f89bc858f7202849$var$ARCHETYPES = [
3637
3924
  invertForeground: false
3638
3925
  }
3639
3926
  ];
3927
+ /**
3928
+ * Linearly interpolate between two archetype numeric parameters.
3929
+ */ function $f89bc858f7202849$var$lerpNum(a, b, t) {
3930
+ return a + (b - a) * t;
3931
+ }
3932
+ /**
3933
+ * Blend two archetypes by interpolating their numeric parameters
3934
+ * and merging their style arrays.
3935
+ */ function $f89bc858f7202849$var$blendArchetypes(a, b, t) {
3936
+ // Merge preferred styles — unique union, primary archetype first
3937
+ const mergedStyles = [
3938
+ ...new Set([
3939
+ ...a.preferredStyles,
3940
+ ...b.preferredStyles
3941
+ ])
3942
+ ];
3943
+ return {
3944
+ name: `${a.name}+${b.name}`,
3945
+ gridSize: Math.round($f89bc858f7202849$var$lerpNum(a.gridSize, b.gridSize, t)),
3946
+ layers: Math.round($f89bc858f7202849$var$lerpNum(a.layers, b.layers, t)),
3947
+ baseOpacity: $f89bc858f7202849$var$lerpNum(a.baseOpacity, b.baseOpacity, t),
3948
+ opacityReduction: $f89bc858f7202849$var$lerpNum(a.opacityReduction, b.opacityReduction, t),
3949
+ minShapeSize: Math.round($f89bc858f7202849$var$lerpNum(a.minShapeSize, b.minShapeSize, t)),
3950
+ maxShapeSize: Math.round($f89bc858f7202849$var$lerpNum(a.maxShapeSize, b.maxShapeSize, t)),
3951
+ backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3952
+ paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3953
+ preferredStyles: mergedStyles,
3954
+ flowLineMultiplier: $f89bc858f7202849$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3955
+ heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3956
+ glowMultiplier: $f89bc858f7202849$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
3957
+ sizePower: $f89bc858f7202849$var$lerpNum(a.sizePower, b.sizePower, t),
3958
+ invertForeground: t < 0.5 ? a.invertForeground : b.invertForeground
3959
+ };
3960
+ }
3640
3961
  function $f89bc858f7202849$export$f1142fd7da4d6590(rng) {
3641
- return $f89bc858f7202849$var$ARCHETYPES[Math.floor(rng() * $f89bc858f7202849$var$ARCHETYPES.length)];
3962
+ const primary = $f89bc858f7202849$var$ARCHETYPES[Math.floor(rng() * $f89bc858f7202849$var$ARCHETYPES.length)];
3963
+ // ~15% chance of blending with a second archetype
3964
+ if (rng() < 0.15) {
3965
+ const secondary = $f89bc858f7202849$var$ARCHETYPES[Math.floor(rng() * $f89bc858f7202849$var$ARCHETYPES.length)];
3966
+ if (secondary.name !== primary.name) {
3967
+ const blendT = 0.25 + rng() * 0.25; // 25-50% blend toward secondary
3968
+ return $f89bc858f7202849$var$blendArchetypes(primary, secondary, blendT);
3969
+ }
3970
+ }
3971
+ return primary;
3642
3972
  }
3643
3973
 
3644
3974
 
@@ -3659,7 +3989,8 @@ const $4f72c5a314eddf25$var$COMPOSITION_MODES = [
3659
3989
  "flow-field",
3660
3990
  "spiral",
3661
3991
  "grid-subdivision",
3662
- "clustered"
3992
+ "clustered",
3993
+ "golden-spiral"
3663
3994
  ];
3664
3995
  // ── Helper: get position based on composition mode ──────────────────
3665
3996
  function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
@@ -3717,6 +4048,21 @@ function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height,
3717
4048
  x: rng() * width,
3718
4049
  y: rng() * height
3719
4050
  };
4051
+ case "golden-spiral":
4052
+ {
4053
+ // Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
4054
+ const PHI = (1 + Math.sqrt(5)) / 2;
4055
+ const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
4056
+ const t = shapeIndex / totalShapes;
4057
+ const angle = shapeIndex * goldenAngle + rng() * 0.3;
4058
+ const maxR = Math.min(width, height) * 0.44;
4059
+ // Shapes spiral outward with sqrt distribution for even area coverage
4060
+ const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
4061
+ return {
4062
+ x: cx + Math.cos(angle) * r,
4063
+ y: cy + Math.sin(angle) * r
4064
+ };
4065
+ }
3720
4066
  }
3721
4067
  }
3722
4068
  // ── Helper: positional color from hierarchy ─────────────────────────
@@ -3974,6 +4320,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3974
4320
  const colorGrade = (0, $d016ad53434219a1$export$6d1620b367f86f7a)(rng);
3975
4321
  // ── 0e. Light direction — consistent shadow angle ──────────────
3976
4322
  const lightAngle = rng() * Math.PI * 2;
4323
+ // ── 0f. Palette evolution — hue drift direction across layers ──
4324
+ const paletteHueShift = (rng() - 0.5) * 40; // -20° to +20° total drift
3977
4325
  const scaleFactor = Math.min(width, height) / 1024;
3978
4326
  const adjustedMinSize = minShapeSize * scaleFactor;
3979
4327
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -4148,11 +4496,59 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4148
4496
  ry + (nearest.y - ry) * pull
4149
4497
  ];
4150
4498
  }
4151
- // ── 4. Flow field seed values ──────────────────────────────────
4499
+ // ── 3b. Void zone decoration intentional negative space ────
4500
+ for (const zone of voidZones){
4501
+ // Subtle halo ring around void zones
4502
+ ctx.globalAlpha = 0.04 + rng() * 0.04;
4503
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
4504
+ ctx.lineWidth = 1.5 * scaleFactor;
4505
+ ctx.beginPath();
4506
+ ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
4507
+ ctx.stroke();
4508
+ // ~50% chance: scatter tiny dots inside the void
4509
+ if (rng() < 0.5) {
4510
+ const dotCount = 3 + Math.floor(rng() * 6);
4511
+ ctx.globalAlpha = 0.06 + rng() * 0.04;
4512
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
4513
+ for(let d = 0; d < dotCount; d++){
4514
+ const angle = rng() * Math.PI * 2;
4515
+ const dist = rng() * zone.radius * 0.7;
4516
+ const dotR = (1 + rng() * 3) * scaleFactor;
4517
+ ctx.beginPath();
4518
+ ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
4519
+ ctx.fill();
4520
+ }
4521
+ }
4522
+ // ~30% chance: thin concentric ring inside
4523
+ if (rng() < 0.3) {
4524
+ ctx.globalAlpha = 0.03 + rng() * 0.03;
4525
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4526
+ ctx.lineWidth = 0.5 * scaleFactor;
4527
+ const innerR = zone.radius * (0.4 + rng() * 0.3);
4528
+ ctx.beginPath();
4529
+ ctx.arc(zone.x, zone.y, innerR, 0, Math.PI * 2);
4530
+ ctx.stroke();
4531
+ }
4532
+ }
4533
+ ctx.globalAlpha = 1;
4534
+ // ── 4. Flow field — simplex noise for organic variation ─────────
4535
+ // Create a seeded simplex noise field (unique per hash)
4536
+ const noiseFieldRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 333));
4537
+ const simplexNoise = (0, $e4b03e131ed2a289$export$bbde7fbaaf9a8d66)(noiseFieldRng);
4538
+ const fbmNoise = (0, $e4b03e131ed2a289$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
4152
4539
  const fieldAngleBase = rng() * Math.PI * 2;
4153
- const fieldFreq = 0.5 + rng() * 2;
4540
+ const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
4154
4541
  function flowAngle(x, y) {
4155
- 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;
4542
+ // Sample FBM noise at the position, scaled by frequency
4543
+ const nx = x / width * fieldFreq;
4544
+ const ny = y / height * fieldFreq;
4545
+ return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
4546
+ }
4547
+ // Noise-based size modulation — shapes in "high noise" areas get scaled
4548
+ function noiseSizeModulation(x, y) {
4549
+ const n = simplexNoise(x / width * 3, y / height * 3);
4550
+ // Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
4551
+ return 0.7 + (n + 1) * 0.3;
4156
4552
  }
4157
4553
  // Track all placed shapes for density checks and connecting curves
4158
4554
  const shapePositions = [];
@@ -4186,7 +4582,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4186
4582
  glowColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(heroStroke, 0.4),
4187
4583
  gradientFillEnd: (0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
4188
4584
  renderStyle: heroStyle,
4189
- rng: rng
4585
+ rng: rng,
4586
+ lightAngle: lightAngle,
4587
+ scaleFactor: scaleFactor
4190
4588
  });
4191
4589
  heroCenter = {
4192
4590
  x: heroFocal.x,
@@ -4220,6 +4618,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4220
4618
  const dofFactor = 1 - layerRatio * 0.5; // 1.0 for front layer, 0.5 for back
4221
4619
  const dofStrokeScale = 0.4 + dofFactor * 0.6; // strokes thin out with depth
4222
4620
  const dofContrastReduction = layerRatio * 0.2; // colors fade toward bg
4621
+ // Color palette evolution — hue-rotate the hierarchy per layer
4622
+ const layerHierarchy = (0, $d016ad53434219a1$export$703ba40a4347f77a)(colorHierarchy, layerRatio, paletteHueShift);
4623
+ // Focal depth: shapes near focal points get more detail
4624
+ const focalDetailBoost = (px, py)=>{
4625
+ let minFocalDist = Infinity;
4626
+ for (const fp of focalPoints){
4627
+ const d = Math.hypot(px - fp.x, py - fp.y);
4628
+ if (d < minFocalDist) minFocalDist = d;
4629
+ }
4630
+ const maxDist = Math.hypot(width, height) * 0.5;
4631
+ return Math.max(0, 1 - minFocalDist / maxDist); // 1.0 at focal, 0.0 at edges
4632
+ };
4223
4633
  for(let i = 0; i < numShapes; i++){
4224
4634
  // Position from composition mode, then focal bias
4225
4635
  const rawPos = $4f72c5a314eddf25$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
@@ -4233,7 +4643,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4233
4643
  }
4234
4644
  // Power distribution for size — archetype controls the curve
4235
4645
  const sizeT = Math.pow(rng(), archetype.sizePower);
4236
- const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
4646
+ const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
4237
4647
  // Size fraction for affinity-aware shape selection
4238
4648
  const sizeFraction = size / adjustedMaxSize;
4239
4649
  // Palette-driven shape selection (replaces naive pickShape)
@@ -4250,9 +4660,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4250
4660
  rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
4251
4661
  }
4252
4662
  }
4253
- // Positional color from hierarchy + jitter
4254
- let fillBase = $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
4255
- const strokeBase = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4663
+ // Positional color from hierarchy + jitter (using evolved layer palette)
4664
+ let fillBase = $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, layerHierarchy, rng);
4665
+ const strokeBase = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(layerHierarchy, rng);
4256
4666
  // Desaturate colors on later layers for depth
4257
4667
  if (atmosphericDesat > 0) fillBase = (0, $d016ad53434219a1$export$fb75607d98509d9)(fillBase, atmosphericDesat);
4258
4668
  // Temperature contrast: shift foreground shapes opposite to background
@@ -4336,7 +4746,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4336
4746
  glowColor: hasGlow ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
4337
4747
  gradientFillEnd: gradientEnd,
4338
4748
  renderStyle: finalRenderStyle,
4339
- rng: rng
4749
+ rng: rng,
4750
+ lightAngle: lightAngle,
4751
+ scaleFactor: scaleFactor
4340
4752
  };
4341
4753
  if (shouldMirror) (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4342
4754
  ...shapeConfig,
@@ -4344,6 +4756,25 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4344
4756
  mirrorGap: size * (0.1 + rng() * 0.3)
4345
4757
  });
4346
4758
  else (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
4759
+ // ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
4760
+ if (rng() < 0.2 && size > adjustedMinSize * 2) {
4761
+ const glazePasses = 2 + Math.floor(rng() * 2);
4762
+ for(let g = 0; g < glazePasses; g++){
4763
+ const glazeScale = 1 - (g + 1) * 0.12; // progressively smaller
4764
+ const glazeAlpha = 0.08 + g * 0.04; // progressively more opaque toward center
4765
+ ctx.globalAlpha = glazeAlpha;
4766
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
4767
+ fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
4768
+ strokeColor: "rgba(0,0,0,0)",
4769
+ strokeWidth: 0,
4770
+ size: size * glazeScale,
4771
+ rotation: rotation,
4772
+ proportionType: "GOLDEN_RATIO",
4773
+ renderStyle: "fill-only",
4774
+ rng: rng
4775
+ });
4776
+ }
4777
+ }
4347
4778
  shapePositions.push({
4348
4779
  x: finalX,
4349
4780
  y: finalY,
@@ -4381,7 +4812,10 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4381
4812
  }
4382
4813
  }
4383
4814
  // ── 5d. Recursive nesting ──────────────────────────────────
4384
- if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
4815
+ // Focal depth: shapes near focal points get more detail
4816
+ const focalProximity = focalDetailBoost(finalX, finalY);
4817
+ const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
4818
+ if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
4385
4819
  const innerCount = 1 + Math.floor(rng() * 3);
4386
4820
  for(let n = 0; n < innerCount; n++){
4387
4821
  // Pick inner shape from palette affinities
@@ -4406,7 +4840,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4406
4840
  }
4407
4841
  }
4408
4842
  // ── 5e. Shape constellations — pre-composed groups ─────────
4409
- if (size > adjustedMaxSize * 0.35 && rng() < 0.12) {
4843
+ const constellationChance = 0.12 + focalProximity * 0.1; // 12-22% near focal
4844
+ if (size > adjustedMaxSize * 0.35 && rng() < constellationChance) {
4410
4845
  const constellation = $4f72c5a314eddf25$var$CONSTELLATIONS[Math.floor(rng() * $4f72c5a314eddf25$var$CONSTELLATIONS.length)];
4411
4846
  const members = constellation.build(rng, size);
4412
4847
  const groupRotation = rng() * Math.PI * 2;
@@ -4444,6 +4879,64 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4444
4879
  }
4445
4880
  // Reset blend mode for post-processing passes
4446
4881
  ctx.globalCompositeOperation = "source-over";
4882
+ // ── 5f. Layered masking / cutout portals ───────────────────────
4883
+ // ~18% of images get 1-3 portal windows that paint over foreground
4884
+ // with a tinted background wash, creating a "peek through" effect.
4885
+ if (rng() < 0.18 && shapePositions.length > 3) {
4886
+ const portalCount = 1 + Math.floor(rng() * 2);
4887
+ for(let p = 0; p < portalCount; p++){
4888
+ // Pick a position biased toward placed shapes
4889
+ const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
4890
+ const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
4891
+ const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
4892
+ const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
4893
+ // Pick a portal shape from the palette
4894
+ const portalShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
4895
+ const portalRotation = rng() * 360;
4896
+ const portalAlpha = 0.6 + rng() * 0.35;
4897
+ ctx.save();
4898
+ ctx.translate(portalX, portalY);
4899
+ ctx.rotate(portalRotation * Math.PI / 180);
4900
+ // Step 1: Clip to the portal shape and fill with background wash
4901
+ ctx.beginPath();
4902
+ (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
4903
+ ctx.clip();
4904
+ // Fill the clipped region with a radial gradient from background colors
4905
+ const portalColor = (0, $d016ad53434219a1$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
4906
+ const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
4907
+ portalGrad.addColorStop(0, portalColor);
4908
+ portalGrad.addColorStop(1, bgEnd);
4909
+ ctx.globalAlpha = portalAlpha;
4910
+ ctx.fillStyle = portalGrad;
4911
+ ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
4912
+ // Optional: subtle inner texture — a few tiny dots inside the portal
4913
+ if (rng() < 0.5) {
4914
+ const dotCount = 3 + Math.floor(rng() * 5);
4915
+ ctx.globalAlpha = portalAlpha * 0.3;
4916
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
4917
+ for(let d = 0; d < dotCount; d++){
4918
+ const dx = (rng() - 0.5) * portalSize * 1.4;
4919
+ const dy = (rng() - 0.5) * portalSize * 1.4;
4920
+ const dr = (1 + rng() * 3) * scaleFactor;
4921
+ ctx.beginPath();
4922
+ ctx.arc(dx, dy, dr, 0, Math.PI * 2);
4923
+ ctx.fill();
4924
+ }
4925
+ }
4926
+ ctx.restore();
4927
+ // Step 2: Draw a border ring around the portal (outside the clip)
4928
+ ctx.save();
4929
+ ctx.translate(portalX, portalY);
4930
+ ctx.rotate(portalRotation * Math.PI / 180);
4931
+ ctx.globalAlpha = 0.15 + rng() * 0.2;
4932
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
4933
+ ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
4934
+ ctx.beginPath();
4935
+ (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
4936
+ ctx.stroke();
4937
+ ctx.restore();
4938
+ }
4939
+ }
4447
4940
  // ── 6. Flow-line pass — variable color, branching, pressure ────
4448
4941
  const baseFlowLines = 6 + Math.floor(rng() * 10);
4449
4942
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
@@ -4510,7 +5003,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4510
5003
  prevY = fy;
4511
5004
  }
4512
5005
  }
4513
- // ── 6b. Apply symmetry mirroring ─────────────────────────────────
5006
+ // ── 6b. Motion/energy lines short directional bursts ─────────
5007
+ const energyArchetypes = [
5008
+ "dense-chaotic",
5009
+ "cosmic",
5010
+ "neon-glow",
5011
+ "bold-graphic"
5012
+ ];
5013
+ const hasEnergyLines = energyArchetypes.some((a)=>archetype.name.includes(a)) || rng() < 0.25;
5014
+ if (hasEnergyLines && shapePositions.length > 0) {
5015
+ const energyCount = 5 + Math.floor(rng() * 10);
5016
+ ctx.lineCap = "round";
5017
+ for(let e = 0; e < energyCount; e++){
5018
+ // Pick a random shape to radiate from
5019
+ const source = shapePositions[Math.floor(rng() * shapePositions.length)];
5020
+ const burstCount = 2 + Math.floor(rng() * 4);
5021
+ const baseAngle = flowAngle(source.x, source.y);
5022
+ for(let b = 0; b < burstCount; b++){
5023
+ const angle = baseAngle + (rng() - 0.5) * 1.2;
5024
+ const lineLen = (source.size * 0.3 + rng() * source.size * 0.5) * scaleFactor * 0.3;
5025
+ const startDist = source.size * 0.5;
5026
+ const sx = source.x + Math.cos(angle) * startDist;
5027
+ const sy = source.y + Math.sin(angle) * startDist;
5028
+ const ex = sx + Math.cos(angle) * lineLen;
5029
+ const ey = sy + Math.sin(angle) * lineLen;
5030
+ ctx.globalAlpha = 0.04 + rng() * 0.06;
5031
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
5032
+ ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
5033
+ ctx.beginPath();
5034
+ ctx.moveTo(sx, sy);
5035
+ ctx.lineTo(ex, ey);
5036
+ ctx.stroke();
5037
+ }
5038
+ }
5039
+ }
5040
+ // ── 6c. Apply symmetry mirroring ─────────────────────────────────
4514
5041
  if (symmetryMode !== "none") {
4515
5042
  const canvas = ctx.canvas;
4516
5043
  ctx.save();
@@ -4621,6 +5148,214 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4621
5148
  ctx.restore();
4622
5149
  ctx.globalCompositeOperation = "source-over";
4623
5150
  }
5151
+ // 10d. Gradient map — map luminance through a two-color gradient
5152
+ // Uses dominant→accent as the dark→light ramp for a cohesive tonal look
5153
+ if (rng() < 0.35) {
5154
+ const gmDark = colorHierarchy.dominant;
5155
+ const gmLight = colorHierarchy.accent;
5156
+ ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
5157
+ ctx.globalCompositeOperation = "color";
5158
+ // Paint a linear gradient from dark color (top) to light color (bottom)
5159
+ const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
5160
+ gmGrad.addColorStop(0, gmDark);
5161
+ gmGrad.addColorStop(1, gmLight);
5162
+ ctx.fillStyle = gmGrad;
5163
+ ctx.fillRect(0, 0, width, height);
5164
+ ctx.globalCompositeOperation = "source-over";
5165
+ }
5166
+ // ── 10e. Generative borders — archetype-driven decorative frames ──
5167
+ {
5168
+ ctx.save();
5169
+ ctx.globalAlpha = 1;
5170
+ ctx.globalCompositeOperation = "source-over";
5171
+ const borderRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 314));
5172
+ const borderPad = Math.min(width, height) * 0.025;
5173
+ const borderColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5174
+ const borderColorSolid = colorHierarchy.accent;
5175
+ const archName = archetype.name;
5176
+ if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
5177
+ // Clean ruled lines with corner ornaments
5178
+ ctx.strokeStyle = borderColor;
5179
+ ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
5180
+ ctx.globalAlpha = 0.18 + borderRng() * 0.1;
5181
+ // Outer rule
5182
+ ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
5183
+ // Inner rule (thinner, offset)
5184
+ const innerPad = borderPad * 1.8;
5185
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5186
+ ctx.globalAlpha *= 0.7;
5187
+ ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
5188
+ // Corner ornaments — small squares at each corner
5189
+ const ornSize = borderPad * 0.6;
5190
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(borderColorSolid, 0.12);
5191
+ const corners = [
5192
+ [
5193
+ borderPad,
5194
+ borderPad
5195
+ ],
5196
+ [
5197
+ width - borderPad - ornSize,
5198
+ borderPad
5199
+ ],
5200
+ [
5201
+ borderPad,
5202
+ height - borderPad - ornSize
5203
+ ],
5204
+ [
5205
+ width - borderPad - ornSize,
5206
+ height - borderPad - ornSize
5207
+ ]
5208
+ ];
5209
+ for (const [cx2, cy2] of corners){
5210
+ ctx.fillRect(cx2, cy2, ornSize, ornSize);
5211
+ // Diagonal cross inside ornament
5212
+ ctx.beginPath();
5213
+ ctx.moveTo(cx2, cy2);
5214
+ ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
5215
+ ctx.moveTo(cx2 + ornSize, cy2);
5216
+ ctx.lineTo(cx2, cy2 + ornSize);
5217
+ ctx.stroke();
5218
+ }
5219
+ } else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
5220
+ // Vine tendrils — organic curving lines along edges
5221
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
5222
+ ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
5223
+ ctx.globalAlpha = 0.12 + borderRng() * 0.08;
5224
+ ctx.lineCap = "round";
5225
+ const tendrilCount = 8 + Math.floor(borderRng() * 8);
5226
+ for(let t = 0; t < tendrilCount; t++){
5227
+ // Start from a random edge point
5228
+ const edge = Math.floor(borderRng() * 4);
5229
+ let tx, ty;
5230
+ if (edge === 0) {
5231
+ tx = borderRng() * width;
5232
+ ty = borderPad;
5233
+ } else if (edge === 1) {
5234
+ tx = borderRng() * width;
5235
+ ty = height - borderPad;
5236
+ } else if (edge === 2) {
5237
+ tx = borderPad;
5238
+ ty = borderRng() * height;
5239
+ } else {
5240
+ tx = width - borderPad;
5241
+ ty = borderRng() * height;
5242
+ }
5243
+ ctx.beginPath();
5244
+ ctx.moveTo(tx, ty);
5245
+ const segs = 3 + Math.floor(borderRng() * 4);
5246
+ for(let s = 0; s < segs; s++){
5247
+ const inward = borderPad * (1 + borderRng() * 2);
5248
+ // Curl inward from edge
5249
+ const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
5250
+ const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
5251
+ const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
5252
+ const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
5253
+ tx = cpx3;
5254
+ ty = cpy3;
5255
+ ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
5256
+ }
5257
+ ctx.stroke();
5258
+ // Small leaf/dot at tendril end
5259
+ if (borderRng() < 0.6) {
5260
+ ctx.beginPath();
5261
+ ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
5262
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
5263
+ ctx.fill();
5264
+ }
5265
+ }
5266
+ } else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
5267
+ // Star-studded arcs along edges
5268
+ ctx.globalAlpha = 0.1 + borderRng() * 0.08;
5269
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5270
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
5271
+ ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
5272
+ // Subtle arc along top and bottom
5273
+ ctx.beginPath();
5274
+ ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
5275
+ ctx.stroke();
5276
+ ctx.beginPath();
5277
+ ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
5278
+ ctx.stroke();
5279
+ // Scatter small stars along the border region
5280
+ const starCount = 15 + Math.floor(borderRng() * 15);
5281
+ for(let s = 0; s < starCount; s++){
5282
+ const edge = Math.floor(borderRng() * 4);
5283
+ let sx, sy;
5284
+ if (edge === 0) {
5285
+ sx = borderRng() * width;
5286
+ sy = borderPad * (0.5 + borderRng());
5287
+ } else if (edge === 1) {
5288
+ sx = borderRng() * width;
5289
+ sy = height - borderPad * (0.5 + borderRng());
5290
+ } else if (edge === 2) {
5291
+ sx = borderPad * (0.5 + borderRng());
5292
+ sy = borderRng() * height;
5293
+ } else {
5294
+ sx = width - borderPad * (0.5 + borderRng());
5295
+ sy = borderRng() * height;
5296
+ }
5297
+ const starR = (1 + borderRng() * 2.5) * scaleFactor;
5298
+ // 4-point star
5299
+ ctx.beginPath();
5300
+ for(let p = 0; p < 8; p++){
5301
+ const a = p / 8 * Math.PI * 2;
5302
+ const r = p % 2 === 0 ? starR : starR * 0.4;
5303
+ const px2 = sx + Math.cos(a) * r;
5304
+ const py2 = sy + Math.sin(a) * r;
5305
+ if (p === 0) ctx.moveTo(px2, py2);
5306
+ else ctx.lineTo(px2, py2);
5307
+ }
5308
+ ctx.closePath();
5309
+ ctx.fill();
5310
+ }
5311
+ } else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
5312
+ // Thin single rule — understated elegance
5313
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
5314
+ ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
5315
+ ctx.globalAlpha = 0.1 + borderRng() * 0.06;
5316
+ ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
5317
+ }
5318
+ // Other archetypes: no border (intentional — not every image needs one)
5319
+ ctx.restore();
5320
+ }
5321
+ // ── 11. Signature mark — unique geometric chop from hash prefix ──
5322
+ {
5323
+ const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));
5324
+ const sigSize = Math.min(width, height) * 0.025;
5325
+ // Bottom-right corner with padding
5326
+ const sigX = width - sigSize * 2.5;
5327
+ const sigY = height - sigSize * 2.5;
5328
+ const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
5329
+ const sigColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
5330
+ ctx.save();
5331
+ ctx.globalAlpha = 0.12 + sigRng() * 0.08;
5332
+ ctx.translate(sigX, sigY);
5333
+ ctx.strokeStyle = sigColor;
5334
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.06);
5335
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5336
+ // Outer ring
5337
+ ctx.beginPath();
5338
+ ctx.arc(0, 0, sigSize, 0, Math.PI * 2);
5339
+ ctx.stroke();
5340
+ ctx.fill();
5341
+ // Inner geometric pattern — unique per hash
5342
+ ctx.beginPath();
5343
+ for(let s = 0; s < sigSegments; s++){
5344
+ const angle1 = sigRng() * Math.PI * 2;
5345
+ const angle2 = sigRng() * Math.PI * 2;
5346
+ const r1 = sigSize * (0.2 + sigRng() * 0.6);
5347
+ const r2 = sigSize * (0.2 + sigRng() * 0.6);
5348
+ ctx.moveTo(Math.cos(angle1) * r1, Math.sin(angle1) * r1);
5349
+ ctx.lineTo(Math.cos(angle2) * r2, Math.sin(angle2) * r2);
5350
+ }
5351
+ ctx.stroke();
5352
+ // Center dot
5353
+ ctx.beginPath();
5354
+ ctx.arc(0, 0, sigSize * 0.12, 0, Math.PI * 2);
5355
+ ctx.fillStyle = sigColor;
5356
+ ctx.fill();
5357
+ ctx.restore();
5358
+ }
4624
5359
  ctx.globalAlpha = 1;
4625
5360
  }
4626
5361