git-hash-art 0.7.0 → 0.8.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/dist/main.js CHANGED
@@ -27,17 +27,22 @@ $parcel$export(module.exports, "DEFAULT_CONFIG", () => $93cf69256c93baa9$export$
27
27
  * identically in Node (@napi-rs/canvas) and browsers.
28
28
  *
29
29
  * Generation pipeline:
30
- * 1. Background radial gradient from hash-derived dark palette
31
- * 1b. Layered background large faint shapes / subtle pattern for depth
32
- * 2. Composition modehash selects: radial, flow-field, spiral, grid-subdivision, or clustered
33
- * 3. Focal points + void zones (negative space)
34
- * 4. Flow field seed values
35
- * 5. Shape layers — blend modes, render styles, weighted selection,
36
- * focal-point placement, atmospheric depth, organic edges
30
+ * 0. Archetype selection + shape palette + color hierarchy
31
+ * 1. Backgroundstyle from archetype, gradient mesh for depth
32
+ * 1b. Layered background archetype-coherent shapes
33
+ * 2. Composition mode + symmetry
34
+ * 3. Focal points + void zones + hero avoidance field
35
+ * 4. Flow field
36
+ * 4b. Hero shape
37
+ * 5. Shape layers — palette-driven selection, affinity-aware styles,
38
+ * size echo, tangent placement, atmospheric depth
37
39
  * 5b. Recursive nesting
38
- * 6. Flow-line passtapered brush-stroke curves
39
- * 7. Noise texture overlay
40
- * 8. Organic connecting curves
40
+ * 6. Flow linesvariable color, branching, pressure simulation
41
+ * 6b. Symmetry mirroring
42
+ * 7. Noise texture
43
+ * 8. Vignette
44
+ * 9. Organic connecting curves
45
+ * 10. Post-processing — color grading, chromatic aberration, bloom
41
46
  */
42
47
  // declare module 'color-scheme';
43
48
 
@@ -411,6 +416,67 @@ function $d016ad53434219a1$export$f2121afcad3d553f(hex, alpha) {
411
416
  const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex);
412
417
  return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
413
418
  }
419
+ function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
420
+ if (colors.length < 3) return {
421
+ dominant: colors[0] || "#888888",
422
+ secondary: colors[1] || colors[0] || "#888888",
423
+ accent: colors[colors.length - 1] || "#888888",
424
+ all: colors
425
+ };
426
+ // Pick dominant as the color closest to the palette's average hue
427
+ const hsls = colors.map((c)=>$d016ad53434219a1$var$hexToHsl(c));
428
+ const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
429
+ let dominantIdx = 0;
430
+ let minDist = 360;
431
+ for(let i = 0; i < hsls.length; i++){
432
+ const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
433
+ if (d < minDist) {
434
+ minDist = d;
435
+ dominantIdx = i;
436
+ }
437
+ }
438
+ // Accent is the color most distant from dominant in hue
439
+ let accentIdx = 0;
440
+ let maxDist = 0;
441
+ for(let i = 0; i < hsls.length; i++){
442
+ if (i === dominantIdx) continue;
443
+ const d = Math.min(Math.abs(hsls[i][0] - hsls[dominantIdx][0]), 360 - Math.abs(hsls[i][0] - hsls[dominantIdx][0]));
444
+ if (d > maxDist) {
445
+ maxDist = d;
446
+ accentIdx = i;
447
+ }
448
+ }
449
+ // Secondary is the remaining color with highest saturation
450
+ let secondaryIdx = 0;
451
+ let maxSat = -1;
452
+ for(let i = 0; i < hsls.length; i++){
453
+ if (i === dominantIdx || i === accentIdx) continue;
454
+ if (hsls[i][1] > maxSat) {
455
+ maxSat = hsls[i][1];
456
+ secondaryIdx = i;
457
+ }
458
+ }
459
+ if (secondaryIdx === dominantIdx) secondaryIdx = accentIdx === 0 ? 1 : 0;
460
+ return {
461
+ dominant: colors[dominantIdx],
462
+ secondary: colors[secondaryIdx],
463
+ accent: colors[accentIdx],
464
+ all: colors
465
+ };
466
+ }
467
+ function $d016ad53434219a1$export$b49f62f0a99da0e8(hierarchy, rng) {
468
+ const roll = rng();
469
+ if (roll < 0.60) return hierarchy.dominant;
470
+ if (roll < 0.85) return hierarchy.secondary;
471
+ return hierarchy.accent;
472
+ }
473
+ function $d016ad53434219a1$export$18a34c25ea7e724b(hex, rng, hueAmount = 8, slAmount = 0.06) {
474
+ const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
475
+ const newH = (h + (rng() - 0.5) * hueAmount * 2 + 360) % 360;
476
+ const newS = Math.max(0, Math.min(1, s + (rng() - 0.5) * slAmount * 2));
477
+ const newL = Math.max(0, Math.min(1, l + (rng() - 0.5) * slAmount * 2));
478
+ return $d016ad53434219a1$var$hslToHex(newH, newS, newL);
479
+ }
414
480
  function $d016ad53434219a1$export$59539d800dbe6858(hex, rng, amount = 0.1) {
415
481
  const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex);
416
482
  const jit = ()=>(rng() - 0.5) * 2 * amount * 255;
@@ -450,6 +516,31 @@ function $d016ad53434219a1$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContra
450
516
  return $d016ad53434219a1$var$hslToHex(h, targetS, targetL);
451
517
  }
452
518
  }
519
+ function $d016ad53434219a1$export$4a3734b8c4b5c0e(hex, gradeHue, intensity) {
520
+ const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
521
+ // Blend hue toward the grade hue
522
+ const hueDiff = (gradeHue - h + 540) % 360 - 180;
523
+ const newH = (h + hueDiff * intensity * 0.3 + 360) % 360;
524
+ // Slightly unify saturation
525
+ const newS = Math.max(0, Math.min(1, s + (0.5 - s) * intensity * 0.15));
526
+ return $d016ad53434219a1$var$hslToHex(newH, newS, l);
527
+ }
528
+ function $d016ad53434219a1$export$6d1620b367f86f7a(rng) {
529
+ // Warm golden, cool blue, rosy, teal, amber
530
+ const GRADE_HUES = [
531
+ 40,
532
+ 220,
533
+ 340,
534
+ 175,
535
+ 30
536
+ ];
537
+ const hue = GRADE_HUES[Math.floor(rng() * GRADE_HUES.length)] + (rng() - 0.5) * 20;
538
+ const intensity = 0.15 + rng() * 0.25;
539
+ return {
540
+ hue: (hue + 360) % 360,
541
+ intensity: intensity
542
+ };
543
+ }
453
544
 
454
545
 
455
546
 
@@ -1480,23 +1571,50 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
1480
1571
  break;
1481
1572
  case "watercolor":
1482
1573
  {
1483
- // Draw 3-4 slightly offset passes at low opacity for a bleed effect
1484
- const passes = 3 + (rng ? Math.floor(rng() * 2) : 0);
1574
+ // Improved watercolor: edge darkening + radial bleed + layered washes
1575
+ const passes = 4 + (rng ? Math.floor(rng() * 2) : 0);
1485
1576
  const savedAlpha = ctx.globalAlpha;
1486
- ctx.globalAlpha = savedAlpha * (0.3 / passes * 2);
1577
+ // Pass 1: Base wash large, soft fill at low opacity
1578
+ ctx.globalAlpha = savedAlpha * 0.15;
1579
+ ctx.save();
1580
+ const baseScale = 1.08 + (rng ? rng() * 0.04 : 0);
1581
+ ctx.scale(baseScale, baseScale);
1582
+ ctx.fill();
1583
+ ctx.restore();
1584
+ // Pass 2: Multiple offset washes with radial displacement
1585
+ ctx.globalAlpha = savedAlpha * (0.25 / passes * 2);
1487
1586
  for(let p = 0; p < passes; p++){
1488
- const jx = rng ? (rng() - 0.5) * size * 0.06 : 0;
1489
- const jy = rng ? (rng() - 0.5) * size * 0.06 : 0;
1587
+ // Radial outward displacement (not uniform) for organic bleed
1588
+ const angle = rng ? rng() * Math.PI * 2 : p * Math.PI / 2;
1589
+ const dist = rng ? rng() * size * 0.05 : size * 0.02;
1590
+ const jx = Math.cos(angle) * dist;
1591
+ const jy = Math.sin(angle) * dist;
1490
1592
  ctx.save();
1491
1593
  ctx.translate(jx, jy);
1492
1594
  ctx.fill();
1493
1595
  ctx.restore();
1494
1596
  }
1597
+ // Pass 3: Edge darkening — draw a slightly smaller shape with lighter fill
1598
+ // to simulate pigment pooling at boundaries
1599
+ ctx.globalAlpha = savedAlpha * 0.35;
1600
+ ctx.save();
1601
+ const innerScale = 0.85 + (rng ? rng() * 0.08 : 0);
1602
+ ctx.scale(innerScale, innerScale);
1603
+ // Lighten the fill for the inner area
1604
+ const origFill = ctx.fillStyle;
1605
+ if (typeof fillColor === "string") ctx.fillStyle = fillColor.replace(/[\d.]+\)$/, (m)=>{
1606
+ const v = parseFloat(m);
1607
+ return Math.min(1, v * 1.4).toFixed(2) + ")";
1608
+ });
1609
+ ctx.fill();
1610
+ ctx.fillStyle = origFill;
1611
+ ctx.restore();
1495
1612
  ctx.globalAlpha = savedAlpha;
1496
- // Light stroke on top
1497
- ctx.globalAlpha *= 0.4;
1613
+ // Soft stroke on top — thinner than normal for delicacy
1614
+ ctx.globalAlpha *= 0.25;
1615
+ ctx.lineWidth = strokeWidth * 0.6;
1498
1616
  ctx.stroke();
1499
- ctx.globalAlpha /= 0.4;
1617
+ ctx.globalAlpha /= 0.25;
1500
1618
  break;
1501
1619
  }
1502
1620
  case "hatched":
@@ -1618,6 +1736,675 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1618
1736
 
1619
1737
 
1620
1738
 
1739
+ /**
1740
+ * Shape affinity system — controls which shapes look good together,
1741
+ * quality tiers for different rendering contexts, and size preferences.
1742
+ *
1743
+ * This replaces the naive "pick any shape" approach with intentional
1744
+ * curation that produces more cohesive compositions.
1745
+ */ // ── Quality tiers ───────────────────────────────────────────────────
1746
+ // Not all shapes render equally well at all sizes or in all contexts.
1747
+ // Tier 1 shapes are visually strong at any size; Tier 3 shapes need
1748
+ // specific conditions to look good.
1749
+ const $e73976f898150d4d$export$4343b39fe47bd82c = {
1750
+ // ── Basic shapes ──────────────────────────────────────────────
1751
+ circle: {
1752
+ tier: 1,
1753
+ minSizeFraction: 0.05,
1754
+ maxSizeFraction: 1.0,
1755
+ affinities: [
1756
+ "circle",
1757
+ "blob",
1758
+ "hexagon",
1759
+ "flowerOfLife",
1760
+ "seedOfLife"
1761
+ ],
1762
+ category: "basic",
1763
+ heroCandidate: false,
1764
+ bestStyles: [
1765
+ "fill-only",
1766
+ "watercolor",
1767
+ "fill-and-stroke"
1768
+ ]
1769
+ },
1770
+ square: {
1771
+ tier: 2,
1772
+ minSizeFraction: 0.08,
1773
+ maxSizeFraction: 0.7,
1774
+ affinities: [
1775
+ "square",
1776
+ "diamond",
1777
+ "superellipse",
1778
+ "islamicPattern"
1779
+ ],
1780
+ category: "basic",
1781
+ heroCandidate: false,
1782
+ bestStyles: [
1783
+ "fill-and-stroke",
1784
+ "stroke-only",
1785
+ "hatched"
1786
+ ]
1787
+ },
1788
+ triangle: {
1789
+ tier: 1,
1790
+ minSizeFraction: 0.06,
1791
+ maxSizeFraction: 0.9,
1792
+ affinities: [
1793
+ "triangle",
1794
+ "diamond",
1795
+ "hexagon",
1796
+ "merkaba",
1797
+ "sriYantra"
1798
+ ],
1799
+ category: "basic",
1800
+ heroCandidate: false,
1801
+ bestStyles: [
1802
+ "fill-and-stroke",
1803
+ "fill-only",
1804
+ "watercolor"
1805
+ ]
1806
+ },
1807
+ hexagon: {
1808
+ tier: 1,
1809
+ minSizeFraction: 0.05,
1810
+ maxSizeFraction: 1.0,
1811
+ affinities: [
1812
+ "hexagon",
1813
+ "circle",
1814
+ "flowerOfLife",
1815
+ "metatronsCube",
1816
+ "triangle"
1817
+ ],
1818
+ category: "basic",
1819
+ heroCandidate: false,
1820
+ bestStyles: [
1821
+ "fill-only",
1822
+ "fill-and-stroke",
1823
+ "watercolor"
1824
+ ]
1825
+ },
1826
+ star: {
1827
+ tier: 2,
1828
+ minSizeFraction: 0.08,
1829
+ maxSizeFraction: 0.6,
1830
+ affinities: [
1831
+ "star",
1832
+ "circle",
1833
+ "mandala",
1834
+ "spirograph"
1835
+ ],
1836
+ category: "basic",
1837
+ heroCandidate: false,
1838
+ bestStyles: [
1839
+ "fill-and-stroke",
1840
+ "stroke-only",
1841
+ "dashed"
1842
+ ]
1843
+ },
1844
+ "jacked-star": {
1845
+ tier: 3,
1846
+ minSizeFraction: 0.1,
1847
+ maxSizeFraction: 0.4,
1848
+ affinities: [
1849
+ "star",
1850
+ "circle"
1851
+ ],
1852
+ category: "basic",
1853
+ heroCandidate: false,
1854
+ bestStyles: [
1855
+ "stroke-only",
1856
+ "dashed"
1857
+ ]
1858
+ },
1859
+ heart: {
1860
+ tier: 3,
1861
+ minSizeFraction: 0.1,
1862
+ maxSizeFraction: 0.5,
1863
+ affinities: [
1864
+ "circle",
1865
+ "blob"
1866
+ ],
1867
+ category: "basic",
1868
+ heroCandidate: false,
1869
+ bestStyles: [
1870
+ "fill-only",
1871
+ "watercolor"
1872
+ ]
1873
+ },
1874
+ diamond: {
1875
+ tier: 2,
1876
+ minSizeFraction: 0.06,
1877
+ maxSizeFraction: 0.8,
1878
+ affinities: [
1879
+ "diamond",
1880
+ "triangle",
1881
+ "square",
1882
+ "merkaba"
1883
+ ],
1884
+ category: "basic",
1885
+ heroCandidate: false,
1886
+ bestStyles: [
1887
+ "fill-and-stroke",
1888
+ "fill-only",
1889
+ "double-stroke"
1890
+ ]
1891
+ },
1892
+ cube: {
1893
+ tier: 3,
1894
+ minSizeFraction: 0.08,
1895
+ maxSizeFraction: 0.5,
1896
+ affinities: [
1897
+ "square",
1898
+ "diamond"
1899
+ ],
1900
+ category: "basic",
1901
+ heroCandidate: false,
1902
+ bestStyles: [
1903
+ "stroke-only",
1904
+ "fill-and-stroke"
1905
+ ]
1906
+ },
1907
+ // ── Complex shapes ────────────────────────────────────────────
1908
+ platonicSolid: {
1909
+ tier: 2,
1910
+ minSizeFraction: 0.15,
1911
+ maxSizeFraction: 0.8,
1912
+ affinities: [
1913
+ "metatronsCube",
1914
+ "merkaba",
1915
+ "hexagon",
1916
+ "triangle"
1917
+ ],
1918
+ category: "complex",
1919
+ heroCandidate: true,
1920
+ bestStyles: [
1921
+ "stroke-only",
1922
+ "double-stroke",
1923
+ "dashed"
1924
+ ]
1925
+ },
1926
+ fibonacciSpiral: {
1927
+ tier: 1,
1928
+ minSizeFraction: 0.2,
1929
+ maxSizeFraction: 1.0,
1930
+ affinities: [
1931
+ "circle",
1932
+ "rose",
1933
+ "spirograph",
1934
+ "flowerOfLife"
1935
+ ],
1936
+ category: "complex",
1937
+ heroCandidate: true,
1938
+ bestStyles: [
1939
+ "stroke-only",
1940
+ "incomplete",
1941
+ "watercolor"
1942
+ ]
1943
+ },
1944
+ islamicPattern: {
1945
+ tier: 2,
1946
+ minSizeFraction: 0.25,
1947
+ maxSizeFraction: 0.9,
1948
+ affinities: [
1949
+ "square",
1950
+ "hexagon",
1951
+ "star",
1952
+ "mandala"
1953
+ ],
1954
+ category: "complex",
1955
+ heroCandidate: true,
1956
+ bestStyles: [
1957
+ "stroke-only",
1958
+ "dashed",
1959
+ "hatched"
1960
+ ]
1961
+ },
1962
+ celticKnot: {
1963
+ tier: 2,
1964
+ minSizeFraction: 0.2,
1965
+ maxSizeFraction: 0.7,
1966
+ affinities: [
1967
+ "circle",
1968
+ "lissajous",
1969
+ "spirograph"
1970
+ ],
1971
+ category: "complex",
1972
+ heroCandidate: true,
1973
+ bestStyles: [
1974
+ "stroke-only",
1975
+ "double-stroke"
1976
+ ]
1977
+ },
1978
+ merkaba: {
1979
+ tier: 1,
1980
+ minSizeFraction: 0.15,
1981
+ maxSizeFraction: 1.0,
1982
+ affinities: [
1983
+ "triangle",
1984
+ "diamond",
1985
+ "sriYantra",
1986
+ "metatronsCube"
1987
+ ],
1988
+ category: "complex",
1989
+ heroCandidate: true,
1990
+ bestStyles: [
1991
+ "stroke-only",
1992
+ "fill-and-stroke",
1993
+ "double-stroke"
1994
+ ]
1995
+ },
1996
+ mandala: {
1997
+ tier: 1,
1998
+ minSizeFraction: 0.2,
1999
+ maxSizeFraction: 1.0,
2000
+ affinities: [
2001
+ "circle",
2002
+ "flowerOfLife",
2003
+ "spirograph",
2004
+ "rose"
2005
+ ],
2006
+ category: "complex",
2007
+ heroCandidate: true,
2008
+ bestStyles: [
2009
+ "stroke-only",
2010
+ "dashed",
2011
+ "incomplete"
2012
+ ]
2013
+ },
2014
+ fractal: {
2015
+ tier: 2,
2016
+ minSizeFraction: 0.2,
2017
+ maxSizeFraction: 0.8,
2018
+ affinities: [
2019
+ "blob",
2020
+ "lissajous",
2021
+ "circle"
2022
+ ],
2023
+ category: "complex",
2024
+ heroCandidate: true,
2025
+ bestStyles: [
2026
+ "stroke-only",
2027
+ "incomplete"
2028
+ ]
2029
+ },
2030
+ // ── Sacred shapes ─────────────────────────────────────────────
2031
+ flowerOfLife: {
2032
+ tier: 1,
2033
+ minSizeFraction: 0.2,
2034
+ maxSizeFraction: 1.0,
2035
+ affinities: [
2036
+ "circle",
2037
+ "hexagon",
2038
+ "seedOfLife",
2039
+ "eggOfLife",
2040
+ "metatronsCube"
2041
+ ],
2042
+ category: "sacred",
2043
+ heroCandidate: true,
2044
+ bestStyles: [
2045
+ "stroke-only",
2046
+ "watercolor",
2047
+ "incomplete"
2048
+ ]
2049
+ },
2050
+ treeOfLife: {
2051
+ tier: 2,
2052
+ minSizeFraction: 0.25,
2053
+ maxSizeFraction: 0.9,
2054
+ affinities: [
2055
+ "circle",
2056
+ "flowerOfLife",
2057
+ "metatronsCube"
2058
+ ],
2059
+ category: "sacred",
2060
+ heroCandidate: true,
2061
+ bestStyles: [
2062
+ "stroke-only",
2063
+ "double-stroke"
2064
+ ]
2065
+ },
2066
+ metatronsCube: {
2067
+ tier: 1,
2068
+ minSizeFraction: 0.2,
2069
+ maxSizeFraction: 1.0,
2070
+ affinities: [
2071
+ "hexagon",
2072
+ "flowerOfLife",
2073
+ "platonicSolid",
2074
+ "merkaba"
2075
+ ],
2076
+ category: "sacred",
2077
+ heroCandidate: true,
2078
+ bestStyles: [
2079
+ "stroke-only",
2080
+ "dashed",
2081
+ "incomplete"
2082
+ ]
2083
+ },
2084
+ sriYantra: {
2085
+ tier: 1,
2086
+ minSizeFraction: 0.2,
2087
+ maxSizeFraction: 1.0,
2088
+ affinities: [
2089
+ "triangle",
2090
+ "merkaba",
2091
+ "mandala",
2092
+ "diamond"
2093
+ ],
2094
+ category: "sacred",
2095
+ heroCandidate: true,
2096
+ bestStyles: [
2097
+ "stroke-only",
2098
+ "fill-and-stroke",
2099
+ "double-stroke"
2100
+ ]
2101
+ },
2102
+ seedOfLife: {
2103
+ tier: 1,
2104
+ minSizeFraction: 0.15,
2105
+ maxSizeFraction: 0.9,
2106
+ affinities: [
2107
+ "circle",
2108
+ "flowerOfLife",
2109
+ "eggOfLife",
2110
+ "hexagon"
2111
+ ],
2112
+ category: "sacred",
2113
+ heroCandidate: true,
2114
+ bestStyles: [
2115
+ "stroke-only",
2116
+ "watercolor",
2117
+ "fill-only"
2118
+ ]
2119
+ },
2120
+ vesicaPiscis: {
2121
+ tier: 2,
2122
+ minSizeFraction: 0.15,
2123
+ maxSizeFraction: 0.7,
2124
+ affinities: [
2125
+ "circle",
2126
+ "seedOfLife",
2127
+ "flowerOfLife"
2128
+ ],
2129
+ category: "sacred",
2130
+ heroCandidate: false,
2131
+ bestStyles: [
2132
+ "stroke-only",
2133
+ "watercolor"
2134
+ ]
2135
+ },
2136
+ torus: {
2137
+ tier: 3,
2138
+ minSizeFraction: 0.2,
2139
+ maxSizeFraction: 0.6,
2140
+ affinities: [
2141
+ "circle",
2142
+ "spirograph",
2143
+ "waveRing"
2144
+ ],
2145
+ category: "sacred",
2146
+ heroCandidate: false,
2147
+ bestStyles: [
2148
+ "stroke-only",
2149
+ "dashed"
2150
+ ]
2151
+ },
2152
+ eggOfLife: {
2153
+ tier: 2,
2154
+ minSizeFraction: 0.15,
2155
+ maxSizeFraction: 0.8,
2156
+ affinities: [
2157
+ "circle",
2158
+ "seedOfLife",
2159
+ "flowerOfLife"
2160
+ ],
2161
+ category: "sacred",
2162
+ heroCandidate: true,
2163
+ bestStyles: [
2164
+ "stroke-only",
2165
+ "watercolor"
2166
+ ]
2167
+ },
2168
+ // ── Procedural shapes ─────────────────────────────────────────
2169
+ blob: {
2170
+ tier: 1,
2171
+ minSizeFraction: 0.05,
2172
+ maxSizeFraction: 1.0,
2173
+ affinities: [
2174
+ "blob",
2175
+ "circle",
2176
+ "superellipse",
2177
+ "waveRing"
2178
+ ],
2179
+ category: "procedural",
2180
+ heroCandidate: false,
2181
+ bestStyles: [
2182
+ "fill-only",
2183
+ "watercolor",
2184
+ "fill-and-stroke"
2185
+ ]
2186
+ },
2187
+ ngon: {
2188
+ tier: 2,
2189
+ minSizeFraction: 0.06,
2190
+ maxSizeFraction: 0.8,
2191
+ affinities: [
2192
+ "hexagon",
2193
+ "triangle",
2194
+ "diamond",
2195
+ "superellipse"
2196
+ ],
2197
+ category: "procedural",
2198
+ heroCandidate: false,
2199
+ bestStyles: [
2200
+ "fill-and-stroke",
2201
+ "fill-only",
2202
+ "hatched"
2203
+ ]
2204
+ },
2205
+ lissajous: {
2206
+ tier: 2,
2207
+ minSizeFraction: 0.15,
2208
+ maxSizeFraction: 0.8,
2209
+ affinities: [
2210
+ "spirograph",
2211
+ "rose",
2212
+ "celticKnot",
2213
+ "fibonacciSpiral"
2214
+ ],
2215
+ category: "procedural",
2216
+ heroCandidate: false,
2217
+ bestStyles: [
2218
+ "stroke-only",
2219
+ "incomplete",
2220
+ "dashed"
2221
+ ]
2222
+ },
2223
+ superellipse: {
2224
+ tier: 1,
2225
+ minSizeFraction: 0.05,
2226
+ maxSizeFraction: 1.0,
2227
+ affinities: [
2228
+ "circle",
2229
+ "square",
2230
+ "blob",
2231
+ "hexagon"
2232
+ ],
2233
+ category: "procedural",
2234
+ heroCandidate: false,
2235
+ bestStyles: [
2236
+ "fill-only",
2237
+ "watercolor",
2238
+ "fill-and-stroke"
2239
+ ]
2240
+ },
2241
+ spirograph: {
2242
+ tier: 1,
2243
+ minSizeFraction: 0.15,
2244
+ maxSizeFraction: 0.9,
2245
+ affinities: [
2246
+ "rose",
2247
+ "lissajous",
2248
+ "mandala",
2249
+ "flowerOfLife"
2250
+ ],
2251
+ category: "procedural",
2252
+ heroCandidate: true,
2253
+ bestStyles: [
2254
+ "stroke-only",
2255
+ "incomplete",
2256
+ "dashed"
2257
+ ]
2258
+ },
2259
+ waveRing: {
2260
+ tier: 2,
2261
+ minSizeFraction: 0.1,
2262
+ maxSizeFraction: 0.8,
2263
+ affinities: [
2264
+ "circle",
2265
+ "blob",
2266
+ "torus",
2267
+ "spirograph"
2268
+ ],
2269
+ category: "procedural",
2270
+ heroCandidate: false,
2271
+ bestStyles: [
2272
+ "stroke-only",
2273
+ "dashed",
2274
+ "incomplete"
2275
+ ]
2276
+ },
2277
+ rose: {
2278
+ tier: 1,
2279
+ minSizeFraction: 0.1,
2280
+ maxSizeFraction: 0.9,
2281
+ affinities: [
2282
+ "spirograph",
2283
+ "mandala",
2284
+ "flowerOfLife",
2285
+ "circle"
2286
+ ],
2287
+ category: "procedural",
2288
+ heroCandidate: true,
2289
+ bestStyles: [
2290
+ "stroke-only",
2291
+ "fill-only",
2292
+ "watercolor"
2293
+ ]
2294
+ }
2295
+ };
2296
+ function $e73976f898150d4d$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
2297
+ const available = shapeNames.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]);
2298
+ // Pick a seed shape — tier 1 shapes that are hero candidates
2299
+ const heroPool = available.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s].tier === 1 && $e73976f898150d4d$export$4343b39fe47bd82c[s].heroCandidate);
2300
+ const seedShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : available[Math.floor(rng() * available.length)];
2301
+ const seedProfile = $e73976f898150d4d$export$4343b39fe47bd82c[seedShape];
2302
+ // Primary: seed shape + its direct affinities (tier 1-2 only)
2303
+ const primaryCandidates = [
2304
+ seedShape,
2305
+ ...seedProfile.affinities
2306
+ ].filter((s)=>available.includes(s)).filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s].tier <= 2);
2307
+ const primary = [
2308
+ ...new Set(primaryCandidates)
2309
+ ].slice(0, 5);
2310
+ // Supporting: affinities of affinities, plus same-category shapes
2311
+ const supportingSet = new Set();
2312
+ for (const p of primary){
2313
+ const profile = $e73976f898150d4d$export$4343b39fe47bd82c[p];
2314
+ if (!profile) continue;
2315
+ for (const aff of profile.affinities)if (available.includes(aff) && !primary.includes(aff)) supportingSet.add(aff);
2316
+ }
2317
+ // Add same-category tier 1-2 shapes
2318
+ for (const s of available){
2319
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
2320
+ if (p.category === seedProfile.category && p.tier <= 2 && !primary.includes(s)) supportingSet.add(s);
2321
+ }
2322
+ const supporting = [
2323
+ ...supportingSet
2324
+ ].slice(0, 6);
2325
+ // Accents: tier 1 shapes from other categories for contrast
2326
+ const usedCategories = new Set([
2327
+ ...primary,
2328
+ ...supporting
2329
+ ].map((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]?.category));
2330
+ const accentCandidates = available.filter((s)=>!primary.includes(s) && !supporting.includes(s) && $e73976f898150d4d$export$4343b39fe47bd82c[s].tier <= 2 && !usedCategories.has($e73976f898150d4d$export$4343b39fe47bd82c[s].category));
2331
+ // Shuffle and take a few
2332
+ const accents = [];
2333
+ const shuffled = [
2334
+ ...accentCandidates
2335
+ ];
2336
+ for(let i = shuffled.length - 1; i > 0; i--){
2337
+ const j = Math.floor(rng() * (i + 1));
2338
+ [shuffled[i], shuffled[j]] = [
2339
+ shuffled[j],
2340
+ shuffled[i]
2341
+ ];
2342
+ }
2343
+ accents.push(...shuffled.slice(0, 3));
2344
+ // For certain archetypes, bias the palette
2345
+ if (archetypeName === "geometric-precision") // Remove blobs and organic shapes from primary
2346
+ return {
2347
+ primary: primary.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]?.category !== "procedural" || s === "ngon"),
2348
+ supporting: supporting.filter((s)=>s !== "blob"),
2349
+ accents: accents
2350
+ };
2351
+ if (archetypeName === "organic-flow") {
2352
+ // Boost procedural/organic shapes
2353
+ const organicBoost = available.filter((s)=>[
2354
+ "blob",
2355
+ "superellipse",
2356
+ "waveRing",
2357
+ "rose"
2358
+ ].includes(s) && !primary.includes(s));
2359
+ return {
2360
+ primary: [
2361
+ ...primary,
2362
+ ...organicBoost.slice(0, 2)
2363
+ ],
2364
+ supporting: supporting,
2365
+ accents: accents
2366
+ };
2367
+ }
2368
+ return {
2369
+ primary: primary,
2370
+ supporting: supporting,
2371
+ accents: accents
2372
+ };
2373
+ }
2374
+ function $e73976f898150d4d$export$3c37d9a045754d0e(palette, rng, sizeFraction) {
2375
+ // Filter each tier by size constraints
2376
+ const validPrimary = palette.primary.filter((s)=>{
2377
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
2378
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2379
+ });
2380
+ const validSupporting = palette.supporting.filter((s)=>{
2381
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
2382
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2383
+ });
2384
+ const validAccents = palette.accents.filter((s)=>{
2385
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
2386
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2387
+ });
2388
+ const roll = rng();
2389
+ if (roll < 0.60 && validPrimary.length > 0) return validPrimary[Math.floor(rng() * validPrimary.length)];
2390
+ if (roll < 0.90 && validSupporting.length > 0) return validSupporting[Math.floor(rng() * validSupporting.length)];
2391
+ if (validAccents.length > 0) return validAccents[Math.floor(rng() * validAccents.length)];
2392
+ // Fallback: any valid primary or supporting
2393
+ const fallback = [
2394
+ ...validPrimary,
2395
+ ...validSupporting
2396
+ ];
2397
+ if (fallback.length > 0) return fallback[Math.floor(rng() * fallback.length)];
2398
+ // Ultimate fallback
2399
+ return palette.primary[0] || "circle";
2400
+ }
2401
+ function $e73976f898150d4d$export$ab873bb6fb56c1a8(shapeName, layerStyle, rng) {
2402
+ const profile = $e73976f898150d4d$export$4343b39fe47bd82c[shapeName];
2403
+ if (!profile || rng() > 0.7) return layerStyle;
2404
+ return profile.bestStyles[Math.floor(rng() * profile.bestStyles.length)];
2405
+ }
2406
+
2407
+
1621
2408
 
1622
2409
  /**
1623
2410
  * Configuration options for image generation.
@@ -1832,6 +2619,69 @@ const $f89bc858f7202849$var$ARCHETYPES = [
1832
2619
  sizePower: 2.5,
1833
2620
  invertForeground: false
1834
2621
  },
2622
+ {
2623
+ name: "watercolor-wash",
2624
+ gridSize: 3,
2625
+ layers: 3,
2626
+ baseOpacity: 0.25,
2627
+ opacityReduction: 0.03,
2628
+ minShapeSize: 200,
2629
+ maxShapeSize: 700,
2630
+ backgroundStyle: "radial-light",
2631
+ paletteMode: "harmonious",
2632
+ preferredStyles: [
2633
+ "watercolor",
2634
+ "fill-only",
2635
+ "incomplete"
2636
+ ],
2637
+ flowLineMultiplier: 0.5,
2638
+ heroShape: false,
2639
+ glowMultiplier: 0.3,
2640
+ sizePower: 0.6,
2641
+ invertForeground: false
2642
+ },
2643
+ {
2644
+ name: "op-art",
2645
+ gridSize: 8,
2646
+ layers: 2,
2647
+ baseOpacity: 0.95,
2648
+ opacityReduction: 0.05,
2649
+ minShapeSize: 20,
2650
+ maxShapeSize: 200,
2651
+ backgroundStyle: "solid-light",
2652
+ paletteMode: "high-contrast",
2653
+ preferredStyles: [
2654
+ "fill-and-stroke",
2655
+ "stroke-only",
2656
+ "dashed"
2657
+ ],
2658
+ flowLineMultiplier: 0,
2659
+ heroShape: false,
2660
+ glowMultiplier: 0,
2661
+ sizePower: 0.4,
2662
+ invertForeground: false
2663
+ },
2664
+ {
2665
+ name: "collage",
2666
+ gridSize: 4,
2667
+ layers: 3,
2668
+ baseOpacity: 0.9,
2669
+ opacityReduction: 0.08,
2670
+ minShapeSize: 80,
2671
+ maxShapeSize: 500,
2672
+ backgroundStyle: "solid-light",
2673
+ paletteMode: "duotone",
2674
+ preferredStyles: [
2675
+ "fill-and-stroke",
2676
+ "fill-only",
2677
+ "double-stroke"
2678
+ ],
2679
+ flowLineMultiplier: 0,
2680
+ heroShape: true,
2681
+ glowMultiplier: 0,
2682
+ sizePower: 0.7,
2683
+ invertForeground: false
2684
+ },
1835
2685
  {
1836
2686
  name: "classic",
1837
2687
  gridSize: 5,
@@ -1859,26 +2709,7 @@ function $f89bc858f7202849$export$f1142fd7da4d6590(rng) {
1859
2709
  }
1860
2710
 
1861
2711
 
1862
- // ── Shape categories for weighted selection ─────────────────────────
1863
- const $4f72c5a314eddf25$var$BASIC_SHAPES = [
1864
- "circle",
1865
- "square",
1866
- "triangle",
1867
- "hexagon",
1868
- "diamond",
1869
- "cube"
1870
- ];
1871
- const $4f72c5a314eddf25$var$COMPLEX_SHAPES = [
1872
- "star",
1873
- "jacked-star",
1874
- "heart",
1875
- "platonicSolid",
1876
- "fibonacciSpiral",
1877
- "islamicPattern",
1878
- "celticKnot",
1879
- "merkaba",
1880
- "fractal"
1881
- ];
2712
+ // ── Shape categories for weighted selection (legacy fallback) ───────
1882
2713
  const $4f72c5a314eddf25$var$SACRED_SHAPES = [
1883
2714
  "mandala",
1884
2715
  "flowerOfLife",
@@ -1890,15 +2721,6 @@ const $4f72c5a314eddf25$var$SACRED_SHAPES = [
1890
2721
  "torus",
1891
2722
  "eggOfLife"
1892
2723
  ];
1893
- const $4f72c5a314eddf25$var$PROCEDURAL_SHAPES = [
1894
- "blob",
1895
- "ngon",
1896
- "lissajous",
1897
- "superellipse",
1898
- "spirograph",
1899
- "waveRing",
1900
- "rose"
1901
- ];
1902
2724
  const $4f72c5a314eddf25$var$COMPOSITION_MODES = [
1903
2725
  "radial",
1904
2726
  "flow-field",
@@ -1906,23 +2728,6 @@ const $4f72c5a314eddf25$var$COMPOSITION_MODES = [
1906
2728
  "grid-subdivision",
1907
2729
  "clustered"
1908
2730
  ];
1909
- // ── Helper: pick shape with layer-aware weighting ───────────────────
1910
- function $4f72c5a314eddf25$var$pickShape(rng, layerRatio, shapeNames) {
1911
- const basicW = 1 - layerRatio * 0.6;
1912
- const complexW = 0.3 + layerRatio * 0.3;
1913
- const sacredW = 0.1 + layerRatio * 0.4;
1914
- const proceduralW = 0.25 + layerRatio * 0.2; // always present, grows with depth
1915
- const total = basicW + complexW + sacredW + proceduralW;
1916
- const roll = rng() * total;
1917
- let pool;
1918
- if (roll < basicW) pool = $4f72c5a314eddf25$var$BASIC_SHAPES;
1919
- else if (roll < basicW + complexW) pool = $4f72c5a314eddf25$var$COMPLEX_SHAPES;
1920
- else if (roll < basicW + complexW + sacredW) pool = $4f72c5a314eddf25$var$SACRED_SHAPES;
1921
- else pool = $4f72c5a314eddf25$var$PROCEDURAL_SHAPES;
1922
- const available = pool.filter((s)=>shapeNames.includes(s));
1923
- if (available.length === 0) return shapeNames[Math.floor(rng() * shapeNames.length)];
1924
- return available[Math.floor(rng() * available.length)];
1925
- }
1926
2731
  // ── Helper: get position based on composition mode ──────────────────
1927
2732
  function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
1928
2733
  switch(mode){
@@ -1981,22 +2786,28 @@ function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height,
1981
2786
  };
1982
2787
  }
1983
2788
  }
1984
- // ── Helper: positional color blending ───────────────────────────────
1985
- function $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, colors, rng) {
1986
- const nx = x / width;
1987
- const ny = y / height;
1988
- const posIndex = (nx * 0.6 + ny * 0.4) * (colors.length - 1);
1989
- const baseIdx = Math.floor(posIndex) % colors.length;
1990
- return (0, $d016ad53434219a1$export$59539d800dbe6858)(colors[baseIdx], rng, 0.08);
2789
+ // ── Helper: positional color from hierarchy ─────────────────────────
2790
+ function $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, hierarchy, rng) {
2791
+ // Blend position into color selection — shapes near center lean dominant
2792
+ const distFromCenter = Math.hypot(x - width / 2, y - height / 2) / Math.hypot(width / 2, height / 2);
2793
+ // Center = more dominant, edges = more accent
2794
+ if (distFromCenter < 0.35) return (0, $d016ad53434219a1$export$18a34c25ea7e724b)(hierarchy.dominant, rng, 10, 0.08);
2795
+ else if (distFromCenter < 0.7) return (0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(hierarchy, rng), rng, 8, 0.06);
2796
+ else {
2797
+ // Edges: bias toward secondary/accent
2798
+ const roll = rng();
2799
+ const color = roll < 0.4 ? hierarchy.secondary : roll < 0.75 ? hierarchy.accent : hierarchy.dominant;
2800
+ return (0, $d016ad53434219a1$export$18a34c25ea7e724b)(color, rng, 12, 0.08);
2801
+ }
1991
2802
  }
1992
- // ── Helper: check if a position is inside a void zone (Feature E) ───
2803
+ // ── Helper: check if a position is inside a void zone ───────────────
1993
2804
  function $4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones) {
1994
2805
  for (const zone of voidZones){
1995
2806
  if (Math.hypot(x - zone.x, y - zone.y) < zone.radius) return true;
1996
2807
  }
1997
2808
  return false;
1998
2809
  }
1999
- // ── Helper: density check for negative space (Feature E) ────────────
2810
+ // ── Helper: density check ───────────────────────────────────────────
2000
2811
  function $4f72c5a314eddf25$var$localDensity(x, y, positions, radius) {
2001
2812
  let count = 0;
2002
2813
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -2092,7 +2903,15 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2092
2903
  const [bgStart, bgEnd] = colorScheme.getBackgroundColorsByMode(archetype.paletteMode);
2093
2904
  const tempMode = colorScheme.getTemperatureMode();
2094
2905
  const fgTempTarget = tempMode === "warm-bg" ? "cool" : tempMode === "cool-bg" ? "warm" : null;
2906
+ // ── 0b. Color hierarchy — dominant/secondary/accent weighting ──
2907
+ const colorHierarchy = (0, $d016ad53434219a1$export$fabac4600b87056)(colors, rng);
2908
+ // ── 0c. Shape palette — curated shapes that work well together ──
2095
2909
  const shapeNames = Object.keys((0, $9c828bde2acaae64$export$4ff7fc6f1af248b5));
2910
+ const shapePalette = (0, $e73976f898150d4d$export$4a95df8944b5033b)(rng, shapeNames, archetype.name);
2911
+ // ── 0d. Color grading — unified tone for the whole image ───────
2912
+ const colorGrade = (0, $d016ad53434219a1$export$6d1620b367f86f7a)(rng);
2913
+ // ── 0e. Light direction — consistent shadow angle ──────────────
2914
+ const lightAngle = rng() * Math.PI * 2;
2096
2915
  const scaleFactor = Math.min(width, height) / 1024;
2097
2916
  const adjustedMinSize = minShapeSize * scaleFactor;
2098
2917
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -2101,27 +2920,45 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2101
2920
  // ── 1. Background ──────────────────────────────────────────────
2102
2921
  const bgRadius = Math.hypot(cx, cy);
2103
2922
  $4f72c5a314eddf25$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
2923
+ // Gradient mesh overlay — 3-4 color control points for richer backgrounds
2924
+ const meshPoints = 3 + Math.floor(rng() * 2);
2925
+ ctx.globalCompositeOperation = "soft-light";
2926
+ for(let i = 0; i < meshPoints; i++){
2927
+ const mx = rng() * width;
2928
+ const my = rng() * height;
2929
+ const mRadius = Math.min(width, height) * (0.3 + rng() * 0.4);
2930
+ const mColor = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
2931
+ const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
2932
+ grad.addColorStop(0, (0, $d016ad53434219a1$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
2933
+ grad.addColorStop(1, "rgba(0,0,0,0)");
2934
+ ctx.globalAlpha = 1;
2935
+ ctx.fillStyle = grad;
2936
+ ctx.fillRect(0, 0, width, height);
2937
+ }
2938
+ ctx.globalCompositeOperation = "source-over";
2104
2939
  // Compute average background luminance for contrast enforcement
2105
2940
  const bgLum = ((0, $d016ad53434219a1$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $d016ad53434219a1$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
2106
- // ── 1b. Layered background (Feature G) ─────────────────────────
2107
- // Draw large, very faint shapes to give the background texture
2941
+ // ── 1b. Layered background archetype-coherent shapes ─────────
2108
2942
  const bgShapeCount = 3 + Math.floor(rng() * 4);
2109
2943
  ctx.globalCompositeOperation = "soft-light";
2110
2944
  for(let i = 0; i < bgShapeCount; i++){
2111
2945
  const bx = rng() * width;
2112
2946
  const by = rng() * height;
2113
2947
  const bSize = width * 0.3 + rng() * width * 0.5;
2114
- const bColor = colors[Math.floor(rng() * colors.length)];
2948
+ const bColor = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
2115
2949
  ctx.globalAlpha = 0.03 + rng() * 0.05;
2116
2950
  ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(bColor, 0.15);
2117
2951
  ctx.beginPath();
2118
- ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
2952
+ // Use archetype-appropriate background shapes
2953
+ if (archetype.name === "geometric-precision" || archetype.name === "op-art") // Rectangular shapes for geometric archetypes
2954
+ ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
2955
+ else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
2119
2956
  ctx.fill();
2120
2957
  }
2121
2958
  // Subtle concentric rings from center
2122
2959
  const ringCount = 2 + Math.floor(rng() * 3);
2123
2960
  ctx.globalAlpha = 0.02 + rng() * 0.03;
2124
- ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colors[0], 0.1);
2961
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
2125
2962
  ctx.lineWidth = 1 * scaleFactor;
2126
2963
  for(let i = 1; i <= ringCount; i++){
2127
2964
  const r = Math.min(width, height) * 0.15 * i;
@@ -2135,7 +2972,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2135
2972
  const symRoll = rng();
2136
2973
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
2137
2974
  // ── 3. Focal points + void zones ───────────────────────────────
2138
- // Rule-of-thirds intersection points for intentional composition
2139
2975
  const THIRDS_POINTS = [
2140
2976
  {
2141
2977
  x: 1 / 3,
@@ -2156,10 +2992,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2156
2992
  ];
2157
2993
  const numFocal = 1 + Math.floor(rng() * 2);
2158
2994
  const focalPoints = [];
2159
- for(let f = 0; f < numFocal; f++)// 70% chance to snap to a rule-of-thirds point, 30% free placement
2160
- if (rng() < 0.7) {
2995
+ for(let f = 0; f < numFocal; f++)if (rng() < 0.7) {
2161
2996
  const tp = THIRDS_POINTS[Math.floor(rng() * THIRDS_POINTS.length)];
2162
- // Small jitter around the thirds point so it's not robotic
2163
2997
  focalPoints.push({
2164
2998
  x: width * (tp.x + (rng() - 0.5) * 0.08),
2165
2999
  y: height * (tp.y + (rng() - 0.5) * 0.08),
@@ -2170,7 +3004,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2170
3004
  y: height * (0.2 + rng() * 0.6),
2171
3005
  strength: 0.3 + rng() * 0.4
2172
3006
  });
2173
- // Feature E: 1-2 void zones where shapes are sparse (negative space)
2174
3007
  const numVoids = Math.floor(rng() * 2) + 1;
2175
3008
  const voidZones = [];
2176
3009
  for(let v = 0; v < numVoids; v++)voidZones.push({
@@ -2202,20 +3035,24 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2202
3035
  }
2203
3036
  // Track all placed shapes for density checks and connecting curves
2204
3037
  const shapePositions = [];
3038
+ // Hero avoidance radius — shapes near the hero orient toward it
3039
+ let heroCenter = null;
2205
3040
  // ── 4b. Hero shape — a dominant focal element ───────────────────
2206
3041
  if (archetype.heroShape && rng() < 0.6) {
2207
3042
  const heroFocal = focalPoints[0];
3043
+ // Use shape palette hero candidates
2208
3044
  const heroPool = [
2209
- ...$4f72c5a314eddf25$var$SACRED_SHAPES,
2210
- "fibonacciSpiral",
2211
- "merkaba",
2212
- "fractal"
2213
- ];
2214
- const heroShape = heroPool.filter((s)=>shapeNames.includes(s))[Math.floor(rng() * heroPool.filter((s)=>shapeNames.includes(s)).length)] || shapeNames[Math.floor(rng() * shapeNames.length)];
3045
+ ...shapePalette.primary,
3046
+ ...shapePalette.supporting
3047
+ ].filter((s)=>(0, $e73976f898150d4d$export$4343b39fe47bd82c)[s]?.heroCandidate && shapeNames.includes(s));
3048
+ const heroShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : shapeNames[Math.floor(rng() * shapeNames.length)];
2215
3049
  const heroSize = adjustedMaxSize * (0.8 + rng() * 0.5);
2216
3050
  const heroRotation = rng() * 360;
2217
- const heroFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05), bgLum), 0.15 + rng() * 0.2);
2218
- const heroStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05), bgLum);
3051
+ const heroFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.dominant, rng, 6, 0.05), bgLum), 0.15 + rng() * 0.2);
3052
+ const heroStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.accent, rng, 6, 0.05), bgLum);
3053
+ // Get best style for this hero shape
3054
+ const heroProfile = (0, $e73976f898150d4d$export$4343b39fe47bd82c)[heroShape];
3055
+ const heroStyle = heroProfile ? heroProfile.bestStyles[Math.floor(rng() * heroProfile.bestStyles.length)] : rng() < 0.4 ? "watercolor" : "fill-and-stroke";
2219
3056
  ctx.globalAlpha = 0.5 + rng() * 0.2;
2220
3057
  (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, heroShape, heroFocal.x, heroFocal.y, {
2221
3058
  fillColor: heroFill,
@@ -2226,14 +3063,20 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2226
3063
  proportionType: "GOLDEN_RATIO",
2227
3064
  glowRadius: (12 + rng() * 20) * scaleFactor,
2228
3065
  glowColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(heroStroke, 0.4),
2229
- gradientFillEnd: (0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1),
2230
- renderStyle: rng() < 0.4 ? "watercolor" : "fill-and-stroke",
3066
+ gradientFillEnd: (0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
3067
+ renderStyle: heroStyle,
2231
3068
  rng: rng
2232
3069
  });
2233
- shapePositions.push({
3070
+ heroCenter = {
2234
3071
  x: heroFocal.x,
2235
3072
  y: heroFocal.y,
2236
3073
  size: heroSize
3074
+ };
3075
+ shapePositions.push({
3076
+ x: heroFocal.x,
3077
+ y: heroFocal.y,
3078
+ size: heroSize,
3079
+ shape: heroShape
2237
3080
  });
2238
3081
  }
2239
3082
  // ── 5. Shape layers ────────────────────────────────────────────
@@ -2244,41 +3087,52 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2244
3087
  const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
2245
3088
  const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
2246
3089
  const layerSizeScale = 1 - layer * 0.15;
2247
- // Feature B: per-layer blend mode
3090
+ // Per-layer blend mode
2248
3091
  const layerBlend = (0, $c3de8257a8baa3b0$export$7bb7bff4e26fa06b)(rng);
2249
3092
  ctx.globalCompositeOperation = layerBlend;
2250
- // Feature C: per-layer render style bias — prefer archetype styles
3093
+ // Per-layer render style bias — prefer archetype styles
2251
3094
  const layerRenderStyle = rng() < 0.6 ? archetype.preferredStyles[Math.floor(rng() * archetype.preferredStyles.length)] : (0, $c3de8257a8baa3b0$export$9fd4e64b2acd410e)(rng);
2252
- // Feature D: atmospheric desaturation for later layers
2253
- const atmosphericDesat = layerRatio * 0.3; // 0 for first layer, up to 0.3 for last
3095
+ // Atmospheric desaturation for later layers
3096
+ const atmosphericDesat = layerRatio * 0.3;
2254
3097
  for(let i = 0; i < numShapes; i++){
2255
3098
  // Position from composition mode, then focal bias
2256
3099
  const rawPos = $4f72c5a314eddf25$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
2257
3100
  const [x, y] = applyFocalBias(rawPos.x, rawPos.y);
2258
- // Feature E: skip shapes in void zones, reduce in dense areas
3101
+ // Skip shapes in void zones, reduce in dense areas
2259
3102
  if ($4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones)) {
2260
- // 85% chance to skip — allows a few shapes to bleed in
2261
3103
  if (rng() < 0.85) continue;
2262
3104
  }
2263
3105
  if ($4f72c5a314eddf25$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
2264
- if (rng() < 0.6) continue; // thin out dense areas
3106
+ if (rng() < 0.6) continue;
2265
3107
  }
2266
- // Weighted shape selection
2267
- const shape = $4f72c5a314eddf25$var$pickShape(rng, layerRatio, shapeNames);
2268
3108
  // Power distribution for size — archetype controls the curve
2269
3109
  const sizeT = Math.pow(rng(), archetype.sizePower);
2270
3110
  const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
3111
+ // Size fraction for affinity-aware shape selection
3112
+ const sizeFraction = size / adjustedMaxSize;
3113
+ // Palette-driven shape selection (replaces naive pickShape)
3114
+ const shape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, sizeFraction);
2271
3115
  // Flow-field rotation in flow-field mode, random otherwise
2272
- const rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
2273
- // Positional color blending + jitter
2274
- let fillBase = $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, colors, rng);
2275
- const strokeBase = colors[Math.floor(rng() * colors.length)];
2276
- // Feature D: desaturate colors on later layers for depth
3116
+ let rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
3117
+ // Hero avoidance: shapes near the hero orient toward it
3118
+ if (heroCenter) {
3119
+ const distToHero = Math.hypot(x - heroCenter.x, y - heroCenter.y);
3120
+ const heroInfluence = heroCenter.size * 1.5;
3121
+ if (distToHero < heroInfluence && distToHero > 0) {
3122
+ const angleToHero = Math.atan2(heroCenter.y - y, heroCenter.x - x) * 180 / Math.PI;
3123
+ const blendFactor = 1 - distToHero / heroInfluence;
3124
+ rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
3125
+ }
3126
+ }
3127
+ // Positional color from hierarchy + jitter
3128
+ let fillBase = $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
3129
+ const strokeBase = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
3130
+ // Desaturate colors on later layers for depth
2277
3131
  if (atmosphericDesat > 0) fillBase = (0, $d016ad53434219a1$export$fb75607d98509d9)(fillBase, atmosphericDesat);
2278
3132
  // Temperature contrast: shift foreground shapes opposite to background
2279
3133
  if (fgTempTarget) fillBase = (0, $d016ad53434219a1$export$51ea55f869b7e0d3)(fillBase, fgTempTarget, 0.15 + layerRatio * 0.1);
2280
- const fillColor = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$59539d800dbe6858)(fillBase, rng, 0.06), bgLum);
2281
- const strokeColor = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$59539d800dbe6858)(strokeBase, rng, 0.05), bgLum);
3134
+ const fillColor = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(fillBase, rng, 6, 0.05), bgLum);
3135
+ const strokeColor = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
2282
3136
  // Semi-transparent fill
2283
3137
  const fillAlpha = 0.2 + rng() * 0.5;
2284
3138
  const transparentFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, fillAlpha);
@@ -2292,12 +3146,16 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2292
3146
  const glowRadius = hasGlow ? (8 + rng() * 20) * scaleFactor : 0;
2293
3147
  // Gradient fill on ~30%
2294
3148
  const hasGradient = rng() < 0.3;
2295
- const gradientEnd = hasGradient ? (0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1) : undefined;
2296
- // Feature C: per-shape render style (70% use layer style, 30% pick their own)
2297
- const shapeRenderStyle = rng() < 0.7 ? layerRenderStyle : (0, $c3de8257a8baa3b0$export$9fd4e64b2acd410e)(rng);
2298
- // Feature F: organic edge jitter — applied via watercolor style on ~15% of shapes
3149
+ const gradientEnd = hasGradient ? (0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1) : undefined;
3150
+ // Affinity-aware render style selection
3151
+ const shapeRenderStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
3152
+ // Organic edge jitter — applied via watercolor style on ~15% of shapes
2299
3153
  const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
2300
3154
  const finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
3155
+ // Consistent light direction — subtle shadow offset
3156
+ const shadowDist = hasGlow ? 0 : size * 0.02;
3157
+ const shadowOffX = shadowDist * Math.cos(lightAngle);
3158
+ const shadowOffY = shadowDist * Math.sin(lightAngle);
2301
3159
  (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
2302
3160
  fillColor: transparentFill,
2303
3161
  strokeColor: strokeColor,
@@ -2305,8 +3163,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2305
3163
  size: size,
2306
3164
  rotation: rotation,
2307
3165
  proportionType: "GOLDEN_RATIO",
2308
- glowRadius: glowRadius,
2309
- glowColor: hasGlow ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.6) : undefined,
3166
+ glowRadius: glowRadius || (shadowDist > 0 ? shadowDist * 2 : 0),
3167
+ glowColor: hasGlow ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
2310
3168
  gradientFillEnd: gradientEnd,
2311
3169
  renderStyle: finalRenderStyle,
2312
3170
  rng: rng
@@ -2314,18 +3172,51 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2314
3172
  shapePositions.push({
2315
3173
  x: x,
2316
3174
  y: y,
2317
- size: size
3175
+ size: size,
3176
+ shape: shape
2318
3177
  });
2319
- // ── 5b. Recursive nesting ──────────────────────────────────
3178
+ // ── 5b. Size echo — large shapes spawn trailing smaller copies ──
3179
+ if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
3180
+ const echoCount = 2 + Math.floor(rng() * 2);
3181
+ const echoAngle = rng() * Math.PI * 2;
3182
+ for(let e = 0; e < echoCount; e++){
3183
+ const echoScale = 0.3 - e * 0.08;
3184
+ const echoDist = size * (0.6 + e * 0.4);
3185
+ const echoX = x + Math.cos(echoAngle) * echoDist;
3186
+ const echoY = y + Math.sin(echoAngle) * echoDist;
3187
+ const echoSize = size * Math.max(0.1, echoScale);
3188
+ if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
3189
+ ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
3190
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
3191
+ fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
3192
+ strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.4),
3193
+ strokeWidth: strokeWidth * 0.6,
3194
+ size: echoSize,
3195
+ rotation: rotation + (e + 1) * 15,
3196
+ proportionType: "GOLDEN_RATIO",
3197
+ renderStyle: finalRenderStyle,
3198
+ rng: rng
3199
+ });
3200
+ shapePositions.push({
3201
+ x: echoX,
3202
+ y: echoY,
3203
+ size: echoSize,
3204
+ shape: shape
3205
+ });
3206
+ }
3207
+ }
3208
+ // ── 5c. Recursive nesting ──────────────────────────────────
2320
3209
  if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
2321
3210
  const innerCount = 1 + Math.floor(rng() * 3);
2322
3211
  for(let n = 0; n < innerCount; n++){
2323
- const innerShape = $4f72c5a314eddf25$var$pickShape(rng, Math.min(1, layerRatio + 0.3), shapeNames);
3212
+ // Pick inner shape from palette affinities
3213
+ const innerSizeFraction = size * 0.25 / adjustedMaxSize;
3214
+ const innerShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
2324
3215
  const innerSize = size * (0.15 + rng() * 0.25);
2325
3216
  const innerOffX = (rng() - 0.5) * size * 0.4;
2326
3217
  const innerOffY = (rng() - 0.5) * size * 0.4;
2327
3218
  const innerRot = rng() * 360;
2328
- const innerFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1), 0.3 + rng() * 0.4);
3219
+ const innerFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
2329
3220
  ctx.globalAlpha = layerOpacity * 0.7;
2330
3221
  (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, innerShape, x + innerOffX, y + innerOffY, {
2331
3222
  fillColor: innerFill,
@@ -2334,7 +3225,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2334
3225
  size: innerSize,
2335
3226
  rotation: innerRot,
2336
3227
  proportionType: "GOLDEN_RATIO",
2337
- renderStyle: shapeRenderStyle,
3228
+ renderStyle: (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng),
2338
3229
  rng: rng
2339
3230
  });
2340
3231
  }
@@ -2343,7 +3234,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2343
3234
  }
2344
3235
  // Reset blend mode for post-processing passes
2345
3236
  ctx.globalCompositeOperation = "source-over";
2346
- // ── 6. Flow-line pass (Feature H: tapered brush strokes) ───────
3237
+ // ── 6. Flow-line pass variable color, branching, pressure ────
2347
3238
  const baseFlowLines = 6 + Math.floor(rng() * 10);
2348
3239
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
2349
3240
  for(let i = 0; i < numFlowLines; i++){
@@ -2352,9 +3243,13 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2352
3243
  const steps = 30 + Math.floor(rng() * 40);
2353
3244
  const stepLen = (3 + rng() * 5) * scaleFactor;
2354
3245
  const startWidth = (1 + rng() * 3) * scaleFactor;
2355
- const lineColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)(colors[Math.floor(rng() * colors.length)], bgLum), 0.4);
3246
+ // Variable color: interpolate between two hierarchy colors along the stroke
3247
+ const lineColorStart = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
3248
+ const lineColorEnd = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
2356
3249
  const lineAlpha = 0.06 + rng() * 0.1;
2357
- // Draw as individual segments with tapering width
3250
+ // Pressure simulation: sinusoidal width variation
3251
+ const pressureFreq = 2 + rng() * 4;
3252
+ const pressurePhase = rng() * Math.PI * 2;
2358
3253
  let prevX = fx;
2359
3254
  let prevY = fy;
2360
3255
  for(let s = 0; s < steps; s++){
@@ -2362,37 +3257,61 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2362
3257
  fx += Math.cos(angle) * stepLen;
2363
3258
  fy += Math.sin(angle) * stepLen;
2364
3259
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
2365
- // Taper: thick at start, thin at end
2366
- const taper = 1 - s / steps * 0.8;
3260
+ const t = s / steps;
3261
+ // Taper + pressure
3262
+ const taper = 1 - t * 0.8;
3263
+ const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
2367
3264
  ctx.globalAlpha = lineAlpha * taper;
3265
+ // Interpolate color along stroke
3266
+ const lineColor = t < 0.5 ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $d016ad53434219a1$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
2368
3267
  ctx.strokeStyle = lineColor;
2369
- ctx.lineWidth = startWidth * taper;
3268
+ ctx.lineWidth = startWidth * taper * pressure;
2370
3269
  ctx.lineCap = "round";
2371
3270
  ctx.beginPath();
2372
3271
  ctx.moveTo(prevX, prevY);
2373
3272
  ctx.lineTo(fx, fy);
2374
3273
  ctx.stroke();
3274
+ // Branching: ~12% chance per step to spawn a thinner child stroke
3275
+ if (rng() < 0.12 && s > 5 && s < steps - 10) {
3276
+ const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
3277
+ let bx = fx;
3278
+ let by = fy;
3279
+ let bPrevX = fx;
3280
+ let bPrevY = fy;
3281
+ const branchSteps = 5 + Math.floor(rng() * 10);
3282
+ const branchWidth = startWidth * taper * 0.4;
3283
+ for(let bs = 0; bs < branchSteps; bs++){
3284
+ const bAngle = branchAngle + (rng() - 0.5) * 0.2;
3285
+ bx += Math.cos(bAngle) * stepLen * 0.8;
3286
+ by += Math.sin(bAngle) * stepLen * 0.8;
3287
+ if (bx < 0 || bx > width || by < 0 || by > height) break;
3288
+ const bTaper = 1 - bs / branchSteps * 0.9;
3289
+ ctx.globalAlpha = lineAlpha * taper * bTaper * 0.6;
3290
+ ctx.lineWidth = branchWidth * bTaper;
3291
+ ctx.beginPath();
3292
+ ctx.moveTo(bPrevX, bPrevY);
3293
+ ctx.lineTo(bx, by);
3294
+ ctx.stroke();
3295
+ bPrevX = bx;
3296
+ bPrevY = by;
3297
+ }
3298
+ }
2375
3299
  prevX = fx;
2376
3300
  prevY = fy;
2377
3301
  }
2378
3302
  }
2379
3303
  // ── 6b. Apply symmetry mirroring ─────────────────────────────────
2380
- // Mirror the rendered content (shapes + flow lines) before post-processing.
2381
- // Uses ctx.canvas which is available in both Node (@napi-rs/canvas) and browsers.
2382
3304
  if (symmetryMode !== "none") {
2383
3305
  const canvas = ctx.canvas;
2384
3306
  ctx.save();
2385
3307
  if (symmetryMode === "bilateral-x" || symmetryMode === "quad") {
2386
- // Mirror left half onto right half
2387
3308
  ctx.save();
2388
3309
  ctx.translate(width, 0);
2389
3310
  ctx.scale(-1, 1);
2390
- // Draw the left half (0 to cx) onto the mirrored right side
2391
3311
  ctx.drawImage(canvas, 0, 0, Math.ceil(cx), height, 0, 0, Math.ceil(cx), height);
2392
3312
  ctx.restore();
2393
3313
  }
2394
3314
  if (symmetryMode === "bilateral-y" || symmetryMode === "quad") {
2395
- // Mirror top half onto bottom half
2396
3315
  ctx.save();
2397
3316
  ctx.translate(0, height);
2398
3317
  ctx.scale(1, -1);
@@ -2415,7 +3334,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2415
3334
  }
2416
3335
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
2417
3336
  ctx.globalAlpha = 1;
2418
- const vignetteStrength = 0.25 + rng() * 0.2; // 25-45% edge darkening
3337
+ const vignetteStrength = 0.25 + rng() * 0.2;
2419
3338
  const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
2420
3339
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
2421
3340
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
@@ -2441,13 +3360,57 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2441
3360
  const cpx = mx + -dy / (dist || 1) * bulge;
2442
3361
  const cpy = my + dx / (dist || 1) * bulge;
2443
3362
  ctx.globalAlpha = 0.06 + rng() * 0.1;
2444
- ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)(colors[Math.floor(rng() * colors.length)], bgLum), 0.3);
3363
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
2445
3364
  ctx.beginPath();
2446
3365
  ctx.moveTo(a.x, a.y);
2447
3366
  ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);
2448
3367
  ctx.stroke();
2449
3368
  }
2450
3369
  }
3370
+ // ── 10. Post-processing ────────────────────────────────────────
3371
+ // 10a. Color grading — unified tone across the whole image
3372
+ // Apply as a semi-transparent overlay in the grade hue
3373
+ ctx.globalAlpha = colorGrade.intensity * 0.25;
3374
+ ctx.globalCompositeOperation = "soft-light";
3375
+ const gradeHsl = `hsl(${Math.round(colorGrade.hue)}, 40%, 50%)`;
3376
+ ctx.fillStyle = gradeHsl;
3377
+ ctx.fillRect(0, 0, width, height);
3378
+ ctx.globalCompositeOperation = "source-over";
3379
+ // 10b. Chromatic aberration — subtle RGB channel offset at edges
3380
+ // Only apply for neon/cosmic/ethereal archetypes where it fits
3381
+ const chromaArchetypes = [
3382
+ "neon-glow",
3383
+ "cosmic",
3384
+ "ethereal"
3385
+ ];
3386
+ if (chromaArchetypes.includes(archetype.name)) {
3387
+ const chromaOffset = Math.ceil(2 * scaleFactor);
3388
+ const canvas = ctx.canvas;
3389
+ // Shift red channel slightly
3390
+ ctx.globalAlpha = 0.03;
3391
+ ctx.globalCompositeOperation = "screen";
3392
+ ctx.drawImage(canvas, chromaOffset, 0, width, height, 0, 0, width, height);
3393
+ // Shift blue channel opposite
3394
+ ctx.drawImage(canvas, -chromaOffset, 0, width, height, 0, 0, width, height);
3395
+ ctx.globalCompositeOperation = "source-over";
3396
+ }
3397
+ // 10c. Bloom — soft glow on bright areas for neon/cosmic archetypes
3398
+ const bloomArchetypes = [
3399
+ "neon-glow",
3400
+ "cosmic"
3401
+ ];
3402
+ if (bloomArchetypes.includes(archetype.name)) {
3403
+ const canvas = ctx.canvas;
3404
+ ctx.globalAlpha = 0.08;
3405
+ ctx.globalCompositeOperation = "screen";
3406
+ // Draw the image slightly scaled up and blurred via shadow
3407
+ ctx.save();
3408
+ ctx.shadowBlur = 30 * scaleFactor;
3409
+ ctx.shadowColor = "rgba(255,255,255,0.3)";
3410
+ ctx.drawImage(canvas, 0, 0, width, height);
3411
+ ctx.restore();
3412
+ ctx.globalCompositeOperation = "source-over";
3413
+ }
2451
3414
  ctx.globalAlpha = 1;
2452
3415
  }
2453
3416