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/browser.js CHANGED
@@ -12,17 +12,22 @@ import $4wRzV$colorscheme from "color-scheme";
12
12
  * identically in Node (@napi-rs/canvas) and browsers.
13
13
  *
14
14
  * Generation pipeline:
15
- * 1. Background radial gradient from hash-derived dark palette
16
- * 1b. Layered background large faint shapes / subtle pattern for depth
17
- * 2. Composition modehash selects: radial, flow-field, spiral, grid-subdivision, or clustered
18
- * 3. Focal points + void zones (negative space)
19
- * 4. Flow field seed values
20
- * 5. Shape layers — blend modes, render styles, weighted selection,
21
- * focal-point placement, atmospheric depth, organic edges
15
+ * 0. Archetype selection + shape palette + color hierarchy
16
+ * 1. Backgroundstyle from archetype, gradient mesh for depth
17
+ * 1b. Layered background archetype-coherent shapes
18
+ * 2. Composition mode + symmetry
19
+ * 3. Focal points + void zones + hero avoidance field
20
+ * 4. Flow field
21
+ * 4b. Hero shape
22
+ * 5. Shape layers — palette-driven selection, affinity-aware styles,
23
+ * size echo, tangent placement, atmospheric depth
22
24
  * 5b. Recursive nesting
23
- * 6. Flow-line passtapered brush-stroke curves
24
- * 7. Noise texture overlay
25
- * 8. Organic connecting curves
25
+ * 6. Flow linesvariable color, branching, pressure simulation
26
+ * 6b. Symmetry mirroring
27
+ * 7. Noise texture
28
+ * 8. Vignette
29
+ * 9. Organic connecting curves
30
+ * 10. Post-processing — color grading, chromatic aberration, bloom
26
31
  */
27
32
  // declare module 'color-scheme';
28
33
 
@@ -388,6 +393,67 @@ function $b5a262d09b87e373$export$f2121afcad3d553f(hex, alpha) {
388
393
  const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex);
389
394
  return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
390
395
  }
396
+ function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
397
+ if (colors.length < 3) return {
398
+ dominant: colors[0] || "#888888",
399
+ secondary: colors[1] || colors[0] || "#888888",
400
+ accent: colors[colors.length - 1] || "#888888",
401
+ all: colors
402
+ };
403
+ // Pick dominant as the color closest to the palette's average hue
404
+ const hsls = colors.map((c)=>$b5a262d09b87e373$var$hexToHsl(c));
405
+ const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
406
+ let dominantIdx = 0;
407
+ let minDist = 360;
408
+ for(let i = 0; i < hsls.length; i++){
409
+ const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
410
+ if (d < minDist) {
411
+ minDist = d;
412
+ dominantIdx = i;
413
+ }
414
+ }
415
+ // Accent is the color most distant from dominant in hue
416
+ let accentIdx = 0;
417
+ let maxDist = 0;
418
+ for(let i = 0; i < hsls.length; i++){
419
+ if (i === dominantIdx) continue;
420
+ const d = Math.min(Math.abs(hsls[i][0] - hsls[dominantIdx][0]), 360 - Math.abs(hsls[i][0] - hsls[dominantIdx][0]));
421
+ if (d > maxDist) {
422
+ maxDist = d;
423
+ accentIdx = i;
424
+ }
425
+ }
426
+ // Secondary is the remaining color with highest saturation
427
+ let secondaryIdx = 0;
428
+ let maxSat = -1;
429
+ for(let i = 0; i < hsls.length; i++){
430
+ if (i === dominantIdx || i === accentIdx) continue;
431
+ if (hsls[i][1] > maxSat) {
432
+ maxSat = hsls[i][1];
433
+ secondaryIdx = i;
434
+ }
435
+ }
436
+ if (secondaryIdx === dominantIdx) secondaryIdx = accentIdx === 0 ? 1 : 0;
437
+ return {
438
+ dominant: colors[dominantIdx],
439
+ secondary: colors[secondaryIdx],
440
+ accent: colors[accentIdx],
441
+ all: colors
442
+ };
443
+ }
444
+ function $b5a262d09b87e373$export$b49f62f0a99da0e8(hierarchy, rng) {
445
+ const roll = rng();
446
+ if (roll < 0.60) return hierarchy.dominant;
447
+ if (roll < 0.85) return hierarchy.secondary;
448
+ return hierarchy.accent;
449
+ }
450
+ function $b5a262d09b87e373$export$18a34c25ea7e724b(hex, rng, hueAmount = 8, slAmount = 0.06) {
451
+ const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
452
+ const newH = (h + (rng() - 0.5) * hueAmount * 2 + 360) % 360;
453
+ const newS = Math.max(0, Math.min(1, s + (rng() - 0.5) * slAmount * 2));
454
+ const newL = Math.max(0, Math.min(1, l + (rng() - 0.5) * slAmount * 2));
455
+ return $b5a262d09b87e373$var$hslToHex(newH, newS, newL);
456
+ }
391
457
  function $b5a262d09b87e373$export$59539d800dbe6858(hex, rng, amount = 0.1) {
392
458
  const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex);
393
459
  const jit = ()=>(rng() - 0.5) * 2 * amount * 255;
@@ -427,6 +493,31 @@ function $b5a262d09b87e373$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContra
427
493
  return $b5a262d09b87e373$var$hslToHex(h, targetS, targetL);
428
494
  }
429
495
  }
496
+ function $b5a262d09b87e373$export$4a3734b8c4b5c0e(hex, gradeHue, intensity) {
497
+ const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
498
+ // Blend hue toward the grade hue
499
+ const hueDiff = (gradeHue - h + 540) % 360 - 180;
500
+ const newH = (h + hueDiff * intensity * 0.3 + 360) % 360;
501
+ // Slightly unify saturation
502
+ const newS = Math.max(0, Math.min(1, s + (0.5 - s) * intensity * 0.15));
503
+ return $b5a262d09b87e373$var$hslToHex(newH, newS, l);
504
+ }
505
+ function $b5a262d09b87e373$export$6d1620b367f86f7a(rng) {
506
+ // Warm golden, cool blue, rosy, teal, amber
507
+ const GRADE_HUES = [
508
+ 40,
509
+ 220,
510
+ 340,
511
+ 175,
512
+ 30
513
+ ];
514
+ const hue = GRADE_HUES[Math.floor(rng() * GRADE_HUES.length)] + (rng() - 0.5) * 20;
515
+ const intensity = 0.15 + rng() * 0.25;
516
+ return {
517
+ hue: (hue + 360) % 360,
518
+ intensity: intensity
519
+ };
520
+ }
430
521
 
431
522
 
432
523
 
@@ -1457,23 +1548,50 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
1457
1548
  break;
1458
1549
  case "watercolor":
1459
1550
  {
1460
- // Draw 3-4 slightly offset passes at low opacity for a bleed effect
1461
- const passes = 3 + (rng ? Math.floor(rng() * 2) : 0);
1551
+ // Improved watercolor: edge darkening + radial bleed + layered washes
1552
+ const passes = 4 + (rng ? Math.floor(rng() * 2) : 0);
1462
1553
  const savedAlpha = ctx.globalAlpha;
1463
- ctx.globalAlpha = savedAlpha * (0.3 / passes * 2);
1554
+ // Pass 1: Base wash large, soft fill at low opacity
1555
+ ctx.globalAlpha = savedAlpha * 0.15;
1556
+ ctx.save();
1557
+ const baseScale = 1.08 + (rng ? rng() * 0.04 : 0);
1558
+ ctx.scale(baseScale, baseScale);
1559
+ ctx.fill();
1560
+ ctx.restore();
1561
+ // Pass 2: Multiple offset washes with radial displacement
1562
+ ctx.globalAlpha = savedAlpha * (0.25 / passes * 2);
1464
1563
  for(let p = 0; p < passes; p++){
1465
- const jx = rng ? (rng() - 0.5) * size * 0.06 : 0;
1466
- const jy = rng ? (rng() - 0.5) * size * 0.06 : 0;
1564
+ // Radial outward displacement (not uniform) for organic bleed
1565
+ const angle = rng ? rng() * Math.PI * 2 : p * Math.PI / 2;
1566
+ const dist = rng ? rng() * size * 0.05 : size * 0.02;
1567
+ const jx = Math.cos(angle) * dist;
1568
+ const jy = Math.sin(angle) * dist;
1467
1569
  ctx.save();
1468
1570
  ctx.translate(jx, jy);
1469
1571
  ctx.fill();
1470
1572
  ctx.restore();
1471
1573
  }
1574
+ // Pass 3: Edge darkening — draw a slightly smaller shape with lighter fill
1575
+ // to simulate pigment pooling at boundaries
1576
+ ctx.globalAlpha = savedAlpha * 0.35;
1577
+ ctx.save();
1578
+ const innerScale = 0.85 + (rng ? rng() * 0.08 : 0);
1579
+ ctx.scale(innerScale, innerScale);
1580
+ // Lighten the fill for the inner area
1581
+ const origFill = ctx.fillStyle;
1582
+ if (typeof fillColor === "string") ctx.fillStyle = fillColor.replace(/[\d.]+\)$/, (m)=>{
1583
+ const v = parseFloat(m);
1584
+ return Math.min(1, v * 1.4).toFixed(2) + ")";
1585
+ });
1586
+ ctx.fill();
1587
+ ctx.fillStyle = origFill;
1588
+ ctx.restore();
1472
1589
  ctx.globalAlpha = savedAlpha;
1473
- // Light stroke on top
1474
- ctx.globalAlpha *= 0.4;
1590
+ // Soft stroke on top — thinner than normal for delicacy
1591
+ ctx.globalAlpha *= 0.25;
1592
+ ctx.lineWidth = strokeWidth * 0.6;
1475
1593
  ctx.stroke();
1476
- ctx.globalAlpha /= 0.4;
1594
+ ctx.globalAlpha /= 0.25;
1477
1595
  break;
1478
1596
  }
1479
1597
  case "hatched":
@@ -1595,6 +1713,675 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1595
1713
 
1596
1714
 
1597
1715
 
1716
+ /**
1717
+ * Shape affinity system — controls which shapes look good together,
1718
+ * quality tiers for different rendering contexts, and size preferences.
1719
+ *
1720
+ * This replaces the naive "pick any shape" approach with intentional
1721
+ * curation that produces more cohesive compositions.
1722
+ */ // ── Quality tiers ───────────────────────────────────────────────────
1723
+ // Not all shapes render equally well at all sizes or in all contexts.
1724
+ // Tier 1 shapes are visually strong at any size; Tier 3 shapes need
1725
+ // specific conditions to look good.
1726
+ const $8286059160ee2e04$export$4343b39fe47bd82c = {
1727
+ // ── Basic shapes ──────────────────────────────────────────────
1728
+ circle: {
1729
+ tier: 1,
1730
+ minSizeFraction: 0.05,
1731
+ maxSizeFraction: 1.0,
1732
+ affinities: [
1733
+ "circle",
1734
+ "blob",
1735
+ "hexagon",
1736
+ "flowerOfLife",
1737
+ "seedOfLife"
1738
+ ],
1739
+ category: "basic",
1740
+ heroCandidate: false,
1741
+ bestStyles: [
1742
+ "fill-only",
1743
+ "watercolor",
1744
+ "fill-and-stroke"
1745
+ ]
1746
+ },
1747
+ square: {
1748
+ tier: 2,
1749
+ minSizeFraction: 0.08,
1750
+ maxSizeFraction: 0.7,
1751
+ affinities: [
1752
+ "square",
1753
+ "diamond",
1754
+ "superellipse",
1755
+ "islamicPattern"
1756
+ ],
1757
+ category: "basic",
1758
+ heroCandidate: false,
1759
+ bestStyles: [
1760
+ "fill-and-stroke",
1761
+ "stroke-only",
1762
+ "hatched"
1763
+ ]
1764
+ },
1765
+ triangle: {
1766
+ tier: 1,
1767
+ minSizeFraction: 0.06,
1768
+ maxSizeFraction: 0.9,
1769
+ affinities: [
1770
+ "triangle",
1771
+ "diamond",
1772
+ "hexagon",
1773
+ "merkaba",
1774
+ "sriYantra"
1775
+ ],
1776
+ category: "basic",
1777
+ heroCandidate: false,
1778
+ bestStyles: [
1779
+ "fill-and-stroke",
1780
+ "fill-only",
1781
+ "watercolor"
1782
+ ]
1783
+ },
1784
+ hexagon: {
1785
+ tier: 1,
1786
+ minSizeFraction: 0.05,
1787
+ maxSizeFraction: 1.0,
1788
+ affinities: [
1789
+ "hexagon",
1790
+ "circle",
1791
+ "flowerOfLife",
1792
+ "metatronsCube",
1793
+ "triangle"
1794
+ ],
1795
+ category: "basic",
1796
+ heroCandidate: false,
1797
+ bestStyles: [
1798
+ "fill-only",
1799
+ "fill-and-stroke",
1800
+ "watercolor"
1801
+ ]
1802
+ },
1803
+ star: {
1804
+ tier: 2,
1805
+ minSizeFraction: 0.08,
1806
+ maxSizeFraction: 0.6,
1807
+ affinities: [
1808
+ "star",
1809
+ "circle",
1810
+ "mandala",
1811
+ "spirograph"
1812
+ ],
1813
+ category: "basic",
1814
+ heroCandidate: false,
1815
+ bestStyles: [
1816
+ "fill-and-stroke",
1817
+ "stroke-only",
1818
+ "dashed"
1819
+ ]
1820
+ },
1821
+ "jacked-star": {
1822
+ tier: 3,
1823
+ minSizeFraction: 0.1,
1824
+ maxSizeFraction: 0.4,
1825
+ affinities: [
1826
+ "star",
1827
+ "circle"
1828
+ ],
1829
+ category: "basic",
1830
+ heroCandidate: false,
1831
+ bestStyles: [
1832
+ "stroke-only",
1833
+ "dashed"
1834
+ ]
1835
+ },
1836
+ heart: {
1837
+ tier: 3,
1838
+ minSizeFraction: 0.1,
1839
+ maxSizeFraction: 0.5,
1840
+ affinities: [
1841
+ "circle",
1842
+ "blob"
1843
+ ],
1844
+ category: "basic",
1845
+ heroCandidate: false,
1846
+ bestStyles: [
1847
+ "fill-only",
1848
+ "watercolor"
1849
+ ]
1850
+ },
1851
+ diamond: {
1852
+ tier: 2,
1853
+ minSizeFraction: 0.06,
1854
+ maxSizeFraction: 0.8,
1855
+ affinities: [
1856
+ "diamond",
1857
+ "triangle",
1858
+ "square",
1859
+ "merkaba"
1860
+ ],
1861
+ category: "basic",
1862
+ heroCandidate: false,
1863
+ bestStyles: [
1864
+ "fill-and-stroke",
1865
+ "fill-only",
1866
+ "double-stroke"
1867
+ ]
1868
+ },
1869
+ cube: {
1870
+ tier: 3,
1871
+ minSizeFraction: 0.08,
1872
+ maxSizeFraction: 0.5,
1873
+ affinities: [
1874
+ "square",
1875
+ "diamond"
1876
+ ],
1877
+ category: "basic",
1878
+ heroCandidate: false,
1879
+ bestStyles: [
1880
+ "stroke-only",
1881
+ "fill-and-stroke"
1882
+ ]
1883
+ },
1884
+ // ── Complex shapes ────────────────────────────────────────────
1885
+ platonicSolid: {
1886
+ tier: 2,
1887
+ minSizeFraction: 0.15,
1888
+ maxSizeFraction: 0.8,
1889
+ affinities: [
1890
+ "metatronsCube",
1891
+ "merkaba",
1892
+ "hexagon",
1893
+ "triangle"
1894
+ ],
1895
+ category: "complex",
1896
+ heroCandidate: true,
1897
+ bestStyles: [
1898
+ "stroke-only",
1899
+ "double-stroke",
1900
+ "dashed"
1901
+ ]
1902
+ },
1903
+ fibonacciSpiral: {
1904
+ tier: 1,
1905
+ minSizeFraction: 0.2,
1906
+ maxSizeFraction: 1.0,
1907
+ affinities: [
1908
+ "circle",
1909
+ "rose",
1910
+ "spirograph",
1911
+ "flowerOfLife"
1912
+ ],
1913
+ category: "complex",
1914
+ heroCandidate: true,
1915
+ bestStyles: [
1916
+ "stroke-only",
1917
+ "incomplete",
1918
+ "watercolor"
1919
+ ]
1920
+ },
1921
+ islamicPattern: {
1922
+ tier: 2,
1923
+ minSizeFraction: 0.25,
1924
+ maxSizeFraction: 0.9,
1925
+ affinities: [
1926
+ "square",
1927
+ "hexagon",
1928
+ "star",
1929
+ "mandala"
1930
+ ],
1931
+ category: "complex",
1932
+ heroCandidate: true,
1933
+ bestStyles: [
1934
+ "stroke-only",
1935
+ "dashed",
1936
+ "hatched"
1937
+ ]
1938
+ },
1939
+ celticKnot: {
1940
+ tier: 2,
1941
+ minSizeFraction: 0.2,
1942
+ maxSizeFraction: 0.7,
1943
+ affinities: [
1944
+ "circle",
1945
+ "lissajous",
1946
+ "spirograph"
1947
+ ],
1948
+ category: "complex",
1949
+ heroCandidate: true,
1950
+ bestStyles: [
1951
+ "stroke-only",
1952
+ "double-stroke"
1953
+ ]
1954
+ },
1955
+ merkaba: {
1956
+ tier: 1,
1957
+ minSizeFraction: 0.15,
1958
+ maxSizeFraction: 1.0,
1959
+ affinities: [
1960
+ "triangle",
1961
+ "diamond",
1962
+ "sriYantra",
1963
+ "metatronsCube"
1964
+ ],
1965
+ category: "complex",
1966
+ heroCandidate: true,
1967
+ bestStyles: [
1968
+ "stroke-only",
1969
+ "fill-and-stroke",
1970
+ "double-stroke"
1971
+ ]
1972
+ },
1973
+ mandala: {
1974
+ tier: 1,
1975
+ minSizeFraction: 0.2,
1976
+ maxSizeFraction: 1.0,
1977
+ affinities: [
1978
+ "circle",
1979
+ "flowerOfLife",
1980
+ "spirograph",
1981
+ "rose"
1982
+ ],
1983
+ category: "complex",
1984
+ heroCandidate: true,
1985
+ bestStyles: [
1986
+ "stroke-only",
1987
+ "dashed",
1988
+ "incomplete"
1989
+ ]
1990
+ },
1991
+ fractal: {
1992
+ tier: 2,
1993
+ minSizeFraction: 0.2,
1994
+ maxSizeFraction: 0.8,
1995
+ affinities: [
1996
+ "blob",
1997
+ "lissajous",
1998
+ "circle"
1999
+ ],
2000
+ category: "complex",
2001
+ heroCandidate: true,
2002
+ bestStyles: [
2003
+ "stroke-only",
2004
+ "incomplete"
2005
+ ]
2006
+ },
2007
+ // ── Sacred shapes ─────────────────────────────────────────────
2008
+ flowerOfLife: {
2009
+ tier: 1,
2010
+ minSizeFraction: 0.2,
2011
+ maxSizeFraction: 1.0,
2012
+ affinities: [
2013
+ "circle",
2014
+ "hexagon",
2015
+ "seedOfLife",
2016
+ "eggOfLife",
2017
+ "metatronsCube"
2018
+ ],
2019
+ category: "sacred",
2020
+ heroCandidate: true,
2021
+ bestStyles: [
2022
+ "stroke-only",
2023
+ "watercolor",
2024
+ "incomplete"
2025
+ ]
2026
+ },
2027
+ treeOfLife: {
2028
+ tier: 2,
2029
+ minSizeFraction: 0.25,
2030
+ maxSizeFraction: 0.9,
2031
+ affinities: [
2032
+ "circle",
2033
+ "flowerOfLife",
2034
+ "metatronsCube"
2035
+ ],
2036
+ category: "sacred",
2037
+ heroCandidate: true,
2038
+ bestStyles: [
2039
+ "stroke-only",
2040
+ "double-stroke"
2041
+ ]
2042
+ },
2043
+ metatronsCube: {
2044
+ tier: 1,
2045
+ minSizeFraction: 0.2,
2046
+ maxSizeFraction: 1.0,
2047
+ affinities: [
2048
+ "hexagon",
2049
+ "flowerOfLife",
2050
+ "platonicSolid",
2051
+ "merkaba"
2052
+ ],
2053
+ category: "sacred",
2054
+ heroCandidate: true,
2055
+ bestStyles: [
2056
+ "stroke-only",
2057
+ "dashed",
2058
+ "incomplete"
2059
+ ]
2060
+ },
2061
+ sriYantra: {
2062
+ tier: 1,
2063
+ minSizeFraction: 0.2,
2064
+ maxSizeFraction: 1.0,
2065
+ affinities: [
2066
+ "triangle",
2067
+ "merkaba",
2068
+ "mandala",
2069
+ "diamond"
2070
+ ],
2071
+ category: "sacred",
2072
+ heroCandidate: true,
2073
+ bestStyles: [
2074
+ "stroke-only",
2075
+ "fill-and-stroke",
2076
+ "double-stroke"
2077
+ ]
2078
+ },
2079
+ seedOfLife: {
2080
+ tier: 1,
2081
+ minSizeFraction: 0.15,
2082
+ maxSizeFraction: 0.9,
2083
+ affinities: [
2084
+ "circle",
2085
+ "flowerOfLife",
2086
+ "eggOfLife",
2087
+ "hexagon"
2088
+ ],
2089
+ category: "sacred",
2090
+ heroCandidate: true,
2091
+ bestStyles: [
2092
+ "stroke-only",
2093
+ "watercolor",
2094
+ "fill-only"
2095
+ ]
2096
+ },
2097
+ vesicaPiscis: {
2098
+ tier: 2,
2099
+ minSizeFraction: 0.15,
2100
+ maxSizeFraction: 0.7,
2101
+ affinities: [
2102
+ "circle",
2103
+ "seedOfLife",
2104
+ "flowerOfLife"
2105
+ ],
2106
+ category: "sacred",
2107
+ heroCandidate: false,
2108
+ bestStyles: [
2109
+ "stroke-only",
2110
+ "watercolor"
2111
+ ]
2112
+ },
2113
+ torus: {
2114
+ tier: 3,
2115
+ minSizeFraction: 0.2,
2116
+ maxSizeFraction: 0.6,
2117
+ affinities: [
2118
+ "circle",
2119
+ "spirograph",
2120
+ "waveRing"
2121
+ ],
2122
+ category: "sacred",
2123
+ heroCandidate: false,
2124
+ bestStyles: [
2125
+ "stroke-only",
2126
+ "dashed"
2127
+ ]
2128
+ },
2129
+ eggOfLife: {
2130
+ tier: 2,
2131
+ minSizeFraction: 0.15,
2132
+ maxSizeFraction: 0.8,
2133
+ affinities: [
2134
+ "circle",
2135
+ "seedOfLife",
2136
+ "flowerOfLife"
2137
+ ],
2138
+ category: "sacred",
2139
+ heroCandidate: true,
2140
+ bestStyles: [
2141
+ "stroke-only",
2142
+ "watercolor"
2143
+ ]
2144
+ },
2145
+ // ── Procedural shapes ─────────────────────────────────────────
2146
+ blob: {
2147
+ tier: 1,
2148
+ minSizeFraction: 0.05,
2149
+ maxSizeFraction: 1.0,
2150
+ affinities: [
2151
+ "blob",
2152
+ "circle",
2153
+ "superellipse",
2154
+ "waveRing"
2155
+ ],
2156
+ category: "procedural",
2157
+ heroCandidate: false,
2158
+ bestStyles: [
2159
+ "fill-only",
2160
+ "watercolor",
2161
+ "fill-and-stroke"
2162
+ ]
2163
+ },
2164
+ ngon: {
2165
+ tier: 2,
2166
+ minSizeFraction: 0.06,
2167
+ maxSizeFraction: 0.8,
2168
+ affinities: [
2169
+ "hexagon",
2170
+ "triangle",
2171
+ "diamond",
2172
+ "superellipse"
2173
+ ],
2174
+ category: "procedural",
2175
+ heroCandidate: false,
2176
+ bestStyles: [
2177
+ "fill-and-stroke",
2178
+ "fill-only",
2179
+ "hatched"
2180
+ ]
2181
+ },
2182
+ lissajous: {
2183
+ tier: 2,
2184
+ minSizeFraction: 0.15,
2185
+ maxSizeFraction: 0.8,
2186
+ affinities: [
2187
+ "spirograph",
2188
+ "rose",
2189
+ "celticKnot",
2190
+ "fibonacciSpiral"
2191
+ ],
2192
+ category: "procedural",
2193
+ heroCandidate: false,
2194
+ bestStyles: [
2195
+ "stroke-only",
2196
+ "incomplete",
2197
+ "dashed"
2198
+ ]
2199
+ },
2200
+ superellipse: {
2201
+ tier: 1,
2202
+ minSizeFraction: 0.05,
2203
+ maxSizeFraction: 1.0,
2204
+ affinities: [
2205
+ "circle",
2206
+ "square",
2207
+ "blob",
2208
+ "hexagon"
2209
+ ],
2210
+ category: "procedural",
2211
+ heroCandidate: false,
2212
+ bestStyles: [
2213
+ "fill-only",
2214
+ "watercolor",
2215
+ "fill-and-stroke"
2216
+ ]
2217
+ },
2218
+ spirograph: {
2219
+ tier: 1,
2220
+ minSizeFraction: 0.15,
2221
+ maxSizeFraction: 0.9,
2222
+ affinities: [
2223
+ "rose",
2224
+ "lissajous",
2225
+ "mandala",
2226
+ "flowerOfLife"
2227
+ ],
2228
+ category: "procedural",
2229
+ heroCandidate: true,
2230
+ bestStyles: [
2231
+ "stroke-only",
2232
+ "incomplete",
2233
+ "dashed"
2234
+ ]
2235
+ },
2236
+ waveRing: {
2237
+ tier: 2,
2238
+ minSizeFraction: 0.1,
2239
+ maxSizeFraction: 0.8,
2240
+ affinities: [
2241
+ "circle",
2242
+ "blob",
2243
+ "torus",
2244
+ "spirograph"
2245
+ ],
2246
+ category: "procedural",
2247
+ heroCandidate: false,
2248
+ bestStyles: [
2249
+ "stroke-only",
2250
+ "dashed",
2251
+ "incomplete"
2252
+ ]
2253
+ },
2254
+ rose: {
2255
+ tier: 1,
2256
+ minSizeFraction: 0.1,
2257
+ maxSizeFraction: 0.9,
2258
+ affinities: [
2259
+ "spirograph",
2260
+ "mandala",
2261
+ "flowerOfLife",
2262
+ "circle"
2263
+ ],
2264
+ category: "procedural",
2265
+ heroCandidate: true,
2266
+ bestStyles: [
2267
+ "stroke-only",
2268
+ "fill-only",
2269
+ "watercolor"
2270
+ ]
2271
+ }
2272
+ };
2273
+ function $8286059160ee2e04$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
2274
+ const available = shapeNames.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]);
2275
+ // Pick a seed shape — tier 1 shapes that are hero candidates
2276
+ const heroPool = available.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s].tier === 1 && $8286059160ee2e04$export$4343b39fe47bd82c[s].heroCandidate);
2277
+ const seedShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : available[Math.floor(rng() * available.length)];
2278
+ const seedProfile = $8286059160ee2e04$export$4343b39fe47bd82c[seedShape];
2279
+ // Primary: seed shape + its direct affinities (tier 1-2 only)
2280
+ const primaryCandidates = [
2281
+ seedShape,
2282
+ ...seedProfile.affinities
2283
+ ].filter((s)=>available.includes(s)).filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s].tier <= 2);
2284
+ const primary = [
2285
+ ...new Set(primaryCandidates)
2286
+ ].slice(0, 5);
2287
+ // Supporting: affinities of affinities, plus same-category shapes
2288
+ const supportingSet = new Set();
2289
+ for (const p of primary){
2290
+ const profile = $8286059160ee2e04$export$4343b39fe47bd82c[p];
2291
+ if (!profile) continue;
2292
+ for (const aff of profile.affinities)if (available.includes(aff) && !primary.includes(aff)) supportingSet.add(aff);
2293
+ }
2294
+ // Add same-category tier 1-2 shapes
2295
+ for (const s of available){
2296
+ const p = $8286059160ee2e04$export$4343b39fe47bd82c[s];
2297
+ if (p.category === seedProfile.category && p.tier <= 2 && !primary.includes(s)) supportingSet.add(s);
2298
+ }
2299
+ const supporting = [
2300
+ ...supportingSet
2301
+ ].slice(0, 6);
2302
+ // Accents: tier 1 shapes from other categories for contrast
2303
+ const usedCategories = new Set([
2304
+ ...primary,
2305
+ ...supporting
2306
+ ].map((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]?.category));
2307
+ const accentCandidates = available.filter((s)=>!primary.includes(s) && !supporting.includes(s) && $8286059160ee2e04$export$4343b39fe47bd82c[s].tier <= 2 && !usedCategories.has($8286059160ee2e04$export$4343b39fe47bd82c[s].category));
2308
+ // Shuffle and take a few
2309
+ const accents = [];
2310
+ const shuffled = [
2311
+ ...accentCandidates
2312
+ ];
2313
+ for(let i = shuffled.length - 1; i > 0; i--){
2314
+ const j = Math.floor(rng() * (i + 1));
2315
+ [shuffled[i], shuffled[j]] = [
2316
+ shuffled[j],
2317
+ shuffled[i]
2318
+ ];
2319
+ }
2320
+ accents.push(...shuffled.slice(0, 3));
2321
+ // For certain archetypes, bias the palette
2322
+ if (archetypeName === "geometric-precision") // Remove blobs and organic shapes from primary
2323
+ return {
2324
+ primary: primary.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]?.category !== "procedural" || s === "ngon"),
2325
+ supporting: supporting.filter((s)=>s !== "blob"),
2326
+ accents: accents
2327
+ };
2328
+ if (archetypeName === "organic-flow") {
2329
+ // Boost procedural/organic shapes
2330
+ const organicBoost = available.filter((s)=>[
2331
+ "blob",
2332
+ "superellipse",
2333
+ "waveRing",
2334
+ "rose"
2335
+ ].includes(s) && !primary.includes(s));
2336
+ return {
2337
+ primary: [
2338
+ ...primary,
2339
+ ...organicBoost.slice(0, 2)
2340
+ ],
2341
+ supporting: supporting,
2342
+ accents: accents
2343
+ };
2344
+ }
2345
+ return {
2346
+ primary: primary,
2347
+ supporting: supporting,
2348
+ accents: accents
2349
+ };
2350
+ }
2351
+ function $8286059160ee2e04$export$3c37d9a045754d0e(palette, rng, sizeFraction) {
2352
+ // Filter each tier by size constraints
2353
+ const validPrimary = palette.primary.filter((s)=>{
2354
+ const p = $8286059160ee2e04$export$4343b39fe47bd82c[s];
2355
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2356
+ });
2357
+ const validSupporting = palette.supporting.filter((s)=>{
2358
+ const p = $8286059160ee2e04$export$4343b39fe47bd82c[s];
2359
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2360
+ });
2361
+ const validAccents = palette.accents.filter((s)=>{
2362
+ const p = $8286059160ee2e04$export$4343b39fe47bd82c[s];
2363
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2364
+ });
2365
+ const roll = rng();
2366
+ if (roll < 0.60 && validPrimary.length > 0) return validPrimary[Math.floor(rng() * validPrimary.length)];
2367
+ if (roll < 0.90 && validSupporting.length > 0) return validSupporting[Math.floor(rng() * validSupporting.length)];
2368
+ if (validAccents.length > 0) return validAccents[Math.floor(rng() * validAccents.length)];
2369
+ // Fallback: any valid primary or supporting
2370
+ const fallback = [
2371
+ ...validPrimary,
2372
+ ...validSupporting
2373
+ ];
2374
+ if (fallback.length > 0) return fallback[Math.floor(rng() * fallback.length)];
2375
+ // Ultimate fallback
2376
+ return palette.primary[0] || "circle";
2377
+ }
2378
+ function $8286059160ee2e04$export$ab873bb6fb56c1a8(shapeName, layerStyle, rng) {
2379
+ const profile = $8286059160ee2e04$export$4343b39fe47bd82c[shapeName];
2380
+ if (!profile || rng() > 0.7) return layerStyle;
2381
+ return profile.bestStyles[Math.floor(rng() * profile.bestStyles.length)];
2382
+ }
2383
+
2384
+
1598
2385
 
1599
2386
  /**
1600
2387
  * Configuration options for image generation.
@@ -1809,6 +2596,69 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
1809
2596
  sizePower: 2.5,
1810
2597
  invertForeground: false
1811
2598
  },
2599
+ {
2600
+ name: "watercolor-wash",
2601
+ gridSize: 3,
2602
+ layers: 3,
2603
+ baseOpacity: 0.25,
2604
+ opacityReduction: 0.03,
2605
+ minShapeSize: 200,
2606
+ maxShapeSize: 700,
2607
+ backgroundStyle: "radial-light",
2608
+ paletteMode: "harmonious",
2609
+ preferredStyles: [
2610
+ "watercolor",
2611
+ "fill-only",
2612
+ "incomplete"
2613
+ ],
2614
+ flowLineMultiplier: 0.5,
2615
+ heroShape: false,
2616
+ glowMultiplier: 0.3,
2617
+ sizePower: 0.6,
2618
+ invertForeground: false
2619
+ },
2620
+ {
2621
+ name: "op-art",
2622
+ gridSize: 8,
2623
+ layers: 2,
2624
+ baseOpacity: 0.95,
2625
+ opacityReduction: 0.05,
2626
+ minShapeSize: 20,
2627
+ maxShapeSize: 200,
2628
+ backgroundStyle: "solid-light",
2629
+ paletteMode: "high-contrast",
2630
+ preferredStyles: [
2631
+ "fill-and-stroke",
2632
+ "stroke-only",
2633
+ "dashed"
2634
+ ],
2635
+ flowLineMultiplier: 0,
2636
+ heroShape: false,
2637
+ glowMultiplier: 0,
2638
+ sizePower: 0.4,
2639
+ invertForeground: false
2640
+ },
2641
+ {
2642
+ name: "collage",
2643
+ gridSize: 4,
2644
+ layers: 3,
2645
+ baseOpacity: 0.9,
2646
+ opacityReduction: 0.08,
2647
+ minShapeSize: 80,
2648
+ maxShapeSize: 500,
2649
+ backgroundStyle: "solid-light",
2650
+ paletteMode: "duotone",
2651
+ preferredStyles: [
2652
+ "fill-and-stroke",
2653
+ "fill-only",
2654
+ "double-stroke"
2655
+ ],
2656
+ flowLineMultiplier: 0,
2657
+ heroShape: true,
2658
+ glowMultiplier: 0,
2659
+ sizePower: 0.7,
2660
+ invertForeground: false
2661
+ },
1812
2662
  {
1813
2663
  name: "classic",
1814
2664
  gridSize: 5,
@@ -1836,26 +2686,7 @@ function $68a238ccd77f2bcd$export$f1142fd7da4d6590(rng) {
1836
2686
  }
1837
2687
 
1838
2688
 
1839
- // ── Shape categories for weighted selection ─────────────────────────
1840
- const $1f63dc64b5593c73$var$BASIC_SHAPES = [
1841
- "circle",
1842
- "square",
1843
- "triangle",
1844
- "hexagon",
1845
- "diamond",
1846
- "cube"
1847
- ];
1848
- const $1f63dc64b5593c73$var$COMPLEX_SHAPES = [
1849
- "star",
1850
- "jacked-star",
1851
- "heart",
1852
- "platonicSolid",
1853
- "fibonacciSpiral",
1854
- "islamicPattern",
1855
- "celticKnot",
1856
- "merkaba",
1857
- "fractal"
1858
- ];
2689
+ // ── Shape categories for weighted selection (legacy fallback) ───────
1859
2690
  const $1f63dc64b5593c73$var$SACRED_SHAPES = [
1860
2691
  "mandala",
1861
2692
  "flowerOfLife",
@@ -1867,15 +2698,6 @@ const $1f63dc64b5593c73$var$SACRED_SHAPES = [
1867
2698
  "torus",
1868
2699
  "eggOfLife"
1869
2700
  ];
1870
- const $1f63dc64b5593c73$var$PROCEDURAL_SHAPES = [
1871
- "blob",
1872
- "ngon",
1873
- "lissajous",
1874
- "superellipse",
1875
- "spirograph",
1876
- "waveRing",
1877
- "rose"
1878
- ];
1879
2701
  const $1f63dc64b5593c73$var$COMPOSITION_MODES = [
1880
2702
  "radial",
1881
2703
  "flow-field",
@@ -1883,23 +2705,6 @@ const $1f63dc64b5593c73$var$COMPOSITION_MODES = [
1883
2705
  "grid-subdivision",
1884
2706
  "clustered"
1885
2707
  ];
1886
- // ── Helper: pick shape with layer-aware weighting ───────────────────
1887
- function $1f63dc64b5593c73$var$pickShape(rng, layerRatio, shapeNames) {
1888
- const basicW = 1 - layerRatio * 0.6;
1889
- const complexW = 0.3 + layerRatio * 0.3;
1890
- const sacredW = 0.1 + layerRatio * 0.4;
1891
- const proceduralW = 0.25 + layerRatio * 0.2; // always present, grows with depth
1892
- const total = basicW + complexW + sacredW + proceduralW;
1893
- const roll = rng() * total;
1894
- let pool;
1895
- if (roll < basicW) pool = $1f63dc64b5593c73$var$BASIC_SHAPES;
1896
- else if (roll < basicW + complexW) pool = $1f63dc64b5593c73$var$COMPLEX_SHAPES;
1897
- else if (roll < basicW + complexW + sacredW) pool = $1f63dc64b5593c73$var$SACRED_SHAPES;
1898
- else pool = $1f63dc64b5593c73$var$PROCEDURAL_SHAPES;
1899
- const available = pool.filter((s)=>shapeNames.includes(s));
1900
- if (available.length === 0) return shapeNames[Math.floor(rng() * shapeNames.length)];
1901
- return available[Math.floor(rng() * available.length)];
1902
- }
1903
2708
  // ── Helper: get position based on composition mode ──────────────────
1904
2709
  function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
1905
2710
  switch(mode){
@@ -1958,22 +2763,28 @@ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height,
1958
2763
  };
1959
2764
  }
1960
2765
  }
1961
- // ── Helper: positional color blending ───────────────────────────────
1962
- function $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng) {
1963
- const nx = x / width;
1964
- const ny = y / height;
1965
- const posIndex = (nx * 0.6 + ny * 0.4) * (colors.length - 1);
1966
- const baseIdx = Math.floor(posIndex) % colors.length;
1967
- return (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[baseIdx], rng, 0.08);
2766
+ // ── Helper: positional color from hierarchy ─────────────────────────
2767
+ function $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, hierarchy, rng) {
2768
+ // Blend position into color selection — shapes near center lean dominant
2769
+ const distFromCenter = Math.hypot(x - width / 2, y - height / 2) / Math.hypot(width / 2, height / 2);
2770
+ // Center = more dominant, edges = more accent
2771
+ if (distFromCenter < 0.35) return (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(hierarchy.dominant, rng, 10, 0.08);
2772
+ else if (distFromCenter < 0.7) return (0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(hierarchy, rng), rng, 8, 0.06);
2773
+ else {
2774
+ // Edges: bias toward secondary/accent
2775
+ const roll = rng();
2776
+ const color = roll < 0.4 ? hierarchy.secondary : roll < 0.75 ? hierarchy.accent : hierarchy.dominant;
2777
+ return (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(color, rng, 12, 0.08);
2778
+ }
1968
2779
  }
1969
- // ── Helper: check if a position is inside a void zone (Feature E) ───
2780
+ // ── Helper: check if a position is inside a void zone ───────────────
1970
2781
  function $1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones) {
1971
2782
  for (const zone of voidZones){
1972
2783
  if (Math.hypot(x - zone.x, y - zone.y) < zone.radius) return true;
1973
2784
  }
1974
2785
  return false;
1975
2786
  }
1976
- // ── Helper: density check for negative space (Feature E) ────────────
2787
+ // ── Helper: density check ───────────────────────────────────────────
1977
2788
  function $1f63dc64b5593c73$var$localDensity(x, y, positions, radius) {
1978
2789
  let count = 0;
1979
2790
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -2069,7 +2880,15 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2069
2880
  const [bgStart, bgEnd] = colorScheme.getBackgroundColorsByMode(archetype.paletteMode);
2070
2881
  const tempMode = colorScheme.getTemperatureMode();
2071
2882
  const fgTempTarget = tempMode === "warm-bg" ? "cool" : tempMode === "cool-bg" ? "warm" : null;
2883
+ // ── 0b. Color hierarchy — dominant/secondary/accent weighting ──
2884
+ const colorHierarchy = (0, $b5a262d09b87e373$export$fabac4600b87056)(colors, rng);
2885
+ // ── 0c. Shape palette — curated shapes that work well together ──
2072
2886
  const shapeNames = Object.keys((0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5));
2887
+ const shapePalette = (0, $8286059160ee2e04$export$4a95df8944b5033b)(rng, shapeNames, archetype.name);
2888
+ // ── 0d. Color grading — unified tone for the whole image ───────
2889
+ const colorGrade = (0, $b5a262d09b87e373$export$6d1620b367f86f7a)(rng);
2890
+ // ── 0e. Light direction — consistent shadow angle ──────────────
2891
+ const lightAngle = rng() * Math.PI * 2;
2073
2892
  const scaleFactor = Math.min(width, height) / 1024;
2074
2893
  const adjustedMinSize = minShapeSize * scaleFactor;
2075
2894
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -2078,27 +2897,45 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2078
2897
  // ── 1. Background ──────────────────────────────────────────────
2079
2898
  const bgRadius = Math.hypot(cx, cy);
2080
2899
  $1f63dc64b5593c73$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
2900
+ // Gradient mesh overlay — 3-4 color control points for richer backgrounds
2901
+ const meshPoints = 3 + Math.floor(rng() * 2);
2902
+ ctx.globalCompositeOperation = "soft-light";
2903
+ for(let i = 0; i < meshPoints; i++){
2904
+ const mx = rng() * width;
2905
+ const my = rng() * height;
2906
+ const mRadius = Math.min(width, height) * (0.3 + rng() * 0.4);
2907
+ const mColor = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng);
2908
+ const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
2909
+ grad.addColorStop(0, (0, $b5a262d09b87e373$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
2910
+ grad.addColorStop(1, "rgba(0,0,0,0)");
2911
+ ctx.globalAlpha = 1;
2912
+ ctx.fillStyle = grad;
2913
+ ctx.fillRect(0, 0, width, height);
2914
+ }
2915
+ ctx.globalCompositeOperation = "source-over";
2081
2916
  // Compute average background luminance for contrast enforcement
2082
2917
  const bgLum = ((0, $b5a262d09b87e373$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $b5a262d09b87e373$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
2083
- // ── 1b. Layered background (Feature G) ─────────────────────────
2084
- // Draw large, very faint shapes to give the background texture
2918
+ // ── 1b. Layered background archetype-coherent shapes ─────────
2085
2919
  const bgShapeCount = 3 + Math.floor(rng() * 4);
2086
2920
  ctx.globalCompositeOperation = "soft-light";
2087
2921
  for(let i = 0; i < bgShapeCount; i++){
2088
2922
  const bx = rng() * width;
2089
2923
  const by = rng() * height;
2090
2924
  const bSize = width * 0.3 + rng() * width * 0.5;
2091
- const bColor = colors[Math.floor(rng() * colors.length)];
2925
+ const bColor = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng);
2092
2926
  ctx.globalAlpha = 0.03 + rng() * 0.05;
2093
2927
  ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(bColor, 0.15);
2094
2928
  ctx.beginPath();
2095
- ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
2929
+ // Use archetype-appropriate background shapes
2930
+ if (archetype.name === "geometric-precision" || archetype.name === "op-art") // Rectangular shapes for geometric archetypes
2931
+ ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
2932
+ else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
2096
2933
  ctx.fill();
2097
2934
  }
2098
2935
  // Subtle concentric rings from center
2099
2936
  const ringCount = 2 + Math.floor(rng() * 3);
2100
2937
  ctx.globalAlpha = 0.02 + rng() * 0.03;
2101
- ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colors[0], 0.1);
2938
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
2102
2939
  ctx.lineWidth = 1 * scaleFactor;
2103
2940
  for(let i = 1; i <= ringCount; i++){
2104
2941
  const r = Math.min(width, height) * 0.15 * i;
@@ -2112,7 +2949,6 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2112
2949
  const symRoll = rng();
2113
2950
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
2114
2951
  // ── 3. Focal points + void zones ───────────────────────────────
2115
- // Rule-of-thirds intersection points for intentional composition
2116
2952
  const THIRDS_POINTS = [
2117
2953
  {
2118
2954
  x: 1 / 3,
@@ -2133,10 +2969,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2133
2969
  ];
2134
2970
  const numFocal = 1 + Math.floor(rng() * 2);
2135
2971
  const focalPoints = [];
2136
- for(let f = 0; f < numFocal; f++)// 70% chance to snap to a rule-of-thirds point, 30% free placement
2137
- if (rng() < 0.7) {
2972
+ for(let f = 0; f < numFocal; f++)if (rng() < 0.7) {
2138
2973
  const tp = THIRDS_POINTS[Math.floor(rng() * THIRDS_POINTS.length)];
2139
- // Small jitter around the thirds point so it's not robotic
2140
2974
  focalPoints.push({
2141
2975
  x: width * (tp.x + (rng() - 0.5) * 0.08),
2142
2976
  y: height * (tp.y + (rng() - 0.5) * 0.08),
@@ -2147,7 +2981,6 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2147
2981
  y: height * (0.2 + rng() * 0.6),
2148
2982
  strength: 0.3 + rng() * 0.4
2149
2983
  });
2150
- // Feature E: 1-2 void zones where shapes are sparse (negative space)
2151
2984
  const numVoids = Math.floor(rng() * 2) + 1;
2152
2985
  const voidZones = [];
2153
2986
  for(let v = 0; v < numVoids; v++)voidZones.push({
@@ -2179,20 +3012,24 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2179
3012
  }
2180
3013
  // Track all placed shapes for density checks and connecting curves
2181
3014
  const shapePositions = [];
3015
+ // Hero avoidance radius — shapes near the hero orient toward it
3016
+ let heroCenter = null;
2182
3017
  // ── 4b. Hero shape — a dominant focal element ───────────────────
2183
3018
  if (archetype.heroShape && rng() < 0.6) {
2184
3019
  const heroFocal = focalPoints[0];
3020
+ // Use shape palette hero candidates
2185
3021
  const heroPool = [
2186
- ...$1f63dc64b5593c73$var$SACRED_SHAPES,
2187
- "fibonacciSpiral",
2188
- "merkaba",
2189
- "fractal"
2190
- ];
2191
- const heroShape = heroPool.filter((s)=>shapeNames.includes(s))[Math.floor(rng() * heroPool.filter((s)=>shapeNames.includes(s)).length)] || shapeNames[Math.floor(rng() * shapeNames.length)];
3022
+ ...shapePalette.primary,
3023
+ ...shapePalette.supporting
3024
+ ].filter((s)=>(0, $8286059160ee2e04$export$4343b39fe47bd82c)[s]?.heroCandidate && shapeNames.includes(s));
3025
+ const heroShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : shapeNames[Math.floor(rng() * shapeNames.length)];
2192
3026
  const heroSize = adjustedMaxSize * (0.8 + rng() * 0.5);
2193
3027
  const heroRotation = rng() * 360;
2194
- const heroFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05), bgLum), 0.15 + rng() * 0.2);
2195
- const heroStroke = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05), bgLum);
3028
+ const heroFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)(colorHierarchy.dominant, rng, 6, 0.05), bgLum), 0.15 + rng() * 0.2);
3029
+ const heroStroke = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)(colorHierarchy.accent, rng, 6, 0.05), bgLum);
3030
+ // Get best style for this hero shape
3031
+ const heroProfile = (0, $8286059160ee2e04$export$4343b39fe47bd82c)[heroShape];
3032
+ const heroStyle = heroProfile ? heroProfile.bestStyles[Math.floor(rng() * heroProfile.bestStyles.length)] : rng() < 0.4 ? "watercolor" : "fill-and-stroke";
2196
3033
  ctx.globalAlpha = 0.5 + rng() * 0.2;
2197
3034
  (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, heroShape, heroFocal.x, heroFocal.y, {
2198
3035
  fillColor: heroFill,
@@ -2203,14 +3040,20 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2203
3040
  proportionType: "GOLDEN_RATIO",
2204
3041
  glowRadius: (12 + rng() * 20) * scaleFactor,
2205
3042
  glowColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(heroStroke, 0.4),
2206
- gradientFillEnd: (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1),
2207
- renderStyle: rng() < 0.4 ? "watercolor" : "fill-and-stroke",
3043
+ gradientFillEnd: (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
3044
+ renderStyle: heroStyle,
2208
3045
  rng: rng
2209
3046
  });
2210
- shapePositions.push({
3047
+ heroCenter = {
2211
3048
  x: heroFocal.x,
2212
3049
  y: heroFocal.y,
2213
3050
  size: heroSize
3051
+ };
3052
+ shapePositions.push({
3053
+ x: heroFocal.x,
3054
+ y: heroFocal.y,
3055
+ size: heroSize,
3056
+ shape: heroShape
2214
3057
  });
2215
3058
  }
2216
3059
  // ── 5. Shape layers ────────────────────────────────────────────
@@ -2221,41 +3064,52 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2221
3064
  const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
2222
3065
  const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
2223
3066
  const layerSizeScale = 1 - layer * 0.15;
2224
- // Feature B: per-layer blend mode
3067
+ // Per-layer blend mode
2225
3068
  const layerBlend = (0, $e0f99502ff383dd8$export$7bb7bff4e26fa06b)(rng);
2226
3069
  ctx.globalCompositeOperation = layerBlend;
2227
- // Feature C: per-layer render style bias — prefer archetype styles
3070
+ // Per-layer render style bias — prefer archetype styles
2228
3071
  const layerRenderStyle = rng() < 0.6 ? archetype.preferredStyles[Math.floor(rng() * archetype.preferredStyles.length)] : (0, $e0f99502ff383dd8$export$9fd4e64b2acd410e)(rng);
2229
- // Feature D: atmospheric desaturation for later layers
2230
- const atmosphericDesat = layerRatio * 0.3; // 0 for first layer, up to 0.3 for last
3072
+ // Atmospheric desaturation for later layers
3073
+ const atmosphericDesat = layerRatio * 0.3;
2231
3074
  for(let i = 0; i < numShapes; i++){
2232
3075
  // Position from composition mode, then focal bias
2233
3076
  const rawPos = $1f63dc64b5593c73$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
2234
3077
  const [x, y] = applyFocalBias(rawPos.x, rawPos.y);
2235
- // Feature E: skip shapes in void zones, reduce in dense areas
3078
+ // Skip shapes in void zones, reduce in dense areas
2236
3079
  if ($1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones)) {
2237
- // 85% chance to skip — allows a few shapes to bleed in
2238
3080
  if (rng() < 0.85) continue;
2239
3081
  }
2240
3082
  if ($1f63dc64b5593c73$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
2241
- if (rng() < 0.6) continue; // thin out dense areas
3083
+ if (rng() < 0.6) continue;
2242
3084
  }
2243
- // Weighted shape selection
2244
- const shape = $1f63dc64b5593c73$var$pickShape(rng, layerRatio, shapeNames);
2245
3085
  // Power distribution for size — archetype controls the curve
2246
3086
  const sizeT = Math.pow(rng(), archetype.sizePower);
2247
3087
  const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
3088
+ // Size fraction for affinity-aware shape selection
3089
+ const sizeFraction = size / adjustedMaxSize;
3090
+ // Palette-driven shape selection (replaces naive pickShape)
3091
+ const shape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, sizeFraction);
2248
3092
  // Flow-field rotation in flow-field mode, random otherwise
2249
- const rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
2250
- // Positional color blending + jitter
2251
- let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng);
2252
- const strokeBase = colors[Math.floor(rng() * colors.length)];
2253
- // Feature D: desaturate colors on later layers for depth
3093
+ let rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
3094
+ // Hero avoidance: shapes near the hero orient toward it
3095
+ if (heroCenter) {
3096
+ const distToHero = Math.hypot(x - heroCenter.x, y - heroCenter.y);
3097
+ const heroInfluence = heroCenter.size * 1.5;
3098
+ if (distToHero < heroInfluence && distToHero > 0) {
3099
+ const angleToHero = Math.atan2(heroCenter.y - y, heroCenter.x - x) * 180 / Math.PI;
3100
+ const blendFactor = 1 - distToHero / heroInfluence;
3101
+ rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
3102
+ }
3103
+ }
3104
+ // Positional color from hierarchy + jitter
3105
+ let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
3106
+ const strokeBase = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng);
3107
+ // Desaturate colors on later layers for depth
2254
3108
  if (atmosphericDesat > 0) fillBase = (0, $b5a262d09b87e373$export$fb75607d98509d9)(fillBase, atmosphericDesat);
2255
3109
  // Temperature contrast: shift foreground shapes opposite to background
2256
3110
  if (fgTempTarget) fillBase = (0, $b5a262d09b87e373$export$51ea55f869b7e0d3)(fillBase, fgTempTarget, 0.15 + layerRatio * 0.1);
2257
- const fillColor = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$59539d800dbe6858)(fillBase, rng, 0.06), bgLum);
2258
- const strokeColor = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$59539d800dbe6858)(strokeBase, rng, 0.05), bgLum);
3111
+ const fillColor = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)(fillBase, rng, 6, 0.05), bgLum);
3112
+ const strokeColor = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
2259
3113
  // Semi-transparent fill
2260
3114
  const fillAlpha = 0.2 + rng() * 0.5;
2261
3115
  const transparentFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, fillAlpha);
@@ -2269,12 +3123,16 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2269
3123
  const glowRadius = hasGlow ? (8 + rng() * 20) * scaleFactor : 0;
2270
3124
  // Gradient fill on ~30%
2271
3125
  const hasGradient = rng() < 0.3;
2272
- const gradientEnd = hasGradient ? (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1) : undefined;
2273
- // Feature C: per-shape render style (70% use layer style, 30% pick their own)
2274
- const shapeRenderStyle = rng() < 0.7 ? layerRenderStyle : (0, $e0f99502ff383dd8$export$9fd4e64b2acd410e)(rng);
2275
- // Feature F: organic edge jitter — applied via watercolor style on ~15% of shapes
3126
+ const gradientEnd = hasGradient ? (0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1) : undefined;
3127
+ // Affinity-aware render style selection
3128
+ const shapeRenderStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
3129
+ // Organic edge jitter — applied via watercolor style on ~15% of shapes
2276
3130
  const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
2277
3131
  const finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
3132
+ // Consistent light direction — subtle shadow offset
3133
+ const shadowDist = hasGlow ? 0 : size * 0.02;
3134
+ const shadowOffX = shadowDist * Math.cos(lightAngle);
3135
+ const shadowOffY = shadowDist * Math.sin(lightAngle);
2278
3136
  (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
2279
3137
  fillColor: transparentFill,
2280
3138
  strokeColor: strokeColor,
@@ -2282,8 +3140,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2282
3140
  size: size,
2283
3141
  rotation: rotation,
2284
3142
  proportionType: "GOLDEN_RATIO",
2285
- glowRadius: glowRadius,
2286
- glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : undefined,
3143
+ glowRadius: glowRadius || (shadowDist > 0 ? shadowDist * 2 : 0),
3144
+ glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
2287
3145
  gradientFillEnd: gradientEnd,
2288
3146
  renderStyle: finalRenderStyle,
2289
3147
  rng: rng
@@ -2291,18 +3149,51 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2291
3149
  shapePositions.push({
2292
3150
  x: x,
2293
3151
  y: y,
2294
- size: size
3152
+ size: size,
3153
+ shape: shape
2295
3154
  });
2296
- // ── 5b. Recursive nesting ──────────────────────────────────
3155
+ // ── 5b. Size echo — large shapes spawn trailing smaller copies ──
3156
+ if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
3157
+ const echoCount = 2 + Math.floor(rng() * 2);
3158
+ const echoAngle = rng() * Math.PI * 2;
3159
+ for(let e = 0; e < echoCount; e++){
3160
+ const echoScale = 0.3 - e * 0.08;
3161
+ const echoDist = size * (0.6 + e * 0.4);
3162
+ const echoX = x + Math.cos(echoAngle) * echoDist;
3163
+ const echoY = y + Math.sin(echoAngle) * echoDist;
3164
+ const echoSize = size * Math.max(0.1, echoScale);
3165
+ if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
3166
+ ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
3167
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
3168
+ fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
3169
+ strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.4),
3170
+ strokeWidth: strokeWidth * 0.6,
3171
+ size: echoSize,
3172
+ rotation: rotation + (e + 1) * 15,
3173
+ proportionType: "GOLDEN_RATIO",
3174
+ renderStyle: finalRenderStyle,
3175
+ rng: rng
3176
+ });
3177
+ shapePositions.push({
3178
+ x: echoX,
3179
+ y: echoY,
3180
+ size: echoSize,
3181
+ shape: shape
3182
+ });
3183
+ }
3184
+ }
3185
+ // ── 5c. Recursive nesting ──────────────────────────────────
2297
3186
  if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
2298
3187
  const innerCount = 1 + Math.floor(rng() * 3);
2299
3188
  for(let n = 0; n < innerCount; n++){
2300
- const innerShape = $1f63dc64b5593c73$var$pickShape(rng, Math.min(1, layerRatio + 0.3), shapeNames);
3189
+ // Pick inner shape from palette affinities
3190
+ const innerSizeFraction = size * 0.25 / adjustedMaxSize;
3191
+ const innerShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
2301
3192
  const innerSize = size * (0.15 + rng() * 0.25);
2302
3193
  const innerOffX = (rng() - 0.5) * size * 0.4;
2303
3194
  const innerOffY = (rng() - 0.5) * size * 0.4;
2304
3195
  const innerRot = rng() * 360;
2305
- const innerFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1), 0.3 + rng() * 0.4);
3196
+ const innerFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
2306
3197
  ctx.globalAlpha = layerOpacity * 0.7;
2307
3198
  (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, innerShape, x + innerOffX, y + innerOffY, {
2308
3199
  fillColor: innerFill,
@@ -2311,7 +3202,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2311
3202
  size: innerSize,
2312
3203
  rotation: innerRot,
2313
3204
  proportionType: "GOLDEN_RATIO",
2314
- renderStyle: shapeRenderStyle,
3205
+ renderStyle: (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng),
2315
3206
  rng: rng
2316
3207
  });
2317
3208
  }
@@ -2320,7 +3211,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2320
3211
  }
2321
3212
  // Reset blend mode for post-processing passes
2322
3213
  ctx.globalCompositeOperation = "source-over";
2323
- // ── 6. Flow-line pass (Feature H: tapered brush strokes) ───────
3214
+ // ── 6. Flow-line pass variable color, branching, pressure ────
2324
3215
  const baseFlowLines = 6 + Math.floor(rng() * 10);
2325
3216
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
2326
3217
  for(let i = 0; i < numFlowLines; i++){
@@ -2329,9 +3220,13 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2329
3220
  const steps = 30 + Math.floor(rng() * 40);
2330
3221
  const stepLen = (3 + rng() * 5) * scaleFactor;
2331
3222
  const startWidth = (1 + rng() * 3) * scaleFactor;
2332
- const lineColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)(colors[Math.floor(rng() * colors.length)], bgLum), 0.4);
3223
+ // Variable color: interpolate between two hierarchy colors along the stroke
3224
+ const lineColorStart = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
3225
+ const lineColorEnd = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
2333
3226
  const lineAlpha = 0.06 + rng() * 0.1;
2334
- // Draw as individual segments with tapering width
3227
+ // Pressure simulation: sinusoidal width variation
3228
+ const pressureFreq = 2 + rng() * 4;
3229
+ const pressurePhase = rng() * Math.PI * 2;
2335
3230
  let prevX = fx;
2336
3231
  let prevY = fy;
2337
3232
  for(let s = 0; s < steps; s++){
@@ -2339,37 +3234,61 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2339
3234
  fx += Math.cos(angle) * stepLen;
2340
3235
  fy += Math.sin(angle) * stepLen;
2341
3236
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
2342
- // Taper: thick at start, thin at end
2343
- const taper = 1 - s / steps * 0.8;
3237
+ const t = s / steps;
3238
+ // Taper + pressure
3239
+ const taper = 1 - t * 0.8;
3240
+ const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
2344
3241
  ctx.globalAlpha = lineAlpha * taper;
3242
+ // Interpolate color along stroke
3243
+ const lineColor = t < 0.5 ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $b5a262d09b87e373$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
2345
3244
  ctx.strokeStyle = lineColor;
2346
- ctx.lineWidth = startWidth * taper;
3245
+ ctx.lineWidth = startWidth * taper * pressure;
2347
3246
  ctx.lineCap = "round";
2348
3247
  ctx.beginPath();
2349
3248
  ctx.moveTo(prevX, prevY);
2350
3249
  ctx.lineTo(fx, fy);
2351
3250
  ctx.stroke();
3251
+ // Branching: ~12% chance per step to spawn a thinner child stroke
3252
+ if (rng() < 0.12 && s > 5 && s < steps - 10) {
3253
+ const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
3254
+ let bx = fx;
3255
+ let by = fy;
3256
+ let bPrevX = fx;
3257
+ let bPrevY = fy;
3258
+ const branchSteps = 5 + Math.floor(rng() * 10);
3259
+ const branchWidth = startWidth * taper * 0.4;
3260
+ for(let bs = 0; bs < branchSteps; bs++){
3261
+ const bAngle = branchAngle + (rng() - 0.5) * 0.2;
3262
+ bx += Math.cos(bAngle) * stepLen * 0.8;
3263
+ by += Math.sin(bAngle) * stepLen * 0.8;
3264
+ if (bx < 0 || bx > width || by < 0 || by > height) break;
3265
+ const bTaper = 1 - bs / branchSteps * 0.9;
3266
+ ctx.globalAlpha = lineAlpha * taper * bTaper * 0.6;
3267
+ ctx.lineWidth = branchWidth * bTaper;
3268
+ ctx.beginPath();
3269
+ ctx.moveTo(bPrevX, bPrevY);
3270
+ ctx.lineTo(bx, by);
3271
+ ctx.stroke();
3272
+ bPrevX = bx;
3273
+ bPrevY = by;
3274
+ }
3275
+ }
2352
3276
  prevX = fx;
2353
3277
  prevY = fy;
2354
3278
  }
2355
3279
  }
2356
3280
  // ── 6b. Apply symmetry mirroring ─────────────────────────────────
2357
- // Mirror the rendered content (shapes + flow lines) before post-processing.
2358
- // Uses ctx.canvas which is available in both Node (@napi-rs/canvas) and browsers.
2359
3281
  if (symmetryMode !== "none") {
2360
3282
  const canvas = ctx.canvas;
2361
3283
  ctx.save();
2362
3284
  if (symmetryMode === "bilateral-x" || symmetryMode === "quad") {
2363
- // Mirror left half onto right half
2364
3285
  ctx.save();
2365
3286
  ctx.translate(width, 0);
2366
3287
  ctx.scale(-1, 1);
2367
- // Draw the left half (0 to cx) onto the mirrored right side
2368
3288
  ctx.drawImage(canvas, 0, 0, Math.ceil(cx), height, 0, 0, Math.ceil(cx), height);
2369
3289
  ctx.restore();
2370
3290
  }
2371
3291
  if (symmetryMode === "bilateral-y" || symmetryMode === "quad") {
2372
- // Mirror top half onto bottom half
2373
3292
  ctx.save();
2374
3293
  ctx.translate(0, height);
2375
3294
  ctx.scale(1, -1);
@@ -2392,7 +3311,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2392
3311
  }
2393
3312
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
2394
3313
  ctx.globalAlpha = 1;
2395
- const vignetteStrength = 0.25 + rng() * 0.2; // 25-45% edge darkening
3314
+ const vignetteStrength = 0.25 + rng() * 0.2;
2396
3315
  const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
2397
3316
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
2398
3317
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
@@ -2418,13 +3337,57 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2418
3337
  const cpx = mx + -dy / (dist || 1) * bulge;
2419
3338
  const cpy = my + dx / (dist || 1) * bulge;
2420
3339
  ctx.globalAlpha = 0.06 + rng() * 0.1;
2421
- ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)(colors[Math.floor(rng() * colors.length)], bgLum), 0.3);
3340
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
2422
3341
  ctx.beginPath();
2423
3342
  ctx.moveTo(a.x, a.y);
2424
3343
  ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);
2425
3344
  ctx.stroke();
2426
3345
  }
2427
3346
  }
3347
+ // ── 10. Post-processing ────────────────────────────────────────
3348
+ // 10a. Color grading — unified tone across the whole image
3349
+ // Apply as a semi-transparent overlay in the grade hue
3350
+ ctx.globalAlpha = colorGrade.intensity * 0.25;
3351
+ ctx.globalCompositeOperation = "soft-light";
3352
+ const gradeHsl = `hsl(${Math.round(colorGrade.hue)}, 40%, 50%)`;
3353
+ ctx.fillStyle = gradeHsl;
3354
+ ctx.fillRect(0, 0, width, height);
3355
+ ctx.globalCompositeOperation = "source-over";
3356
+ // 10b. Chromatic aberration — subtle RGB channel offset at edges
3357
+ // Only apply for neon/cosmic/ethereal archetypes where it fits
3358
+ const chromaArchetypes = [
3359
+ "neon-glow",
3360
+ "cosmic",
3361
+ "ethereal"
3362
+ ];
3363
+ if (chromaArchetypes.includes(archetype.name)) {
3364
+ const chromaOffset = Math.ceil(2 * scaleFactor);
3365
+ const canvas = ctx.canvas;
3366
+ // Shift red channel slightly
3367
+ ctx.globalAlpha = 0.03;
3368
+ ctx.globalCompositeOperation = "screen";
3369
+ ctx.drawImage(canvas, chromaOffset, 0, width, height, 0, 0, width, height);
3370
+ // Shift blue channel opposite
3371
+ ctx.drawImage(canvas, -chromaOffset, 0, width, height, 0, 0, width, height);
3372
+ ctx.globalCompositeOperation = "source-over";
3373
+ }
3374
+ // 10c. Bloom — soft glow on bright areas for neon/cosmic archetypes
3375
+ const bloomArchetypes = [
3376
+ "neon-glow",
3377
+ "cosmic"
3378
+ ];
3379
+ if (bloomArchetypes.includes(archetype.name)) {
3380
+ const canvas = ctx.canvas;
3381
+ ctx.globalAlpha = 0.08;
3382
+ ctx.globalCompositeOperation = "screen";
3383
+ // Draw the image slightly scaled up and blurred via shadow
3384
+ ctx.save();
3385
+ ctx.shadowBlur = 30 * scaleFactor;
3386
+ ctx.shadowColor = "rgba(255,255,255,0.3)";
3387
+ ctx.drawImage(canvas, 0, 0, width, height);
3388
+ ctx.restore();
3389
+ ctx.globalCompositeOperation = "source-over";
3390
+ }
2428
3391
  ctx.globalAlpha = 1;
2429
3392
  }
2430
3393