git-hash-art 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ALGORITHM.md CHANGED
@@ -9,10 +9,11 @@ Hash String
9
9
 
10
10
  ├─► Seed (mulberry32 PRNG)
11
11
 
12
- ├─► Archetype Selection (1 of 17 visual personalities)
12
+ ├─► Archetype Selection (1 of 17 visual personalities, ~15% chance of blending two)
13
13
 
14
14
  ├─► Color Scheme (palette mode + temperature mode + contrast enforcement)
15
15
  │ └─► Color Hierarchy (dominant 60% / secondary 25% / accent 15%)
16
+ │ └─► Per-Layer Palette Evolution (±20° hue drift across layers)
16
17
 
17
18
  ├─► Shape Palette (affinity-curated primary / supporting / accent shapes)
18
19
 
@@ -33,12 +34,15 @@ Hash String
33
34
  2. Composition Mode Selection
34
35
  2b. Symmetry Mode Selection (none / bilateral / quad)
35
36
  3. Focal Points (rule-of-thirds biased) + Void Zones
37
+ 3b. Void Zone Decoration (halos, scattered dots, concentric rings)
36
38
  4. Flow Field Initialization
37
39
  4b. Hero Shape (palette-aware, affinity-styled)
38
40
  5. Shape Layers (× N layers, archetype-tuned)
39
41
  │ ├─ Blend Mode (per-layer compositing)
40
42
  │ ├─ Render Style (affinity-aware per shape)
41
43
  │ ├─ Depth-of-Field (stroke thinning + contrast reduction on far layers)
44
+ │ ├─ Color Palette Evolution (per-layer hue drift via evolveHierarchy)
45
+ │ ├─ Focal Depth Boost (nesting/constellation chance ↑ near focal points)
42
46
  │ ├─ Position (composition mode + focal bias + density check)
43
47
  │ ├─ Shape Selection (palette-driven with size constraints)
44
48
  │ ├─ Hero Avoidance Field (nearby shapes orient toward hero)
@@ -53,14 +57,18 @@ Hash String
53
57
  │ ├─ 5d. Recursive Nesting (~15% of large shapes, palette-aware)
54
58
  │ └─ 5e. Shape Constellations (~12% of large shapes, pre-composed groups)
55
59
  6. Flow-Line Pass (variable color, pressure, branching)
56
- 6b. Symmetry Mirroring (bilateral-x, bilateral-y, or quad)
57
- 7. Noise Texture Overlay
58
- 8. Vignette (radial edge darkening)
59
- 9. Organic Connecting Curves
60
- 10. Post-Processing
60
+ 6b. Motion/Energy Lines (directional bursts from shapes)
61
+ 6c. Layered Transparency / Glazing (~20% of shapes get multi-pass redraws)
62
+ 7. Symmetry Mirroring (bilateral-x, bilateral-y, or quad)
63
+ 7. Symmetry Mirroring (bilateral-x, bilateral-y, or quad)
64
+ 8. Noise Texture Overlay
65
+ 9. Vignette (radial edge darkening)
66
+ 10. Organic Connecting Curves
67
+ 11. Post-Processing
61
68
  ├─ Color Grading (unified tone overlay)
62
69
  ├─ Chromatic Aberration (neon/cosmic/ethereal only)
63
70
  └─ Bloom (neon/cosmic only)
71
+ 12. Signature Mark (deterministic geometric chop mark)
64
72
  ```
65
73
 
66
74
  ## 1. Deterministic RNG
@@ -126,6 +134,18 @@ Each archetype controls:
126
134
 
127
135
  **celestial** — Boosts sacred geometry and cosmic shapes (crescent, geodesicDome, mandala, flowerOfLife, spirograph, fibonacciSpiral). Neon palette on dark background with heavy glow (2.5×) and deep layering (5 layers) creates a starfield-like composition with luminous geometric forms.
128
136
 
137
+ ### Archetype Blending
138
+
139
+ ~15% of hashes trigger **archetype blending**, where two archetypes are interpolated to create a hybrid personality. `blendArchetypes(primary, secondary, ratio)` works as follows:
140
+
141
+ - **Blend ratio:** 25–50% (the secondary archetype never dominates)
142
+ - **Numeric parameters** (`gridSize`, `layers`, `baseOpacity`, `minShapeSize`, etc.) are linearly interpolated between the two archetypes
143
+ - **Style arrays** (`preferredStyles`) are merged — the primary's styles come first, then any unique styles from the secondary
144
+ - **Categorical parameters** (`backgroundStyle`, `paletteMode`) use the primary's value when the blend ratio is below 50%, otherwise the secondary's
145
+ - **Boolean parameters** (`heroShape`) use the primary's value
146
+
147
+ This creates subtle hybrid personalities — e.g., a blend of `neon-glow` and `organic-flow` might produce glowing shapes with heavy flow lines and earth-toned neon outlines.
148
+
129
149
  ## 3. Color Scheme
130
150
 
131
151
  Color generation uses the `color-scheme` library seeded from the hash, then applies archetype-specific palette modes.
@@ -173,6 +193,16 @@ The scheme detects whether the background leans warm or cool, then shifts foregr
173
193
 
174
194
  Every shape color is checked against the background luminance. If the contrast ratio is too low, the color is lightened or darkened to ensure visibility.
175
195
 
196
+ ### Color Palette Evolution
197
+
198
+ Colors aren't static across layers — they evolve. `evolveHierarchy(hierarchy, layerFraction)` applies a progressive hue rotation to the entire color hierarchy as rendering moves through layers:
199
+
200
+ - **Total drift:** ±20° across the full layer stack (direction determined by the dominant color's initial hue)
201
+ - **Per-layer rotation:** `layerFraction × 20°` applied via `hueRotate(color, degrees)`, which converts to HSL, shifts the hue, and converts back
202
+ - **Effect:** Early layers lean toward the original palette; later layers drift toward adjacent hues on the color wheel
203
+
204
+ This creates a subtle color journey across the depth of the image — warm reds in the background might shift toward orange-gold in the foreground, giving the composition a sense of temporal progression without breaking palette coherence.
205
+
176
206
  ## 4. Shape Affinity System
177
207
 
178
208
  Not all shapes look equally good at all sizes or in all combinations. The affinity system replaces naive random shape selection with intentional curation.
@@ -297,6 +327,16 @@ Symmetry is applied by drawing the canvas image onto itself with `scale(-1, 1)`
297
327
 
298
328
  1–2 **void zones** are placed randomly. Shapes landing inside a void zone have an 85% chance of being skipped, creating breathing room in the composition.
299
329
 
330
+ ### Void Zone Decoration
331
+
332
+ Void zones aren't left completely empty — they receive subtle decorative elements that acknowledge the negative space:
333
+
334
+ - **Halo ring:** A thin circular stroke at the void zone's boundary using the accent color at 6–12% opacity
335
+ - **Scattered dots (~50% chance):** 5–12 tiny dots (1–3px) scattered randomly inside the void zone at 4–10% opacity, creating a dust-like texture
336
+ - **Inner concentric ring (~30% chance):** A smaller ring at 40–70% of the void zone radius at 3–8% opacity
337
+
338
+ These decorations are subtle enough to preserve the breathing room while preventing void zones from feeling like rendering errors.
339
+
300
340
  ## 8. Hero Shape
301
341
 
302
342
  When the archetype enables `heroShape` and the RNG roll passes (60% chance), a dominant focal element is drawn at the first focal point:
@@ -321,6 +361,16 @@ The core of the image: `layers` passes (archetype-controlled, typically 2–5),
321
361
  - **Atmospheric desaturation:** Later layers are progressively desaturated (up to 30%) to simulate depth
322
362
  - **Depth-of-field:** Later layers get thinner strokes (down to 40% of base width) and reduced contrast (up to 20% opacity reduction), simulating camera focus falloff
323
363
 
364
+ ### Focal Depth Boost
365
+
366
+ Shapes near focal points receive enhanced detail via `focalDetailBoost()`:
367
+
368
+ - **Nesting chance:** Boosted from the base 15% up to 15–30% for shapes within 25% of canvas size from a focal point
369
+ - **Constellation chance:** Boosted from the base 12% up to 12–22% near focal points
370
+ - **Boost strength:** Proportional to proximity — shapes right at the focal point get the full boost, shapes at the edge of the influence radius get minimal boost
371
+
372
+ This creates a natural depth-of-detail effect where the areas your eye is drawn to (focal points) contain the most intricate shape compositions.
373
+
324
374
  ### Per-Shape Pipeline
325
375
 
326
376
  For each shape in a layer:
@@ -379,7 +429,7 @@ Each member shape uses hierarchy colors with HSL jitter and affinity-aware rende
379
429
 
380
430
  ## 10. Render Styles
381
431
 
382
- Each shape is drawn using one of 14 render styles:
432
+ Each shape is drawn using one of 15 render styles:
383
433
 
384
434
  | Style | Description |
385
435
  | ----- | ----------- |
@@ -397,6 +447,7 @@ Each shape is drawn using one of 14 render styles:
397
447
  | wood-grain | 20% base tint + clipped parallel wavy lines at a random angle, simulating wood texture |
398
448
  | marble-vein | 35% soft base + clipped branching vein lines that drift and fork, simulating marble stone |
399
449
  | fabric-weave | 15% ghost base + clipped interlocking horizontal and vertical thread lines at alternating opacities |
450
+ | hand-drawn | Fill + 2–3 wobbly offset stroke passes simulating a hand-drawn sketch |
400
451
 
401
452
  ### Texture Fill Details
402
453
 
@@ -414,6 +465,38 @@ The 4 new texture fills (noise-grain, wood-grain, marble-vein, fabric-weave) all
414
465
 
415
466
  **fabric-weave** draws horizontal threads at full spacing, then vertical threads at half-spacing offsets, creating an over-under weave pattern. The two thread directions use different opacities (55% vs 45%) and colors (stroke vs fill) for visual distinction.
416
467
 
468
+ ### Hand-Drawn Style
469
+
470
+ The `hand-drawn` style simulates a sketchy, imprecise outline:
471
+
472
+ 1. **Base fill:** Standard fill of the shape
473
+ 2. **Wobbly strokes (2–3 passes):** Each pass redraws the shape's outline with a small random offset (1–3px) and slightly varied line width, creating the impression of a hand tracing the same line multiple times
474
+ 3. **Opacity variation:** Each pass uses 40–70% opacity so the overlapping strokes build up naturally
475
+
476
+ This style works particularly well on organic shapes (circle, triangle, blob) where the wobble feels intentional rather than broken.
477
+
478
+ ### Layered Transparency / Glazing
479
+
480
+ ~20% of shapes receive **glazing** — 2–3 additional fill-only passes drawn immediately after the main shape:
481
+
482
+ - Each pass is progressively smaller (85% → 72% → 61% of the original size)
483
+ - Each pass is progressively more opaque (adding 5–10% opacity per pass)
484
+ - Only the fill is redrawn (no stroke), creating a pigment-pooling effect where the center of the shape appears richer and more saturated
485
+
486
+ This simulates the traditional oil painting technique of building up translucent layers to create depth and luminosity.
487
+
488
+ ### Motion / Energy Lines
489
+
490
+ Short directional line bursts radiate from shapes to suggest movement and energy:
491
+
492
+ - **Trigger:** Always for `dense-chaotic`, `cosmic`, `neon-glow`, and `bold-graphic` archetypes; ~25% random chance for others
493
+ - **Count:** 3–6 lines per shape
494
+ - **Length:** 8–20% of the shape's size
495
+ - **Direction:** Radiating outward from the shape center at evenly-spaced angles with ±15° jitter
496
+ - **Style:** Thin lines (0.5–1.5px) using the shape's stroke color at 15–35% opacity, tapering toward the ends
497
+
498
+ Motion lines are drawn after the main shape layers but before symmetry mirroring, so they participate in any bilateral/quad reflections.
499
+
417
500
  ### Watercolor Detail
418
501
 
419
502
  The watercolor style simulates wet media through 4 passes:
@@ -461,3 +544,18 @@ Only for `neon-glow`, `cosmic`, and `ethereal` archetypes. The canvas is drawn o
461
544
  ### Bloom
462
545
 
463
546
  Only for `neon-glow` and `cosmic` archetypes. The canvas is redrawn with `shadowBlur` at 30px (scaled) and white shadow color, composited via `screen` at 8% opacity. This creates a soft glow around bright areas.
547
+
548
+ ## 13. Signature Mark
549
+
550
+ The final rendering step places a small deterministic **chop mark** (inspired by East Asian seal stamps) in the bottom-right corner of the canvas:
551
+
552
+ - **RNG isolation:** Uses a separate RNG seeded with a salt of 42, so the signature is independent of all other rendering decisions
553
+ - **Position:** Bottom-right corner with a small margin (3% of canvas size)
554
+ - **Size:** 1.5–2.5% of the shorter canvas dimension
555
+ - **Structure:**
556
+ 1. Outer circle ring (stroke-only) at 12–20% opacity
557
+ 2. 2–4 inner lines crossing the circle at unique angles derived from the hash, creating a distinctive geometric pattern
558
+ 3. Center dot at slightly higher opacity
559
+ - **Color:** Uses the accent color from the hierarchy at very low opacity so it's visible but never distracting
560
+
561
+ The signature is unique per hash (different inner line patterns) but always recognizable as a chop mark, giving each generated image a subtle artist's stamp.
package/CHANGELOG.md CHANGED
@@ -4,12 +4,20 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [0.10.0](https://github.com/gfargo/git-hash-art/compare/0.9.0...0.10.0)
8
+
9
+ - feat: color palette evolution, edge treatment, glazing, motion lines, archetype blending, focal depth, signature mark [`#17`](https://github.com/gfargo/git-hash-art/pull/17)
10
+ - feat: color palette evolution, edge treatment, negative space, glazing, motion lines, archetype blending, focal depth, signature mark [`b496771`](https://github.com/gfargo/git-hash-art/commit/b49677108ae170fef9c1108d40d7db6995efb48c)
11
+
7
12
  #### [0.9.0](https://github.com/gfargo/git-hash-art/compare/0.8.0...0.9.0)
8
13
 
14
+ > 19 March 2026
15
+
9
16
  - feat: new archetypes, depth-of-field, texture fills, constellations, background patterns [`#16`](https://github.com/gfargo/git-hash-art/pull/16)
10
17
  - docs: update ALGORITHM.md with visual quality improvements [`#15`](https://github.com/gfargo/git-hash-art/pull/15)
11
18
  - feat: add 4 archetypes, depth-of-field, texture fills, constellations, background patterns [`7f9b0fc`](https://github.com/gfargo/git-hash-art/commit/7f9b0fcbc4a17272dd6a62371e037fb0555c830b)
12
19
  - add version comparison script [`9301176`](https://github.com/gfargo/git-hash-art/commit/9301176b4f1fe0af85d5d7f85e58db9be10ec382)
20
+ - chore: release v0.9.0 [`32311c2`](https://github.com/gfargo/git-hash-art/commit/32311c2fda12dbcb388e77d9e2faba380d3b0c29)
13
21
 
14
22
  #### [0.8.0](https://github.com/gfargo/git-hash-art/compare/0.7.0...0.8.0)
15
23
 
package/dist/browser.js CHANGED
@@ -518,6 +518,18 @@ function $b5a262d09b87e373$export$6d1620b367f86f7a(rng) {
518
518
  intensity: intensity
519
519
  };
520
520
  }
521
+ function $b5a262d09b87e373$export$1793a1bfbe4f6ff5(hex, degrees) {
522
+ const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
523
+ return $b5a262d09b87e373$var$hslToHex((h + degrees + 360) % 360, s, l);
524
+ }
525
+ function $b5a262d09b87e373$export$703ba40a4347f77a(base, layerRatio, hueShiftPerLayer) {
526
+ const shift = layerRatio * hueShiftPerLayer;
527
+ return {
528
+ dominant: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.dominant, shift),
529
+ secondary: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
530
+ accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
531
+ };
532
+ }
521
533
 
522
534
 
523
535
 
@@ -1801,7 +1813,8 @@ const $e0f99502ff383dd8$var$RENDER_STYLES = [
1801
1813
  "noise-grain",
1802
1814
  "wood-grain",
1803
1815
  "marble-vein",
1804
- "fabric-weave"
1816
+ "fabric-weave",
1817
+ "hand-drawn"
1805
1818
  ];
1806
1819
  function $e0f99502ff383dd8$export$9fd4e64b2acd410e(rng) {
1807
1820
  return $e0f99502ff383dd8$var$RENDER_STYLES[Math.floor(rng() * $e0f99502ff383dd8$var$RENDER_STYLES.length)];
@@ -2182,6 +2195,33 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
2182
2195
  ctx.globalAlpha /= 0.3;
2183
2196
  break;
2184
2197
  }
2198
+ case "hand-drawn":
2199
+ {
2200
+ // Wobbly hand-drawn edge treatment — fill normally, then redraw
2201
+ // the outline with perturbed control points for a sketchy feel
2202
+ const savedAlphaHD = ctx.globalAlpha;
2203
+ ctx.globalAlpha = savedAlphaHD * 0.85;
2204
+ ctx.fill();
2205
+ ctx.globalAlpha = savedAlphaHD;
2206
+ // Draw 2-3 slightly offset wobbly strokes for a sketchy look
2207
+ const wobblePasses = 2 + (rng ? Math.floor(rng() * 2) : 0);
2208
+ ctx.lineWidth = strokeWidth * 0.8;
2209
+ for(let wp = 0; wp < wobblePasses; wp++){
2210
+ ctx.globalAlpha = savedAlphaHD * (0.4 - wp * 0.1);
2211
+ ctx.save();
2212
+ // Slight random offset per pass
2213
+ const wobbleX = rng ? (rng() - 0.5) * size * 0.02 : 0;
2214
+ const wobbleY = rng ? (rng() - 0.5) * size * 0.02 : 0;
2215
+ ctx.translate(wobbleX, wobbleY);
2216
+ // Slightly different scale per pass for edge variation
2217
+ const wobbleScale = 1 + (rng ? (rng() - 0.5) * 0.03 : 0);
2218
+ ctx.scale(wobbleScale, wobbleScale);
2219
+ ctx.stroke();
2220
+ ctx.restore();
2221
+ }
2222
+ ctx.globalAlpha = savedAlphaHD;
2223
+ break;
2224
+ }
2185
2225
  case "fill-and-stroke":
2186
2226
  default:
2187
2227
  ctx.fill();
@@ -2317,7 +2357,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2317
2357
  bestStyles: [
2318
2358
  "fill-only",
2319
2359
  "watercolor",
2320
- "fill-and-stroke"
2360
+ "fill-and-stroke",
2361
+ "hand-drawn"
2321
2362
  ]
2322
2363
  },
2323
2364
  square: {
@@ -2354,7 +2395,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2354
2395
  bestStyles: [
2355
2396
  "fill-and-stroke",
2356
2397
  "fill-only",
2357
- "watercolor"
2398
+ "watercolor",
2399
+ "hand-drawn"
2358
2400
  ]
2359
2401
  },
2360
2402
  hexagon: {
@@ -2734,7 +2776,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2734
2776
  bestStyles: [
2735
2777
  "fill-only",
2736
2778
  "watercolor",
2737
- "fill-and-stroke"
2779
+ "fill-and-stroke",
2780
+ "hand-drawn"
2738
2781
  ]
2739
2782
  },
2740
2783
  ngon: {
@@ -3614,8 +3657,51 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
3614
3657
  invertForeground: false
3615
3658
  }
3616
3659
  ];
3660
+ /**
3661
+ * Linearly interpolate between two archetype numeric parameters.
3662
+ */ function $68a238ccd77f2bcd$var$lerpNum(a, b, t) {
3663
+ return a + (b - a) * t;
3664
+ }
3665
+ /**
3666
+ * Blend two archetypes by interpolating their numeric parameters
3667
+ * and merging their style arrays.
3668
+ */ function $68a238ccd77f2bcd$var$blendArchetypes(a, b, t) {
3669
+ // Merge preferred styles — unique union, primary archetype first
3670
+ const mergedStyles = [
3671
+ ...new Set([
3672
+ ...a.preferredStyles,
3673
+ ...b.preferredStyles
3674
+ ])
3675
+ ];
3676
+ return {
3677
+ name: `${a.name}+${b.name}`,
3678
+ gridSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.gridSize, b.gridSize, t)),
3679
+ layers: Math.round($68a238ccd77f2bcd$var$lerpNum(a.layers, b.layers, t)),
3680
+ baseOpacity: $68a238ccd77f2bcd$var$lerpNum(a.baseOpacity, b.baseOpacity, t),
3681
+ opacityReduction: $68a238ccd77f2bcd$var$lerpNum(a.opacityReduction, b.opacityReduction, t),
3682
+ minShapeSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.minShapeSize, b.minShapeSize, t)),
3683
+ maxShapeSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.maxShapeSize, b.maxShapeSize, t)),
3684
+ backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3685
+ paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3686
+ preferredStyles: mergedStyles,
3687
+ flowLineMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3688
+ heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3689
+ glowMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
3690
+ sizePower: $68a238ccd77f2bcd$var$lerpNum(a.sizePower, b.sizePower, t),
3691
+ invertForeground: t < 0.5 ? a.invertForeground : b.invertForeground
3692
+ };
3693
+ }
3617
3694
  function $68a238ccd77f2bcd$export$f1142fd7da4d6590(rng) {
3618
- return $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3695
+ const primary = $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3696
+ // ~15% chance of blending with a second archetype
3697
+ if (rng() < 0.15) {
3698
+ const secondary = $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3699
+ if (secondary.name !== primary.name) {
3700
+ const blendT = 0.25 + rng() * 0.25; // 25-50% blend toward secondary
3701
+ return $68a238ccd77f2bcd$var$blendArchetypes(primary, secondary, blendT);
3702
+ }
3703
+ }
3704
+ return primary;
3619
3705
  }
3620
3706
 
3621
3707
 
@@ -3951,6 +4037,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3951
4037
  const colorGrade = (0, $b5a262d09b87e373$export$6d1620b367f86f7a)(rng);
3952
4038
  // ── 0e. Light direction — consistent shadow angle ──────────────
3953
4039
  const lightAngle = rng() * Math.PI * 2;
4040
+ // ── 0f. Palette evolution — hue drift direction across layers ──
4041
+ const paletteHueShift = (rng() - 0.5) * 40; // -20° to +20° total drift
3954
4042
  const scaleFactor = Math.min(width, height) / 1024;
3955
4043
  const adjustedMinSize = minShapeSize * scaleFactor;
3956
4044
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -4125,6 +4213,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4125
4213
  ry + (nearest.y - ry) * pull
4126
4214
  ];
4127
4215
  }
4216
+ // ── 3b. Void zone decoration — intentional negative space ────
4217
+ for (const zone of voidZones){
4218
+ // Subtle halo ring around void zones
4219
+ ctx.globalAlpha = 0.04 + rng() * 0.04;
4220
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
4221
+ ctx.lineWidth = 1.5 * scaleFactor;
4222
+ ctx.beginPath();
4223
+ ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
4224
+ ctx.stroke();
4225
+ // ~50% chance: scatter tiny dots inside the void
4226
+ if (rng() < 0.5) {
4227
+ const dotCount = 3 + Math.floor(rng() * 6);
4228
+ ctx.globalAlpha = 0.06 + rng() * 0.04;
4229
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
4230
+ for(let d = 0; d < dotCount; d++){
4231
+ const angle = rng() * Math.PI * 2;
4232
+ const dist = rng() * zone.radius * 0.7;
4233
+ const dotR = (1 + rng() * 3) * scaleFactor;
4234
+ ctx.beginPath();
4235
+ ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
4236
+ ctx.fill();
4237
+ }
4238
+ }
4239
+ // ~30% chance: thin concentric ring inside
4240
+ if (rng() < 0.3) {
4241
+ ctx.globalAlpha = 0.03 + rng() * 0.03;
4242
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4243
+ ctx.lineWidth = 0.5 * scaleFactor;
4244
+ const innerR = zone.radius * (0.4 + rng() * 0.3);
4245
+ ctx.beginPath();
4246
+ ctx.arc(zone.x, zone.y, innerR, 0, Math.PI * 2);
4247
+ ctx.stroke();
4248
+ }
4249
+ }
4250
+ ctx.globalAlpha = 1;
4128
4251
  // ── 4. Flow field seed values ──────────────────────────────────
4129
4252
  const fieldAngleBase = rng() * Math.PI * 2;
4130
4253
  const fieldFreq = 0.5 + rng() * 2;
@@ -4197,6 +4320,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4197
4320
  const dofFactor = 1 - layerRatio * 0.5; // 1.0 for front layer, 0.5 for back
4198
4321
  const dofStrokeScale = 0.4 + dofFactor * 0.6; // strokes thin out with depth
4199
4322
  const dofContrastReduction = layerRatio * 0.2; // colors fade toward bg
4323
+ // Color palette evolution — hue-rotate the hierarchy per layer
4324
+ const layerHierarchy = (0, $b5a262d09b87e373$export$703ba40a4347f77a)(colorHierarchy, layerRatio, paletteHueShift);
4325
+ // Focal depth: shapes near focal points get more detail
4326
+ const focalDetailBoost = (px, py)=>{
4327
+ let minFocalDist = Infinity;
4328
+ for (const fp of focalPoints){
4329
+ const d = Math.hypot(px - fp.x, py - fp.y);
4330
+ if (d < minFocalDist) minFocalDist = d;
4331
+ }
4332
+ const maxDist = Math.hypot(width, height) * 0.5;
4333
+ return Math.max(0, 1 - minFocalDist / maxDist); // 1.0 at focal, 0.0 at edges
4334
+ };
4200
4335
  for(let i = 0; i < numShapes; i++){
4201
4336
  // Position from composition mode, then focal bias
4202
4337
  const rawPos = $1f63dc64b5593c73$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
@@ -4227,9 +4362,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4227
4362
  rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
4228
4363
  }
4229
4364
  }
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);
4365
+ // Positional color from hierarchy + jitter (using evolved layer palette)
4366
+ let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, layerHierarchy, rng);
4367
+ const strokeBase = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng);
4233
4368
  // Desaturate colors on later layers for depth
4234
4369
  if (atmosphericDesat > 0) fillBase = (0, $b5a262d09b87e373$export$fb75607d98509d9)(fillBase, atmosphericDesat);
4235
4370
  // Temperature contrast: shift foreground shapes opposite to background
@@ -4321,6 +4456,25 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4321
4456
  mirrorGap: size * (0.1 + rng() * 0.3)
4322
4457
  });
4323
4458
  else (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
4459
+ // ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
4460
+ if (rng() < 0.2 && size > adjustedMinSize * 2) {
4461
+ const glazePasses = 2 + Math.floor(rng() * 2);
4462
+ for(let g = 0; g < glazePasses; g++){
4463
+ const glazeScale = 1 - (g + 1) * 0.12; // progressively smaller
4464
+ const glazeAlpha = 0.08 + g * 0.04; // progressively more opaque toward center
4465
+ ctx.globalAlpha = glazeAlpha;
4466
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
4467
+ fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
4468
+ strokeColor: "rgba(0,0,0,0)",
4469
+ strokeWidth: 0,
4470
+ size: size * glazeScale,
4471
+ rotation: rotation,
4472
+ proportionType: "GOLDEN_RATIO",
4473
+ renderStyle: "fill-only",
4474
+ rng: rng
4475
+ });
4476
+ }
4477
+ }
4324
4478
  shapePositions.push({
4325
4479
  x: finalX,
4326
4480
  y: finalY,
@@ -4358,7 +4512,10 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4358
4512
  }
4359
4513
  }
4360
4514
  // ── 5d. Recursive nesting ──────────────────────────────────
4361
- if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
4515
+ // Focal depth: shapes near focal points get more detail
4516
+ const focalProximity = focalDetailBoost(finalX, finalY);
4517
+ const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
4518
+ if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
4362
4519
  const innerCount = 1 + Math.floor(rng() * 3);
4363
4520
  for(let n = 0; n < innerCount; n++){
4364
4521
  // Pick inner shape from palette affinities
@@ -4383,7 +4540,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4383
4540
  }
4384
4541
  }
4385
4542
  // ── 5e. Shape constellations — pre-composed groups ─────────
4386
- if (size > adjustedMaxSize * 0.35 && rng() < 0.12) {
4543
+ const constellationChance = 0.12 + focalProximity * 0.1; // 12-22% near focal
4544
+ if (size > adjustedMaxSize * 0.35 && rng() < constellationChance) {
4387
4545
  const constellation = $1f63dc64b5593c73$var$CONSTELLATIONS[Math.floor(rng() * $1f63dc64b5593c73$var$CONSTELLATIONS.length)];
4388
4546
  const members = constellation.build(rng, size);
4389
4547
  const groupRotation = rng() * Math.PI * 2;
@@ -4487,7 +4645,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4487
4645
  prevY = fy;
4488
4646
  }
4489
4647
  }
4490
- // ── 6b. Apply symmetry mirroring ─────────────────────────────────
4648
+ // ── 6b. Motion/energy lines short directional bursts ─────────
4649
+ const energyArchetypes = [
4650
+ "dense-chaotic",
4651
+ "cosmic",
4652
+ "neon-glow",
4653
+ "bold-graphic"
4654
+ ];
4655
+ const hasEnergyLines = energyArchetypes.some((a)=>archetype.name.includes(a)) || rng() < 0.25;
4656
+ if (hasEnergyLines && shapePositions.length > 0) {
4657
+ const energyCount = 5 + Math.floor(rng() * 10);
4658
+ ctx.lineCap = "round";
4659
+ for(let e = 0; e < energyCount; e++){
4660
+ // Pick a random shape to radiate from
4661
+ const source = shapePositions[Math.floor(rng() * shapePositions.length)];
4662
+ const burstCount = 2 + Math.floor(rng() * 4);
4663
+ const baseAngle = flowAngle(source.x, source.y);
4664
+ for(let b = 0; b < burstCount; b++){
4665
+ const angle = baseAngle + (rng() - 0.5) * 1.2;
4666
+ const lineLen = (source.size * 0.3 + rng() * source.size * 0.5) * scaleFactor * 0.3;
4667
+ const startDist = source.size * 0.5;
4668
+ const sx = source.x + Math.cos(angle) * startDist;
4669
+ const sy = source.y + Math.sin(angle) * startDist;
4670
+ const ex = sx + Math.cos(angle) * lineLen;
4671
+ const ey = sy + Math.sin(angle) * lineLen;
4672
+ ctx.globalAlpha = 0.04 + rng() * 0.06;
4673
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
4674
+ ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
4675
+ ctx.beginPath();
4676
+ ctx.moveTo(sx, sy);
4677
+ ctx.lineTo(ex, ey);
4678
+ ctx.stroke();
4679
+ }
4680
+ }
4681
+ }
4682
+ // ── 6c. Apply symmetry mirroring ─────────────────────────────────
4491
4683
  if (symmetryMode !== "none") {
4492
4684
  const canvas = ctx.canvas;
4493
4685
  ctx.save();
@@ -4598,6 +4790,44 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4598
4790
  ctx.restore();
4599
4791
  ctx.globalCompositeOperation = "source-over";
4600
4792
  }
4793
+ // ── 11. Signature mark — unique geometric chop from hash prefix ──
4794
+ {
4795
+ const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
4796
+ const sigSize = Math.min(width, height) * 0.025;
4797
+ // Bottom-right corner with padding
4798
+ const sigX = width - sigSize * 2.5;
4799
+ const sigY = height - sigSize * 2.5;
4800
+ const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
4801
+ const sigColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
4802
+ ctx.save();
4803
+ ctx.globalAlpha = 0.12 + sigRng() * 0.08;
4804
+ ctx.translate(sigX, sigY);
4805
+ ctx.strokeStyle = sigColor;
4806
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.06);
4807
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
4808
+ // Outer ring
4809
+ ctx.beginPath();
4810
+ ctx.arc(0, 0, sigSize, 0, Math.PI * 2);
4811
+ ctx.stroke();
4812
+ ctx.fill();
4813
+ // Inner geometric pattern — unique per hash
4814
+ ctx.beginPath();
4815
+ for(let s = 0; s < sigSegments; s++){
4816
+ const angle1 = sigRng() * Math.PI * 2;
4817
+ const angle2 = sigRng() * Math.PI * 2;
4818
+ const r1 = sigSize * (0.2 + sigRng() * 0.6);
4819
+ const r2 = sigSize * (0.2 + sigRng() * 0.6);
4820
+ ctx.moveTo(Math.cos(angle1) * r1, Math.sin(angle1) * r1);
4821
+ ctx.lineTo(Math.cos(angle2) * r2, Math.sin(angle2) * r2);
4822
+ }
4823
+ ctx.stroke();
4824
+ // Center dot
4825
+ ctx.beginPath();
4826
+ ctx.arc(0, 0, sigSize * 0.12, 0, Math.PI * 2);
4827
+ ctx.fillStyle = sigColor;
4828
+ ctx.fill();
4829
+ ctx.restore();
4830
+ }
4601
4831
  ctx.globalAlpha = 1;
4602
4832
  }
4603
4833