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/browser.js CHANGED
@@ -61,6 +61,136 @@ const $616009579e3d72c5$export$bb9e4790bc99ae59 = {
61
61
  PI: Math.PI,
62
62
  PHI: (1 + Math.sqrt(5)) / 2
63
63
  };
64
+ function $616009579e3d72c5$export$bbde7fbaaf9a8d66(rng) {
65
+ // Build a deterministic permutation table (256 entries, doubled)
66
+ const perm = new Uint8Array(512);
67
+ const p = new Uint8Array(256);
68
+ for(let i = 0; i < 256; i++)p[i] = i;
69
+ // Fisher-Yates shuffle with our seeded RNG
70
+ for(let i = 255; i > 0; i--){
71
+ const j = Math.floor(rng() * (i + 1));
72
+ const tmp = p[i];
73
+ p[i] = p[j];
74
+ p[j] = tmp;
75
+ }
76
+ for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
77
+ // 12 gradient vectors for 2D simplex
78
+ const GRAD2 = [
79
+ [
80
+ 1,
81
+ 1
82
+ ],
83
+ [
84
+ -1,
85
+ 1
86
+ ],
87
+ [
88
+ 1,
89
+ -1
90
+ ],
91
+ [
92
+ -1,
93
+ -1
94
+ ],
95
+ [
96
+ 1,
97
+ 0
98
+ ],
99
+ [
100
+ -1,
101
+ 0
102
+ ],
103
+ [
104
+ 0,
105
+ 1
106
+ ],
107
+ [
108
+ 0,
109
+ -1
110
+ ],
111
+ [
112
+ 1,
113
+ 1
114
+ ],
115
+ [
116
+ -1,
117
+ 1
118
+ ],
119
+ [
120
+ 1,
121
+ -1
122
+ ],
123
+ [
124
+ -1,
125
+ -1
126
+ ]
127
+ ];
128
+ const F2 = 0.5 * (Math.sqrt(3) - 1);
129
+ const G2 = (3 - Math.sqrt(3)) / 6;
130
+ function dot2(g, x, y) {
131
+ return g[0] * x + g[1] * y;
132
+ }
133
+ return function noise2D(xin, yin) {
134
+ const s = (xin + yin) * F2;
135
+ const i = Math.floor(xin + s);
136
+ const j = Math.floor(yin + s);
137
+ const t = (i + j) * G2;
138
+ const X0 = i - t;
139
+ const Y0 = j - t;
140
+ const x0 = xin - X0;
141
+ const y0 = yin - Y0;
142
+ let i1, j1;
143
+ if (x0 > y0) {
144
+ i1 = 1;
145
+ j1 = 0;
146
+ } else {
147
+ i1 = 0;
148
+ j1 = 1;
149
+ }
150
+ const x1 = x0 - i1 + G2;
151
+ const y1 = y0 - j1 + G2;
152
+ const x2 = x0 - 1 + 2 * G2;
153
+ const y2 = y0 - 1 + 2 * G2;
154
+ const ii = i & 255;
155
+ const jj = j & 255;
156
+ let n0 = 0, n1 = 0, n2 = 0;
157
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
158
+ if (t0 >= 0) {
159
+ t0 *= t0;
160
+ const gi0 = perm[ii + perm[jj]] % 12;
161
+ n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
162
+ }
163
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
164
+ if (t1 >= 0) {
165
+ t1 *= t1;
166
+ const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
167
+ n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
168
+ }
169
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
170
+ if (t2 >= 0) {
171
+ t2 *= t2;
172
+ const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
173
+ n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
174
+ }
175
+ // Scale to approximately [-1, 1]
176
+ return 70 * (n0 + n1 + n2);
177
+ };
178
+ }
179
+ function $616009579e3d72c5$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
180
+ return function fbm(x, y) {
181
+ let value = 0;
182
+ let amplitude = 1;
183
+ let frequency = 1;
184
+ let maxAmp = 0;
185
+ for(let i = 0; i < octaves; i++){
186
+ value += noise(x * frequency, y * frequency) * amplitude;
187
+ maxAmp += amplitude;
188
+ amplitude *= gain;
189
+ frequency *= lacunarity;
190
+ }
191
+ return value / maxAmp;
192
+ };
193
+ }
64
194
  class $616009579e3d72c5$export$da2372f11bc66b3f {
65
195
  static getProportionalSize(baseSize, proportion) {
66
196
  return baseSize * proportion;
@@ -254,6 +384,48 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
254
384
  $b5a262d09b87e373$var$hslToHex(baseHue, 0.7, 0.35)
255
385
  ];
256
386
  }
387
+ case "split-complementary":
388
+ {
389
+ // Base hue + two colors flanking the complement (±30°)
390
+ const comp = (baseHue + 180) % 360;
391
+ const split1 = (comp - 30 + 360) % 360;
392
+ const split2 = (comp + 30) % 360;
393
+ const sat = 0.55 + this.rng() * 0.25;
394
+ return [
395
+ $b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
396
+ $b5a262d09b87e373$var$hslToHex(baseHue, sat * 0.8, 0.65),
397
+ $b5a262d09b87e373$var$hslToHex(split1, sat, 0.5),
398
+ $b5a262d09b87e373$var$hslToHex(split2, sat, 0.5),
399
+ $b5a262d09b87e373$var$hslToHex(split1, sat * 0.7, 0.7)
400
+ ];
401
+ }
402
+ case "analogous-accent":
403
+ {
404
+ // Tight cluster of 3 analogous hues + 1 distant accent
405
+ const step = 15 + this.rng() * 20; // 15-35° apart
406
+ const h1 = (baseHue - step + 360) % 360;
407
+ const h2 = (baseHue + step) % 360;
408
+ const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
409
+ const sat = 0.5 + this.rng() * 0.3;
410
+ return [
411
+ $b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
412
+ $b5a262d09b87e373$var$hslToHex(h1, sat, 0.55),
413
+ $b5a262d09b87e373$var$hslToHex(h2, sat, 0.45),
414
+ $b5a262d09b87e373$var$hslToHex(accentHue, sat + 0.15, 0.5)
415
+ ];
416
+ }
417
+ case "limited-palette":
418
+ {
419
+ // Only 3 colors — like a risograph print
420
+ const h2 = (baseHue + 120 + this.rng() * 40) % 360;
421
+ const h3 = (baseHue + 220 + this.rng() * 40) % 360;
422
+ const sat = 0.6 + this.rng() * 0.2;
423
+ return [
424
+ $b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
425
+ $b5a262d09b87e373$var$hslToHex(h2, sat, 0.5),
426
+ $b5a262d09b87e373$var$hslToHex(h3, sat * 0.9, 0.55)
427
+ ];
428
+ }
257
429
  case "harmonious":
258
430
  default:
259
431
  return this.getColors();
@@ -274,6 +446,14 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
274
446
  "#f5f5f0",
275
447
  "#e8e8e0"
276
448
  ];
449
+ case "split-complementary":
450
+ case "analogous-accent":
451
+ return this.getBackgroundColors();
452
+ case "limited-palette":
453
+ return [
454
+ $b5a262d09b87e373$var$hslToHex(this.seed % 360, 0.08, 0.94),
455
+ $b5a262d09b87e373$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
456
+ ];
277
457
  case "neon":
278
458
  return [
279
459
  "#0a0a12",
@@ -518,6 +698,19 @@ function $b5a262d09b87e373$export$6d1620b367f86f7a(rng) {
518
698
  intensity: intensity
519
699
  };
520
700
  }
701
+ function $b5a262d09b87e373$export$1793a1bfbe4f6ff5(hex, degrees) {
702
+ const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
703
+ return $b5a262d09b87e373$var$hslToHex((h + degrees + 360) % 360, s, l);
704
+ }
705
+ function $b5a262d09b87e373$export$703ba40a4347f77a(base, layerRatio, hueShiftPerLayer) {
706
+ const shift = layerRatio * hueShiftPerLayer;
707
+ return {
708
+ dominant: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.dominant, shift),
709
+ secondary: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
710
+ accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
711
+ all: base.all.map((c)=>$b5a262d09b87e373$export$1793a1bfbe4f6ff5(c, shift * 0.6))
712
+ };
713
+ }
521
714
 
522
715
 
523
716
 
@@ -1801,7 +1994,8 @@ const $e0f99502ff383dd8$var$RENDER_STYLES = [
1801
1994
  "noise-grain",
1802
1995
  "wood-grain",
1803
1996
  "marble-vein",
1804
- "fabric-weave"
1997
+ "fabric-weave",
1998
+ "hand-drawn"
1805
1999
  ];
1806
2000
  function $e0f99502ff383dd8$export$9fd4e64b2acd410e(rng) {
1807
2001
  return $e0f99502ff383dd8$var$RENDER_STYLES[Math.floor(rng() * $e0f99502ff383dd8$var$RENDER_STYLES.length)];
@@ -1897,6 +2091,23 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
1897
2091
  ctx.fill();
1898
2092
  ctx.fillStyle = origFill;
1899
2093
  ctx.restore();
2094
+ // Pass 4: Organic edge erosion — irregular bites along the boundary
2095
+ if (rng && size > 20) {
2096
+ const erosionBites = 6 + Math.floor(rng() * 8);
2097
+ const edgeRadius = size * 0.45;
2098
+ ctx.save();
2099
+ ctx.globalCompositeOperation = "destination-out";
2100
+ ctx.globalAlpha = 0.6 + rng() * 0.3;
2101
+ for(let eb = 0; eb < erosionBites; eb++){
2102
+ const biteAngle = rng() * Math.PI * 2;
2103
+ const biteDist = edgeRadius * (0.85 + rng() * 0.25);
2104
+ const biteR = size * (0.02 + rng() * 0.04);
2105
+ ctx.beginPath();
2106
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2107
+ ctx.fill();
2108
+ }
2109
+ ctx.restore();
2110
+ }
1900
2111
  ctx.globalAlpha = savedAlpha;
1901
2112
  // Soft stroke on top — thinner than normal for delicacy
1902
2113
  ctx.globalAlpha *= 0.25;
@@ -2182,6 +2393,50 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
2182
2393
  ctx.globalAlpha /= 0.3;
2183
2394
  break;
2184
2395
  }
2396
+ case "hand-drawn":
2397
+ {
2398
+ // Wobbly hand-drawn edge treatment — fill normally, then redraw
2399
+ // the outline with perturbed control points for a sketchy feel
2400
+ const savedAlphaHD = ctx.globalAlpha;
2401
+ ctx.globalAlpha = savedAlphaHD * 0.85;
2402
+ ctx.fill();
2403
+ ctx.globalAlpha = savedAlphaHD;
2404
+ // Draw 2-3 slightly offset wobbly strokes for a sketchy look
2405
+ const wobblePasses = 2 + (rng ? Math.floor(rng() * 2) : 0);
2406
+ ctx.lineWidth = strokeWidth * 0.8;
2407
+ for(let wp = 0; wp < wobblePasses; wp++){
2408
+ ctx.globalAlpha = savedAlphaHD * (0.4 - wp * 0.1);
2409
+ ctx.save();
2410
+ // Slight random offset per pass
2411
+ const wobbleX = rng ? (rng() - 0.5) * size * 0.02 : 0;
2412
+ const wobbleY = rng ? (rng() - 0.5) * size * 0.02 : 0;
2413
+ ctx.translate(wobbleX, wobbleY);
2414
+ // Slightly different scale per pass for edge variation
2415
+ const wobbleScale = 1 + (rng ? (rng() - 0.5) * 0.03 : 0);
2416
+ ctx.scale(wobbleScale, wobbleScale);
2417
+ ctx.stroke();
2418
+ ctx.restore();
2419
+ }
2420
+ // Organic edge erosion — small irregular bites for rough paper feel
2421
+ if (rng && size > 20) {
2422
+ const erosionBites = 4 + Math.floor(rng() * 6);
2423
+ const edgeRadius = size * 0.42;
2424
+ ctx.save();
2425
+ ctx.globalCompositeOperation = "destination-out";
2426
+ ctx.globalAlpha = 0.5 + rng() * 0.3;
2427
+ for(let eb = 0; eb < erosionBites; eb++){
2428
+ const biteAngle = rng() * Math.PI * 2;
2429
+ const biteDist = edgeRadius * (0.9 + rng() * 0.2);
2430
+ const biteR = size * (0.015 + rng() * 0.03);
2431
+ ctx.beginPath();
2432
+ ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
2433
+ ctx.fill();
2434
+ }
2435
+ ctx.restore();
2436
+ }
2437
+ ctx.globalAlpha = savedAlphaHD;
2438
+ break;
2439
+ }
2185
2440
  case "fill-and-stroke":
2186
2441
  default:
2187
2442
  ctx.fill();
@@ -2190,12 +2445,20 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
2190
2445
  }
2191
2446
  }
2192
2447
  function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2193
- 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;
2448
+ 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;
2194
2449
  ctx.save();
2195
2450
  ctx.translate(x, y);
2196
2451
  ctx.rotate(rotation * Math.PI / 180);
2197
- // Glow / shadow effect
2198
- if (glowRadius > 0) {
2452
+ // ── Drop shadow — soft colored shadow offset along light direction ──
2453
+ if (lightAngle !== undefined && size > 10) {
2454
+ const shadowDist = size * 0.035;
2455
+ const shadowBlurR = size * 0.06;
2456
+ ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
2457
+ ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
2458
+ ctx.shadowBlur = shadowBlurR;
2459
+ ctx.shadowColor = "rgba(0,0,0,0.12)";
2460
+ } else if (glowRadius > 0) {
2461
+ // Glow / shadow effect (legacy path)
2199
2462
  ctx.shadowBlur = glowRadius;
2200
2463
  ctx.shadowColor = glowColor || fillColor;
2201
2464
  ctx.shadowOffsetX = 0;
@@ -2217,8 +2480,29 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2217
2480
  });
2218
2481
  $e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
2219
2482
  }
2220
- // Reset shadow so patterns aren't double-glowed
2221
- if (glowRadius > 0) ctx.shadowBlur = 0;
2483
+ // Reset shadow so patterns and highlight aren't double-shadowed
2484
+ ctx.shadowBlur = 0;
2485
+ ctx.shadowOffsetX = 0;
2486
+ ctx.shadowOffsetY = 0;
2487
+ ctx.shadowColor = "transparent";
2488
+ // ── Specular highlight — bright arc on the light-facing side ──
2489
+ if (lightAngle !== undefined && size > 15 && rng) {
2490
+ const hlRadius = size * 0.35;
2491
+ const hlDist = size * 0.15;
2492
+ const hlX = Math.cos(lightAngle) * hlDist;
2493
+ const hlY = Math.sin(lightAngle) * hlDist;
2494
+ const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2495
+ hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
2496
+ hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
2497
+ hlGrad.addColorStop(1, "rgba(255,255,255,0)");
2498
+ const savedOp = ctx.globalCompositeOperation;
2499
+ ctx.globalCompositeOperation = "soft-light";
2500
+ ctx.fillStyle = hlGrad;
2501
+ ctx.beginPath();
2502
+ ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
2503
+ ctx.fill();
2504
+ ctx.globalCompositeOperation = savedOp;
2505
+ }
2222
2506
  // Layer additional patterns if specified
2223
2507
  if (patterns.length > 0) (0, $616009579e3d72c5$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
2224
2508
  baseSize: size,
@@ -2317,7 +2601,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2317
2601
  bestStyles: [
2318
2602
  "fill-only",
2319
2603
  "watercolor",
2320
- "fill-and-stroke"
2604
+ "fill-and-stroke",
2605
+ "hand-drawn"
2321
2606
  ]
2322
2607
  },
2323
2608
  square: {
@@ -2354,7 +2639,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2354
2639
  bestStyles: [
2355
2640
  "fill-and-stroke",
2356
2641
  "fill-only",
2357
- "watercolor"
2642
+ "watercolor",
2643
+ "hand-drawn"
2358
2644
  ]
2359
2645
  },
2360
2646
  hexagon: {
@@ -2734,7 +3020,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2734
3020
  bestStyles: [
2735
3021
  "fill-only",
2736
3022
  "watercolor",
2737
- "fill-and-stroke"
3023
+ "fill-and-stroke",
3024
+ "hand-drawn"
2738
3025
  ]
2739
3026
  },
2740
3027
  ngon: {
@@ -3614,8 +3901,51 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3614
3901
  invertForeground: false
3615
3902
  }
3616
3903
  ];
3904
+ /**
3905
+ * Linearly interpolate between two archetype numeric parameters.
3906
+ */ function $68a238ccd77f2bcd$var$lerpNum(a, b, t) {
3907
+ return a + (b - a) * t;
3908
+ }
3909
+ /**
3910
+ * Blend two archetypes by interpolating their numeric parameters
3911
+ * and merging their style arrays.
3912
+ */ function $68a238ccd77f2bcd$var$blendArchetypes(a, b, t) {
3913
+ // Merge preferred styles — unique union, primary archetype first
3914
+ const mergedStyles = [
3915
+ ...new Set([
3916
+ ...a.preferredStyles,
3917
+ ...b.preferredStyles
3918
+ ])
3919
+ ];
3920
+ return {
3921
+ name: `${a.name}+${b.name}`,
3922
+ gridSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.gridSize, b.gridSize, t)),
3923
+ layers: Math.round($68a238ccd77f2bcd$var$lerpNum(a.layers, b.layers, t)),
3924
+ baseOpacity: $68a238ccd77f2bcd$var$lerpNum(a.baseOpacity, b.baseOpacity, t),
3925
+ opacityReduction: $68a238ccd77f2bcd$var$lerpNum(a.opacityReduction, b.opacityReduction, t),
3926
+ minShapeSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.minShapeSize, b.minShapeSize, t)),
3927
+ maxShapeSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.maxShapeSize, b.maxShapeSize, t)),
3928
+ backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3929
+ paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3930
+ preferredStyles: mergedStyles,
3931
+ flowLineMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3932
+ heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3933
+ glowMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
3934
+ sizePower: $68a238ccd77f2bcd$var$lerpNum(a.sizePower, b.sizePower, t),
3935
+ invertForeground: t < 0.5 ? a.invertForeground : b.invertForeground
3936
+ };
3937
+ }
3617
3938
  function $68a238ccd77f2bcd$export$f1142fd7da4d6590(rng) {
3618
- return $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3939
+ const primary = $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3940
+ // ~15% chance of blending with a second archetype
3941
+ if (rng() < 0.15) {
3942
+ const secondary = $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3943
+ if (secondary.name !== primary.name) {
3944
+ const blendT = 0.25 + rng() * 0.25; // 25-50% blend toward secondary
3945
+ return $68a238ccd77f2bcd$var$blendArchetypes(primary, secondary, blendT);
3946
+ }
3947
+ }
3948
+ return primary;
3619
3949
  }
3620
3950
 
3621
3951
 
@@ -3636,7 +3966,8 @@ const $1f63dc64b5593c73$var$COMPOSITION_MODES = [
3636
3966
  "flow-field",
3637
3967
  "spiral",
3638
3968
  "grid-subdivision",
3639
- "clustered"
3969
+ "clustered",
3970
+ "golden-spiral"
3640
3971
  ];
3641
3972
  // ── Helper: get position based on composition mode ──────────────────
3642
3973
  function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
@@ -3694,6 +4025,21 @@ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height,
3694
4025
  x: rng() * width,
3695
4026
  y: rng() * height
3696
4027
  };
4028
+ case "golden-spiral":
4029
+ {
4030
+ // Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
4031
+ const PHI = (1 + Math.sqrt(5)) / 2;
4032
+ const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
4033
+ const t = shapeIndex / totalShapes;
4034
+ const angle = shapeIndex * goldenAngle + rng() * 0.3;
4035
+ const maxR = Math.min(width, height) * 0.44;
4036
+ // Shapes spiral outward with sqrt distribution for even area coverage
4037
+ const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
4038
+ return {
4039
+ x: cx + Math.cos(angle) * r,
4040
+ y: cy + Math.sin(angle) * r
4041
+ };
4042
+ }
3697
4043
  }
3698
4044
  }
3699
4045
  // ── Helper: positional color from hierarchy ─────────────────────────
@@ -3951,6 +4297,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3951
4297
  const colorGrade = (0, $b5a262d09b87e373$export$6d1620b367f86f7a)(rng);
3952
4298
  // ── 0e. Light direction — consistent shadow angle ──────────────
3953
4299
  const lightAngle = rng() * Math.PI * 2;
4300
+ // ── 0f. Palette evolution — hue drift direction across layers ──
4301
+ const paletteHueShift = (rng() - 0.5) * 40; // -20° to +20° total drift
3954
4302
  const scaleFactor = Math.min(width, height) / 1024;
3955
4303
  const adjustedMinSize = minShapeSize * scaleFactor;
3956
4304
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -4125,11 +4473,59 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4125
4473
  ry + (nearest.y - ry) * pull
4126
4474
  ];
4127
4475
  }
4128
- // ── 4. Flow field seed values ──────────────────────────────────
4476
+ // ── 3b. Void zone decoration intentional negative space ────
4477
+ for (const zone of voidZones){
4478
+ // Subtle halo ring around void zones
4479
+ ctx.globalAlpha = 0.04 + rng() * 0.04;
4480
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
4481
+ ctx.lineWidth = 1.5 * scaleFactor;
4482
+ ctx.beginPath();
4483
+ ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
4484
+ ctx.stroke();
4485
+ // ~50% chance: scatter tiny dots inside the void
4486
+ if (rng() < 0.5) {
4487
+ const dotCount = 3 + Math.floor(rng() * 6);
4488
+ ctx.globalAlpha = 0.06 + rng() * 0.04;
4489
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
4490
+ for(let d = 0; d < dotCount; d++){
4491
+ const angle = rng() * Math.PI * 2;
4492
+ const dist = rng() * zone.radius * 0.7;
4493
+ const dotR = (1 + rng() * 3) * scaleFactor;
4494
+ ctx.beginPath();
4495
+ ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
4496
+ ctx.fill();
4497
+ }
4498
+ }
4499
+ // ~30% chance: thin concentric ring inside
4500
+ if (rng() < 0.3) {
4501
+ ctx.globalAlpha = 0.03 + rng() * 0.03;
4502
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4503
+ ctx.lineWidth = 0.5 * scaleFactor;
4504
+ const innerR = zone.radius * (0.4 + rng() * 0.3);
4505
+ ctx.beginPath();
4506
+ ctx.arc(zone.x, zone.y, innerR, 0, Math.PI * 2);
4507
+ ctx.stroke();
4508
+ }
4509
+ }
4510
+ ctx.globalAlpha = 1;
4511
+ // ── 4. Flow field — simplex noise for organic variation ─────────
4512
+ // Create a seeded simplex noise field (unique per hash)
4513
+ const noiseFieldRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 333));
4514
+ const simplexNoise = (0, $616009579e3d72c5$export$bbde7fbaaf9a8d66)(noiseFieldRng);
4515
+ const fbmNoise = (0, $616009579e3d72c5$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
4129
4516
  const fieldAngleBase = rng() * Math.PI * 2;
4130
- const fieldFreq = 0.5 + rng() * 2;
4517
+ const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
4131
4518
  function flowAngle(x, y) {
4132
- 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;
4519
+ // Sample FBM noise at the position, scaled by frequency
4520
+ const nx = x / width * fieldFreq;
4521
+ const ny = y / height * fieldFreq;
4522
+ return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
4523
+ }
4524
+ // Noise-based size modulation — shapes in "high noise" areas get scaled
4525
+ function noiseSizeModulation(x, y) {
4526
+ const n = simplexNoise(x / width * 3, y / height * 3);
4527
+ // Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
4528
+ return 0.7 + (n + 1) * 0.3;
4133
4529
  }
4134
4530
  // Track all placed shapes for density checks and connecting curves
4135
4531
  const shapePositions = [];
@@ -4163,7 +4559,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4163
4559
  glowColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(heroStroke, 0.4),
4164
4560
  gradientFillEnd: (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
4165
4561
  renderStyle: heroStyle,
4166
- rng: rng
4562
+ rng: rng,
4563
+ lightAngle: lightAngle,
4564
+ scaleFactor: scaleFactor
4167
4565
  });
4168
4566
  heroCenter = {
4169
4567
  x: heroFocal.x,
@@ -4197,6 +4595,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4197
4595
  const dofFactor = 1 - layerRatio * 0.5; // 1.0 for front layer, 0.5 for back
4198
4596
  const dofStrokeScale = 0.4 + dofFactor * 0.6; // strokes thin out with depth
4199
4597
  const dofContrastReduction = layerRatio * 0.2; // colors fade toward bg
4598
+ // Color palette evolution — hue-rotate the hierarchy per layer
4599
+ const layerHierarchy = (0, $b5a262d09b87e373$export$703ba40a4347f77a)(colorHierarchy, layerRatio, paletteHueShift);
4600
+ // Focal depth: shapes near focal points get more detail
4601
+ const focalDetailBoost = (px, py)=>{
4602
+ let minFocalDist = Infinity;
4603
+ for (const fp of focalPoints){
4604
+ const d = Math.hypot(px - fp.x, py - fp.y);
4605
+ if (d < minFocalDist) minFocalDist = d;
4606
+ }
4607
+ const maxDist = Math.hypot(width, height) * 0.5;
4608
+ return Math.max(0, 1 - minFocalDist / maxDist); // 1.0 at focal, 0.0 at edges
4609
+ };
4200
4610
  for(let i = 0; i < numShapes; i++){
4201
4611
  // Position from composition mode, then focal bias
4202
4612
  const rawPos = $1f63dc64b5593c73$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
@@ -4210,7 +4620,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4210
4620
  }
4211
4621
  // Power distribution for size — archetype controls the curve
4212
4622
  const sizeT = Math.pow(rng(), archetype.sizePower);
4213
- const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
4623
+ const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
4214
4624
  // Size fraction for affinity-aware shape selection
4215
4625
  const sizeFraction = size / adjustedMaxSize;
4216
4626
  // Palette-driven shape selection (replaces naive pickShape)
@@ -4227,9 +4637,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4227
4637
  rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
4228
4638
  }
4229
4639
  }
4230
- // Positional color from hierarchy + jitter
4231
- let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
4232
- const strokeBase = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4640
+ // Positional color from hierarchy + jitter (using evolved layer palette)
4641
+ let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, layerHierarchy, rng);
4642
+ const strokeBase = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng);
4233
4643
  // Desaturate colors on later layers for depth
4234
4644
  if (atmosphericDesat > 0) fillBase = (0, $b5a262d09b87e373$export$fb75607d98509d9)(fillBase, atmosphericDesat);
4235
4645
  // Temperature contrast: shift foreground shapes opposite to background
@@ -4313,7 +4723,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4313
4723
  glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
4314
4724
  gradientFillEnd: gradientEnd,
4315
4725
  renderStyle: finalRenderStyle,
4316
- rng: rng
4726
+ rng: rng,
4727
+ lightAngle: lightAngle,
4728
+ scaleFactor: scaleFactor
4317
4729
  };
4318
4730
  if (shouldMirror) (0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4319
4731
  ...shapeConfig,
@@ -4321,6 +4733,25 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4321
4733
  mirrorGap: size * (0.1 + rng() * 0.3)
4322
4734
  });
4323
4735
  else (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
4736
+ // ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
4737
+ if (rng() < 0.2 && size > adjustedMinSize * 2) {
4738
+ const glazePasses = 2 + Math.floor(rng() * 2);
4739
+ for(let g = 0; g < glazePasses; g++){
4740
+ const glazeScale = 1 - (g + 1) * 0.12; // progressively smaller
4741
+ const glazeAlpha = 0.08 + g * 0.04; // progressively more opaque toward center
4742
+ ctx.globalAlpha = glazeAlpha;
4743
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
4744
+ fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
4745
+ strokeColor: "rgba(0,0,0,0)",
4746
+ strokeWidth: 0,
4747
+ size: size * glazeScale,
4748
+ rotation: rotation,
4749
+ proportionType: "GOLDEN_RATIO",
4750
+ renderStyle: "fill-only",
4751
+ rng: rng
4752
+ });
4753
+ }
4754
+ }
4324
4755
  shapePositions.push({
4325
4756
  x: finalX,
4326
4757
  y: finalY,
@@ -4358,7 +4789,10 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4358
4789
  }
4359
4790
  }
4360
4791
  // ── 5d. Recursive nesting ──────────────────────────────────
4361
- if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
4792
+ // Focal depth: shapes near focal points get more detail
4793
+ const focalProximity = focalDetailBoost(finalX, finalY);
4794
+ const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
4795
+ if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
4362
4796
  const innerCount = 1 + Math.floor(rng() * 3);
4363
4797
  for(let n = 0; n < innerCount; n++){
4364
4798
  // Pick inner shape from palette affinities
@@ -4383,7 +4817,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4383
4817
  }
4384
4818
  }
4385
4819
  // ── 5e. Shape constellations — pre-composed groups ─────────
4386
- if (size > adjustedMaxSize * 0.35 && rng() < 0.12) {
4820
+ const constellationChance = 0.12 + focalProximity * 0.1; // 12-22% near focal
4821
+ if (size > adjustedMaxSize * 0.35 && rng() < constellationChance) {
4387
4822
  const constellation = $1f63dc64b5593c73$var$CONSTELLATIONS[Math.floor(rng() * $1f63dc64b5593c73$var$CONSTELLATIONS.length)];
4388
4823
  const members = constellation.build(rng, size);
4389
4824
  const groupRotation = rng() * Math.PI * 2;
@@ -4421,6 +4856,64 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4421
4856
  }
4422
4857
  // Reset blend mode for post-processing passes
4423
4858
  ctx.globalCompositeOperation = "source-over";
4859
+ // ── 5f. Layered masking / cutout portals ───────────────────────
4860
+ // ~18% of images get 1-3 portal windows that paint over foreground
4861
+ // with a tinted background wash, creating a "peek through" effect.
4862
+ if (rng() < 0.18 && shapePositions.length > 3) {
4863
+ const portalCount = 1 + Math.floor(rng() * 2);
4864
+ for(let p = 0; p < portalCount; p++){
4865
+ // Pick a position biased toward placed shapes
4866
+ const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
4867
+ const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
4868
+ const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
4869
+ const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
4870
+ // Pick a portal shape from the palette
4871
+ const portalShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
4872
+ const portalRotation = rng() * 360;
4873
+ const portalAlpha = 0.6 + rng() * 0.35;
4874
+ ctx.save();
4875
+ ctx.translate(portalX, portalY);
4876
+ ctx.rotate(portalRotation * Math.PI / 180);
4877
+ // Step 1: Clip to the portal shape and fill with background wash
4878
+ ctx.beginPath();
4879
+ (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
4880
+ ctx.clip();
4881
+ // Fill the clipped region with a radial gradient from background colors
4882
+ const portalColor = (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
4883
+ const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
4884
+ portalGrad.addColorStop(0, portalColor);
4885
+ portalGrad.addColorStop(1, bgEnd);
4886
+ ctx.globalAlpha = portalAlpha;
4887
+ ctx.fillStyle = portalGrad;
4888
+ ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
4889
+ // Optional: subtle inner texture — a few tiny dots inside the portal
4890
+ if (rng() < 0.5) {
4891
+ const dotCount = 3 + Math.floor(rng() * 5);
4892
+ ctx.globalAlpha = portalAlpha * 0.3;
4893
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
4894
+ for(let d = 0; d < dotCount; d++){
4895
+ const dx = (rng() - 0.5) * portalSize * 1.4;
4896
+ const dy = (rng() - 0.5) * portalSize * 1.4;
4897
+ const dr = (1 + rng() * 3) * scaleFactor;
4898
+ ctx.beginPath();
4899
+ ctx.arc(dx, dy, dr, 0, Math.PI * 2);
4900
+ ctx.fill();
4901
+ }
4902
+ }
4903
+ ctx.restore();
4904
+ // Step 2: Draw a border ring around the portal (outside the clip)
4905
+ ctx.save();
4906
+ ctx.translate(portalX, portalY);
4907
+ ctx.rotate(portalRotation * Math.PI / 180);
4908
+ ctx.globalAlpha = 0.15 + rng() * 0.2;
4909
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
4910
+ ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
4911
+ ctx.beginPath();
4912
+ (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
4913
+ ctx.stroke();
4914
+ ctx.restore();
4915
+ }
4916
+ }
4424
4917
  // ── 6. Flow-line pass — variable color, branching, pressure ────
4425
4918
  const baseFlowLines = 6 + Math.floor(rng() * 10);
4426
4919
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
@@ -4487,7 +4980,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4487
4980
  prevY = fy;
4488
4981
  }
4489
4982
  }
4490
- // ── 6b. Apply symmetry mirroring ─────────────────────────────────
4983
+ // ── 6b. Motion/energy lines short directional bursts ─────────
4984
+ const energyArchetypes = [
4985
+ "dense-chaotic",
4986
+ "cosmic",
4987
+ "neon-glow",
4988
+ "bold-graphic"
4989
+ ];
4990
+ const hasEnergyLines = energyArchetypes.some((a)=>archetype.name.includes(a)) || rng() < 0.25;
4991
+ if (hasEnergyLines && shapePositions.length > 0) {
4992
+ const energyCount = 5 + Math.floor(rng() * 10);
4993
+ ctx.lineCap = "round";
4994
+ for(let e = 0; e < energyCount; e++){
4995
+ // Pick a random shape to radiate from
4996
+ const source = shapePositions[Math.floor(rng() * shapePositions.length)];
4997
+ const burstCount = 2 + Math.floor(rng() * 4);
4998
+ const baseAngle = flowAngle(source.x, source.y);
4999
+ for(let b = 0; b < burstCount; b++){
5000
+ const angle = baseAngle + (rng() - 0.5) * 1.2;
5001
+ const lineLen = (source.size * 0.3 + rng() * source.size * 0.5) * scaleFactor * 0.3;
5002
+ const startDist = source.size * 0.5;
5003
+ const sx = source.x + Math.cos(angle) * startDist;
5004
+ const sy = source.y + Math.sin(angle) * startDist;
5005
+ const ex = sx + Math.cos(angle) * lineLen;
5006
+ const ey = sy + Math.sin(angle) * lineLen;
5007
+ ctx.globalAlpha = 0.04 + rng() * 0.06;
5008
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
5009
+ ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
5010
+ ctx.beginPath();
5011
+ ctx.moveTo(sx, sy);
5012
+ ctx.lineTo(ex, ey);
5013
+ ctx.stroke();
5014
+ }
5015
+ }
5016
+ }
5017
+ // ── 6c. Apply symmetry mirroring ─────────────────────────────────
4491
5018
  if (symmetryMode !== "none") {
4492
5019
  const canvas = ctx.canvas;
4493
5020
  ctx.save();
@@ -4598,6 +5125,214 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4598
5125
  ctx.restore();
4599
5126
  ctx.globalCompositeOperation = "source-over";
4600
5127
  }
5128
+ // 10d. Gradient map — map luminance through a two-color gradient
5129
+ // Uses dominant→accent as the dark→light ramp for a cohesive tonal look
5130
+ if (rng() < 0.35) {
5131
+ const gmDark = colorHierarchy.dominant;
5132
+ const gmLight = colorHierarchy.accent;
5133
+ ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
5134
+ ctx.globalCompositeOperation = "color";
5135
+ // Paint a linear gradient from dark color (top) to light color (bottom)
5136
+ const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
5137
+ gmGrad.addColorStop(0, gmDark);
5138
+ gmGrad.addColorStop(1, gmLight);
5139
+ ctx.fillStyle = gmGrad;
5140
+ ctx.fillRect(0, 0, width, height);
5141
+ ctx.globalCompositeOperation = "source-over";
5142
+ }
5143
+ // ── 10e. Generative borders — archetype-driven decorative frames ──
5144
+ {
5145
+ ctx.save();
5146
+ ctx.globalAlpha = 1;
5147
+ ctx.globalCompositeOperation = "source-over";
5148
+ const borderRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 314));
5149
+ const borderPad = Math.min(width, height) * 0.025;
5150
+ const borderColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5151
+ const borderColorSolid = colorHierarchy.accent;
5152
+ const archName = archetype.name;
5153
+ if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
5154
+ // Clean ruled lines with corner ornaments
5155
+ ctx.strokeStyle = borderColor;
5156
+ ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
5157
+ ctx.globalAlpha = 0.18 + borderRng() * 0.1;
5158
+ // Outer rule
5159
+ ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
5160
+ // Inner rule (thinner, offset)
5161
+ const innerPad = borderPad * 1.8;
5162
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5163
+ ctx.globalAlpha *= 0.7;
5164
+ ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
5165
+ // Corner ornaments — small squares at each corner
5166
+ const ornSize = borderPad * 0.6;
5167
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(borderColorSolid, 0.12);
5168
+ const corners = [
5169
+ [
5170
+ borderPad,
5171
+ borderPad
5172
+ ],
5173
+ [
5174
+ width - borderPad - ornSize,
5175
+ borderPad
5176
+ ],
5177
+ [
5178
+ borderPad,
5179
+ height - borderPad - ornSize
5180
+ ],
5181
+ [
5182
+ width - borderPad - ornSize,
5183
+ height - borderPad - ornSize
5184
+ ]
5185
+ ];
5186
+ for (const [cx2, cy2] of corners){
5187
+ ctx.fillRect(cx2, cy2, ornSize, ornSize);
5188
+ // Diagonal cross inside ornament
5189
+ ctx.beginPath();
5190
+ ctx.moveTo(cx2, cy2);
5191
+ ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
5192
+ ctx.moveTo(cx2 + ornSize, cy2);
5193
+ ctx.lineTo(cx2, cy2 + ornSize);
5194
+ ctx.stroke();
5195
+ }
5196
+ } else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
5197
+ // Vine tendrils — organic curving lines along edges
5198
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
5199
+ ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
5200
+ ctx.globalAlpha = 0.12 + borderRng() * 0.08;
5201
+ ctx.lineCap = "round";
5202
+ const tendrilCount = 8 + Math.floor(borderRng() * 8);
5203
+ for(let t = 0; t < tendrilCount; t++){
5204
+ // Start from a random edge point
5205
+ const edge = Math.floor(borderRng() * 4);
5206
+ let tx, ty;
5207
+ if (edge === 0) {
5208
+ tx = borderRng() * width;
5209
+ ty = borderPad;
5210
+ } else if (edge === 1) {
5211
+ tx = borderRng() * width;
5212
+ ty = height - borderPad;
5213
+ } else if (edge === 2) {
5214
+ tx = borderPad;
5215
+ ty = borderRng() * height;
5216
+ } else {
5217
+ tx = width - borderPad;
5218
+ ty = borderRng() * height;
5219
+ }
5220
+ ctx.beginPath();
5221
+ ctx.moveTo(tx, ty);
5222
+ const segs = 3 + Math.floor(borderRng() * 4);
5223
+ for(let s = 0; s < segs; s++){
5224
+ const inward = borderPad * (1 + borderRng() * 2);
5225
+ // Curl inward from edge
5226
+ const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
5227
+ const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
5228
+ const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
5229
+ const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
5230
+ tx = cpx3;
5231
+ ty = cpy3;
5232
+ ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
5233
+ }
5234
+ ctx.stroke();
5235
+ // Small leaf/dot at tendril end
5236
+ if (borderRng() < 0.6) {
5237
+ ctx.beginPath();
5238
+ ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
5239
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
5240
+ ctx.fill();
5241
+ }
5242
+ }
5243
+ } else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
5244
+ // Star-studded arcs along edges
5245
+ ctx.globalAlpha = 0.1 + borderRng() * 0.08;
5246
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
5247
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
5248
+ ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
5249
+ // Subtle arc along top and bottom
5250
+ ctx.beginPath();
5251
+ ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
5252
+ ctx.stroke();
5253
+ ctx.beginPath();
5254
+ ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
5255
+ ctx.stroke();
5256
+ // Scatter small stars along the border region
5257
+ const starCount = 15 + Math.floor(borderRng() * 15);
5258
+ for(let s = 0; s < starCount; s++){
5259
+ const edge = Math.floor(borderRng() * 4);
5260
+ let sx, sy;
5261
+ if (edge === 0) {
5262
+ sx = borderRng() * width;
5263
+ sy = borderPad * (0.5 + borderRng());
5264
+ } else if (edge === 1) {
5265
+ sx = borderRng() * width;
5266
+ sy = height - borderPad * (0.5 + borderRng());
5267
+ } else if (edge === 2) {
5268
+ sx = borderPad * (0.5 + borderRng());
5269
+ sy = borderRng() * height;
5270
+ } else {
5271
+ sx = width - borderPad * (0.5 + borderRng());
5272
+ sy = borderRng() * height;
5273
+ }
5274
+ const starR = (1 + borderRng() * 2.5) * scaleFactor;
5275
+ // 4-point star
5276
+ ctx.beginPath();
5277
+ for(let p = 0; p < 8; p++){
5278
+ const a = p / 8 * Math.PI * 2;
5279
+ const r = p % 2 === 0 ? starR : starR * 0.4;
5280
+ const px2 = sx + Math.cos(a) * r;
5281
+ const py2 = sy + Math.sin(a) * r;
5282
+ if (p === 0) ctx.moveTo(px2, py2);
5283
+ else ctx.lineTo(px2, py2);
5284
+ }
5285
+ ctx.closePath();
5286
+ ctx.fill();
5287
+ }
5288
+ } else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
5289
+ // Thin single rule — understated elegance
5290
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
5291
+ ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
5292
+ ctx.globalAlpha = 0.1 + borderRng() * 0.06;
5293
+ ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
5294
+ }
5295
+ // Other archetypes: no border (intentional — not every image needs one)
5296
+ ctx.restore();
5297
+ }
5298
+ // ── 11. Signature mark — unique geometric chop from hash prefix ──
5299
+ {
5300
+ const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
5301
+ const sigSize = Math.min(width, height) * 0.025;
5302
+ // Bottom-right corner with padding
5303
+ const sigX = width - sigSize * 2.5;
5304
+ const sigY = height - sigSize * 2.5;
5305
+ const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
5306
+ const sigColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
5307
+ ctx.save();
5308
+ ctx.globalAlpha = 0.12 + sigRng() * 0.08;
5309
+ ctx.translate(sigX, sigY);
5310
+ ctx.strokeStyle = sigColor;
5311
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.06);
5312
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
5313
+ // Outer ring
5314
+ ctx.beginPath();
5315
+ ctx.arc(0, 0, sigSize, 0, Math.PI * 2);
5316
+ ctx.stroke();
5317
+ ctx.fill();
5318
+ // Inner geometric pattern — unique per hash
5319
+ ctx.beginPath();
5320
+ for(let s = 0; s < sigSegments; s++){
5321
+ const angle1 = sigRng() * Math.PI * 2;
5322
+ const angle2 = sigRng() * Math.PI * 2;
5323
+ const r1 = sigSize * (0.2 + sigRng() * 0.6);
5324
+ const r2 = sigSize * (0.2 + sigRng() * 0.6);
5325
+ ctx.moveTo(Math.cos(angle1) * r1, Math.sin(angle1) * r1);
5326
+ ctx.lineTo(Math.cos(angle2) * r2, Math.sin(angle2) * r2);
5327
+ }
5328
+ ctx.stroke();
5329
+ // Center dot
5330
+ ctx.beginPath();
5331
+ ctx.arc(0, 0, sigSize * 0.12, 0, Math.PI * 2);
5332
+ ctx.fillStyle = sigColor;
5333
+ ctx.fill();
5334
+ ctx.restore();
5335
+ }
4601
5336
  ctx.globalAlpha = 1;
4602
5337
  }
4603
5338