git-hash-art 0.10.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",
@@ -550,7 +730,8 @@ function $d016ad53434219a1$export$703ba40a4347f77a(base, layerRatio, hueShiftPer
550
730
  return {
551
731
  dominant: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.dominant, shift),
552
732
  secondary: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
553
- accent: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
733
+ accent: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
734
+ all: base.all.map((c)=>$d016ad53434219a1$export$1793a1bfbe4f6ff5(c, shift * 0.6))
554
735
  };
555
736
  }
556
737
 
@@ -1933,6 +2114,23 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
1933
2114
  ctx.fill();
1934
2115
  ctx.fillStyle = origFill;
1935
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
+ }
1936
2134
  ctx.globalAlpha = savedAlpha;
1937
2135
  // Soft stroke on top — thinner than normal for delicacy
1938
2136
  ctx.globalAlpha *= 0.25;
@@ -2242,6 +2440,23 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2242
2440
  ctx.stroke();
2243
2441
  ctx.restore();
2244
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
+ }
2245
2460
  ctx.globalAlpha = savedAlphaHD;
2246
2461
  break;
2247
2462
  }
@@ -2253,12 +2468,20 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2253
2468
  }
2254
2469
  }
2255
2470
  function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2256
- 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;
2257
2472
  ctx.save();
2258
2473
  ctx.translate(x, y);
2259
2474
  ctx.rotate(rotation * Math.PI / 180);
2260
- // Glow / shadow effect
2261
- 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)
2262
2485
  ctx.shadowBlur = glowRadius;
2263
2486
  ctx.shadowColor = glowColor || fillColor;
2264
2487
  ctx.shadowOffsetX = 0;
@@ -2280,8 +2503,29 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2280
2503
  });
2281
2504
  $c3de8257a8baa3b0$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
2282
2505
  }
2283
- // Reset shadow so patterns aren't double-glowed
2284
- 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
+ }
2285
2529
  // Layer additional patterns if specified
2286
2530
  if (patterns.length > 0) (0, $e4b03e131ed2a289$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
2287
2531
  baseSize: size,
@@ -3745,7 +3989,8 @@ const $4f72c5a314eddf25$var$COMPOSITION_MODES = [
3745
3989
  "flow-field",
3746
3990
  "spiral",
3747
3991
  "grid-subdivision",
3748
- "clustered"
3992
+ "clustered",
3993
+ "golden-spiral"
3749
3994
  ];
3750
3995
  // ── Helper: get position based on composition mode ──────────────────
3751
3996
  function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
@@ -3803,6 +4048,21 @@ function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height,
3803
4048
  x: rng() * width,
3804
4049
  y: rng() * height
3805
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
+ }
3806
4066
  }
3807
4067
  }
3808
4068
  // ── Helper: positional color from hierarchy ─────────────────────────
@@ -4271,11 +4531,24 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4271
4531
  }
4272
4532
  }
4273
4533
  ctx.globalAlpha = 1;
4274
- // ── 4. Flow field seed values ──────────────────────────────────
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);
4275
4539
  const fieldAngleBase = rng() * Math.PI * 2;
4276
- const fieldFreq = 0.5 + rng() * 2;
4540
+ const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
4277
4541
  function flowAngle(x, y) {
4278
- 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;
4279
4552
  }
4280
4553
  // Track all placed shapes for density checks and connecting curves
4281
4554
  const shapePositions = [];
@@ -4309,7 +4582,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4309
4582
  glowColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(heroStroke, 0.4),
4310
4583
  gradientFillEnd: (0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
4311
4584
  renderStyle: heroStyle,
4312
- rng: rng
4585
+ rng: rng,
4586
+ lightAngle: lightAngle,
4587
+ scaleFactor: scaleFactor
4313
4588
  });
4314
4589
  heroCenter = {
4315
4590
  x: heroFocal.x,
@@ -4368,7 +4643,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4368
4643
  }
4369
4644
  // Power distribution for size — archetype controls the curve
4370
4645
  const sizeT = Math.pow(rng(), archetype.sizePower);
4371
- const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
4646
+ const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
4372
4647
  // Size fraction for affinity-aware shape selection
4373
4648
  const sizeFraction = size / adjustedMaxSize;
4374
4649
  // Palette-driven shape selection (replaces naive pickShape)
@@ -4471,7 +4746,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4471
4746
  glowColor: hasGlow ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
4472
4747
  gradientFillEnd: gradientEnd,
4473
4748
  renderStyle: finalRenderStyle,
4474
- rng: rng
4749
+ rng: rng,
4750
+ lightAngle: lightAngle,
4751
+ scaleFactor: scaleFactor
4475
4752
  };
4476
4753
  if (shouldMirror) (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4477
4754
  ...shapeConfig,
@@ -4602,6 +4879,64 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4602
4879
  }
4603
4880
  // Reset blend mode for post-processing passes
4604
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
+ }
4605
4940
  // ── 6. Flow-line pass — variable color, branching, pressure ────
4606
4941
  const baseFlowLines = 6 + Math.floor(rng() * 10);
4607
4942
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
@@ -4813,6 +5148,176 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4813
5148
  ctx.restore();
4814
5149
  ctx.globalCompositeOperation = "source-over";
4815
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
+ }
4816
5321
  // ── 11. Signature mark — unique geometric chop from hash prefix ──
4817
5322
  {
4818
5323
  const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));