git-hash-art 0.6.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/module.js CHANGED
@@ -13,17 +13,22 @@ import $4RUNL$colorscheme from "color-scheme";
13
13
  * identically in Node (@napi-rs/canvas) and browsers.
14
14
  *
15
15
  * Generation pipeline:
16
- * 1. Background radial gradient from hash-derived dark palette
17
- * 1b. Layered background large faint shapes / subtle pattern for depth
18
- * 2. Composition modehash selects: radial, flow-field, spiral, grid-subdivision, or clustered
19
- * 3. Focal points + void zones (negative space)
20
- * 4. Flow field seed values
21
- * 5. Shape layers — blend modes, render styles, weighted selection,
22
- * focal-point placement, atmospheric depth, organic edges
16
+ * 0. Archetype selection + shape palette + color hierarchy
17
+ * 1. Backgroundstyle from archetype, gradient mesh for depth
18
+ * 1b. Layered background archetype-coherent shapes
19
+ * 2. Composition mode + symmetry
20
+ * 3. Focal points + void zones + hero avoidance field
21
+ * 4. Flow field
22
+ * 4b. Hero shape
23
+ * 5. Shape layers — palette-driven selection, affinity-aware styles,
24
+ * size echo, tangent placement, atmospheric depth
23
25
  * 5b. Recursive nesting
24
- * 6. Flow-line passtapered brush-stroke curves
25
- * 7. Noise texture overlay
26
- * 8. Organic connecting curves
26
+ * 6. Flow linesvariable color, branching, pressure simulation
27
+ * 6b. Symmetry mirroring
28
+ * 7. Noise texture
29
+ * 8. Vignette
30
+ * 9. Organic connecting curves
31
+ * 10. Post-processing — color grading, chromatic aberration, bloom
27
32
  */
28
33
  // declare module 'color-scheme';
29
34
 
@@ -397,6 +402,67 @@ function $9d614e7d77fc2947$export$f2121afcad3d553f(hex, alpha) {
397
402
  const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex);
398
403
  return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
399
404
  }
405
+ function $9d614e7d77fc2947$export$fabac4600b87056(colors, rng) {
406
+ if (colors.length < 3) return {
407
+ dominant: colors[0] || "#888888",
408
+ secondary: colors[1] || colors[0] || "#888888",
409
+ accent: colors[colors.length - 1] || "#888888",
410
+ all: colors
411
+ };
412
+ // Pick dominant as the color closest to the palette's average hue
413
+ const hsls = colors.map((c)=>$9d614e7d77fc2947$var$hexToHsl(c));
414
+ const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
415
+ let dominantIdx = 0;
416
+ let minDist = 360;
417
+ for(let i = 0; i < hsls.length; i++){
418
+ const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
419
+ if (d < minDist) {
420
+ minDist = d;
421
+ dominantIdx = i;
422
+ }
423
+ }
424
+ // Accent is the color most distant from dominant in hue
425
+ let accentIdx = 0;
426
+ let maxDist = 0;
427
+ for(let i = 0; i < hsls.length; i++){
428
+ if (i === dominantIdx) continue;
429
+ const d = Math.min(Math.abs(hsls[i][0] - hsls[dominantIdx][0]), 360 - Math.abs(hsls[i][0] - hsls[dominantIdx][0]));
430
+ if (d > maxDist) {
431
+ maxDist = d;
432
+ accentIdx = i;
433
+ }
434
+ }
435
+ // Secondary is the remaining color with highest saturation
436
+ let secondaryIdx = 0;
437
+ let maxSat = -1;
438
+ for(let i = 0; i < hsls.length; i++){
439
+ if (i === dominantIdx || i === accentIdx) continue;
440
+ if (hsls[i][1] > maxSat) {
441
+ maxSat = hsls[i][1];
442
+ secondaryIdx = i;
443
+ }
444
+ }
445
+ if (secondaryIdx === dominantIdx) secondaryIdx = accentIdx === 0 ? 1 : 0;
446
+ return {
447
+ dominant: colors[dominantIdx],
448
+ secondary: colors[secondaryIdx],
449
+ accent: colors[accentIdx],
450
+ all: colors
451
+ };
452
+ }
453
+ function $9d614e7d77fc2947$export$b49f62f0a99da0e8(hierarchy, rng) {
454
+ const roll = rng();
455
+ if (roll < 0.60) return hierarchy.dominant;
456
+ if (roll < 0.85) return hierarchy.secondary;
457
+ return hierarchy.accent;
458
+ }
459
+ function $9d614e7d77fc2947$export$18a34c25ea7e724b(hex, rng, hueAmount = 8, slAmount = 0.06) {
460
+ const [h, s, l] = $9d614e7d77fc2947$var$hexToHsl(hex);
461
+ const newH = (h + (rng() - 0.5) * hueAmount * 2 + 360) % 360;
462
+ const newS = Math.max(0, Math.min(1, s + (rng() - 0.5) * slAmount * 2));
463
+ const newL = Math.max(0, Math.min(1, l + (rng() - 0.5) * slAmount * 2));
464
+ return $9d614e7d77fc2947$var$hslToHex(newH, newS, newL);
465
+ }
400
466
  function $9d614e7d77fc2947$export$59539d800dbe6858(hex, rng, amount = 0.1) {
401
467
  const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex);
402
468
  const jit = ()=>(rng() - 0.5) * 2 * amount * 255;
@@ -412,6 +478,55 @@ function $9d614e7d77fc2947$export$51ea55f869b7e0d3(hex, target, amount) {
412
478
  const [h, s, l] = $9d614e7d77fc2947$var$hexToHsl(hex);
413
479
  return $9d614e7d77fc2947$var$hslToHex($9d614e7d77fc2947$var$shiftHueToward(h, target, amount), s, l);
414
480
  }
481
+ function $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe(hex) {
482
+ const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex).map((c)=>{
483
+ const s = c / 255;
484
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
485
+ });
486
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
487
+ }
488
+ function $9d614e7d77fc2947$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
489
+ const fgLum = $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe(fgHex);
490
+ const diff = Math.abs(fgLum - bgLuminance);
491
+ if (diff >= minContrast) return fgHex;
492
+ const [h, s, l] = $9d614e7d77fc2947$var$hexToHsl(fgHex);
493
+ if (bgLuminance > 0.5) {
494
+ // Light background — darken and boost saturation
495
+ const targetL = Math.max(0.08, l - (minContrast - diff) * 1.5);
496
+ const targetS = Math.min(1, s + 0.2);
497
+ return $9d614e7d77fc2947$var$hslToHex(h, targetS, targetL);
498
+ } else {
499
+ // Dark background — lighten and boost saturation
500
+ const targetL = Math.min(0.92, l + (minContrast - diff) * 1.5);
501
+ const targetS = Math.min(1, s + 0.15);
502
+ return $9d614e7d77fc2947$var$hslToHex(h, targetS, targetL);
503
+ }
504
+ }
505
+ function $9d614e7d77fc2947$export$4a3734b8c4b5c0e(hex, gradeHue, intensity) {
506
+ const [h, s, l] = $9d614e7d77fc2947$var$hexToHsl(hex);
507
+ // Blend hue toward the grade hue
508
+ const hueDiff = (gradeHue - h + 540) % 360 - 180;
509
+ const newH = (h + hueDiff * intensity * 0.3 + 360) % 360;
510
+ // Slightly unify saturation
511
+ const newS = Math.max(0, Math.min(1, s + (0.5 - s) * intensity * 0.15));
512
+ return $9d614e7d77fc2947$var$hslToHex(newH, newS, l);
513
+ }
514
+ function $9d614e7d77fc2947$export$6d1620b367f86f7a(rng) {
515
+ // Warm golden, cool blue, rosy, teal, amber
516
+ const GRADE_HUES = [
517
+ 40,
518
+ 220,
519
+ 340,
520
+ 175,
521
+ 30
522
+ ];
523
+ const hue = GRADE_HUES[Math.floor(rng() * GRADE_HUES.length)] + (rng() - 0.5) * 20;
524
+ const intensity = 0.15 + rng() * 0.25;
525
+ return {
526
+ hue: (hue + 360) % 360,
527
+ intensity: intensity
528
+ };
529
+ }
415
530
 
416
531
 
417
532
 
@@ -1192,10 +1307,172 @@ const $d63629e16208c310$export$c2fc138f94dd4b2a = {
1192
1307
  };
1193
1308
 
1194
1309
 
1310
+ /**
1311
+ * Procedural shape generators — hash-derived shapes that are unique
1312
+ * per generation. Unlike the fixed shape library, these produce geometry
1313
+ * that doesn't repeat across hashes.
1314
+ *
1315
+ * All draw functions accept an RNG via the config parameter so the
1316
+ * shapes are deterministic from the hash.
1317
+ */ const $a7241acce45d5513$export$580f80cfb9de73bc = (ctx, size, config)=>{
1318
+ const rng = config?.rng ?? Math.random;
1319
+ const r = size / 2;
1320
+ const numPoints = 5 + Math.floor(rng() * 5); // 5-9 lobes
1321
+ const points = [];
1322
+ for(let i = 0; i < numPoints; i++){
1323
+ const angle = i / numPoints * Math.PI * 2;
1324
+ const jitter = 0.5 + rng() * 0.5; // radius varies 50-100%
1325
+ points.push({
1326
+ x: Math.cos(angle) * r * jitter,
1327
+ y: Math.sin(angle) * r * jitter
1328
+ });
1329
+ }
1330
+ ctx.beginPath();
1331
+ // Start at midpoint between last and first point
1332
+ const last = points[points.length - 1];
1333
+ const first = points[0];
1334
+ ctx.moveTo((last.x + first.x) / 2, (last.y + first.y) / 2);
1335
+ for(let i = 0; i < numPoints; i++){
1336
+ const curr = points[i];
1337
+ const next = points[(i + 1) % numPoints];
1338
+ const midX = (curr.x + next.x) / 2;
1339
+ const midY = (curr.y + next.y) / 2;
1340
+ ctx.quadraticCurveTo(curr.x, curr.y, midX, midY);
1341
+ }
1342
+ ctx.closePath();
1343
+ };
1344
+ const $a7241acce45d5513$export$7a6094023f0902a6 = (ctx, size, config)=>{
1345
+ const rng = config?.rng ?? Math.random;
1346
+ const r = size / 2;
1347
+ const sides = 3 + Math.floor(rng() * 10); // 3-12 sides
1348
+ const jitterAmount = 0.1 + rng() * 0.4; // 10-50% vertex displacement
1349
+ ctx.beginPath();
1350
+ for(let i = 0; i < sides; i++){
1351
+ const angle = i / sides * Math.PI * 2 - Math.PI / 2;
1352
+ const radiusJitter = 1 - jitterAmount + rng() * jitterAmount * 2;
1353
+ const x = Math.cos(angle) * r * radiusJitter;
1354
+ const y = Math.sin(angle) * r * radiusJitter;
1355
+ if (i === 0) ctx.moveTo(x, y);
1356
+ else ctx.lineTo(x, y);
1357
+ }
1358
+ ctx.closePath();
1359
+ };
1360
+ const $a7241acce45d5513$export$ef56b4a8316e47d5 = (ctx, size, config)=>{
1361
+ const rng = config?.rng ?? Math.random;
1362
+ const r = size / 2;
1363
+ // Frequency ratios — small integers produce recognizable patterns
1364
+ const freqA = 1 + Math.floor(rng() * 5); // 1-5
1365
+ const freqB = 1 + Math.floor(rng() * 5); // 1-5
1366
+ const phase = rng() * Math.PI; // phase offset
1367
+ const steps = 120;
1368
+ ctx.beginPath();
1369
+ for(let i = 0; i <= steps; i++){
1370
+ const t = i / steps * Math.PI * 2;
1371
+ const x = Math.sin(freqA * t + phase) * r;
1372
+ const y = Math.sin(freqB * t) * r;
1373
+ if (i === 0) ctx.moveTo(x, y);
1374
+ else ctx.lineTo(x, y);
1375
+ }
1376
+ ctx.closePath();
1377
+ };
1378
+ const $a7241acce45d5513$export$1db9219b4f34658c = (ctx, size, config)=>{
1379
+ const rng = config?.rng ?? Math.random;
1380
+ const r = size / 2;
1381
+ // Exponent range: 0.3 (spiky astroid) to 5 (rounded rectangle)
1382
+ const n = 0.3 + rng() * 4.7;
1383
+ const steps = 120;
1384
+ ctx.beginPath();
1385
+ for(let i = 0; i <= steps; i++){
1386
+ const t = i / steps * Math.PI * 2;
1387
+ const cosT = Math.cos(t);
1388
+ const sinT = Math.sin(t);
1389
+ // Superellipse parametric form
1390
+ const x = Math.sign(cosT) * Math.pow(Math.abs(cosT), 2 / n) * r;
1391
+ const y = Math.sign(sinT) * Math.pow(Math.abs(sinT), 2 / n) * r;
1392
+ if (i === 0) ctx.moveTo(x, y);
1393
+ else ctx.lineTo(x, y);
1394
+ }
1395
+ ctx.closePath();
1396
+ };
1397
+ const $a7241acce45d5513$export$b027c64d22b01985 = (ctx, size, config)=>{
1398
+ const rng = config?.rng ?? Math.random;
1399
+ const scale = size / 2;
1400
+ // R = outer radius, r = inner radius, d = pen distance from inner center
1401
+ const R = 1;
1402
+ const r = 0.2 + rng() * 0.6; // 0.2-0.8
1403
+ const d = 0.3 + rng() * 0.7; // 0.3-1.0
1404
+ // Number of full rotations needed to close the curve
1405
+ const gcd = (a, b)=>{
1406
+ const ai = Math.round(a * 1000);
1407
+ const bi = Math.round(b * 1000);
1408
+ const g = (x, y)=>y === 0 ? x : g(y, x % y);
1409
+ return g(ai, bi) / 1000;
1410
+ };
1411
+ const period = r / gcd(R, r);
1412
+ const maxT = Math.min(period, 10) * Math.PI * 2; // cap at 10 rotations
1413
+ const steps = Math.min(600, Math.floor(maxT * 20));
1414
+ ctx.beginPath();
1415
+ for(let i = 0; i <= steps; i++){
1416
+ const t = i / steps * maxT;
1417
+ const x = ((R - r) * Math.cos(t) + d * Math.cos((R - r) / r * t)) * scale / (1 + d);
1418
+ const y = ((R - r) * Math.sin(t) - d * Math.sin((R - r) / r * t)) * scale / (1 + d);
1419
+ if (i === 0) ctx.moveTo(x, y);
1420
+ else ctx.lineTo(x, y);
1421
+ }
1422
+ ctx.closePath();
1423
+ };
1424
+ const $a7241acce45d5513$export$7608ccd03bfb705d = (ctx, size, config)=>{
1425
+ const rng = config?.rng ?? Math.random;
1426
+ const r = size / 2;
1427
+ const rings = 2 + Math.floor(rng() * 4); // 2-5 rings
1428
+ const freq = 3 + Math.floor(rng() * 12); // 3-14 waves per ring
1429
+ const amp = 0.05 + rng() * 0.15; // 5-20% of radius
1430
+ ctx.beginPath();
1431
+ for(let ring = 0; ring < rings; ring++){
1432
+ const baseR = r * (0.3 + ring / rings * 0.7);
1433
+ const steps = 80;
1434
+ for(let i = 0; i <= steps; i++){
1435
+ const t = i / steps * Math.PI * 2;
1436
+ const wave = Math.sin(t * freq + ring * 1.5) * baseR * amp;
1437
+ const x = Math.cos(t) * (baseR + wave);
1438
+ const y = Math.sin(t) * (baseR + wave);
1439
+ if (i === 0) ctx.moveTo(x, y);
1440
+ else ctx.lineTo(x, y);
1441
+ }
1442
+ }
1443
+ };
1444
+ const $a7241acce45d5513$export$11a377e7498bb523 = (ctx, size, config)=>{
1445
+ const rng = config?.rng ?? Math.random;
1446
+ const r = size / 2;
1447
+ const k = 2 + Math.floor(rng() * 6); // 2-7 petal parameter
1448
+ const steps = 200;
1449
+ ctx.beginPath();
1450
+ for(let i = 0; i <= steps; i++){
1451
+ const theta = i / steps * Math.PI * 2 * (k % 2 === 0 ? 1 : 2);
1452
+ const rr = Math.cos(k * theta) * r;
1453
+ const x = rr * Math.cos(theta);
1454
+ const y = rr * Math.sin(theta);
1455
+ if (i === 0) ctx.moveTo(x, y);
1456
+ else ctx.lineTo(x, y);
1457
+ }
1458
+ ctx.closePath();
1459
+ };
1460
+ const $a7241acce45d5513$export$40cfb4c637f2fbb5 = {
1461
+ blob: $a7241acce45d5513$export$580f80cfb9de73bc,
1462
+ ngon: $a7241acce45d5513$export$7a6094023f0902a6,
1463
+ lissajous: $a7241acce45d5513$export$ef56b4a8316e47d5,
1464
+ superellipse: $a7241acce45d5513$export$1db9219b4f34658c,
1465
+ spirograph: $a7241acce45d5513$export$b027c64d22b01985,
1466
+ waveRing: $a7241acce45d5513$export$7608ccd03bfb705d,
1467
+ rose: $a7241acce45d5513$export$11a377e7498bb523
1468
+ };
1469
+
1470
+
1195
1471
  const $701ba7c7229ef06d$export$4ff7fc6f1af248b5 = {
1196
1472
  ...(0, $f225038c018b3815$export$492753207a5258e1),
1197
1473
  ...(0, $8bde0a7ee87832b5$export$dbe318a13ce51887),
1198
- ...(0, $d63629e16208c310$export$c2fc138f94dd4b2a)
1474
+ ...(0, $d63629e16208c310$export$c2fc138f94dd4b2a),
1475
+ ...(0, $a7241acce45d5513$export$40cfb4c637f2fbb5)
1199
1476
  };
1200
1477
 
1201
1478
 
@@ -1280,23 +1557,50 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
1280
1557
  break;
1281
1558
  case "watercolor":
1282
1559
  {
1283
- // Draw 3-4 slightly offset passes at low opacity for a bleed effect
1284
- const passes = 3 + (rng ? Math.floor(rng() * 2) : 0);
1560
+ // Improved watercolor: edge darkening + radial bleed + layered washes
1561
+ const passes = 4 + (rng ? Math.floor(rng() * 2) : 0);
1285
1562
  const savedAlpha = ctx.globalAlpha;
1286
- ctx.globalAlpha = savedAlpha * (0.3 / passes * 2);
1563
+ // Pass 1: Base wash large, soft fill at low opacity
1564
+ ctx.globalAlpha = savedAlpha * 0.15;
1565
+ ctx.save();
1566
+ const baseScale = 1.08 + (rng ? rng() * 0.04 : 0);
1567
+ ctx.scale(baseScale, baseScale);
1568
+ ctx.fill();
1569
+ ctx.restore();
1570
+ // Pass 2: Multiple offset washes with radial displacement
1571
+ ctx.globalAlpha = savedAlpha * (0.25 / passes * 2);
1287
1572
  for(let p = 0; p < passes; p++){
1288
- const jx = rng ? (rng() - 0.5) * size * 0.06 : 0;
1289
- const jy = rng ? (rng() - 0.5) * size * 0.06 : 0;
1573
+ // Radial outward displacement (not uniform) for organic bleed
1574
+ const angle = rng ? rng() * Math.PI * 2 : p * Math.PI / 2;
1575
+ const dist = rng ? rng() * size * 0.05 : size * 0.02;
1576
+ const jx = Math.cos(angle) * dist;
1577
+ const jy = Math.sin(angle) * dist;
1290
1578
  ctx.save();
1291
1579
  ctx.translate(jx, jy);
1292
1580
  ctx.fill();
1293
1581
  ctx.restore();
1294
1582
  }
1583
+ // Pass 3: Edge darkening — draw a slightly smaller shape with lighter fill
1584
+ // to simulate pigment pooling at boundaries
1585
+ ctx.globalAlpha = savedAlpha * 0.35;
1586
+ ctx.save();
1587
+ const innerScale = 0.85 + (rng ? rng() * 0.08 : 0);
1588
+ ctx.scale(innerScale, innerScale);
1589
+ // Lighten the fill for the inner area
1590
+ const origFill = ctx.fillStyle;
1591
+ if (typeof fillColor === "string") ctx.fillStyle = fillColor.replace(/[\d.]+\)$/, (m)=>{
1592
+ const v = parseFloat(m);
1593
+ return Math.min(1, v * 1.4).toFixed(2) + ")";
1594
+ });
1595
+ ctx.fill();
1596
+ ctx.fillStyle = origFill;
1597
+ ctx.restore();
1295
1598
  ctx.globalAlpha = savedAlpha;
1296
- // Light stroke on top
1297
- ctx.globalAlpha *= 0.4;
1599
+ // Soft stroke on top — thinner than normal for delicacy
1600
+ ctx.globalAlpha *= 0.25;
1601
+ ctx.lineWidth = strokeWidth * 0.6;
1298
1602
  ctx.stroke();
1299
- ctx.globalAlpha /= 0.4;
1603
+ ctx.globalAlpha /= 0.25;
1300
1604
  break;
1301
1605
  }
1302
1606
  case "hatched":
@@ -1399,7 +1703,9 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1399
1703
  ctx.lineWidth = strokeWidth;
1400
1704
  const drawFunction = (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[shape];
1401
1705
  if (drawFunction) {
1402
- drawFunction(ctx, size);
1706
+ drawFunction(ctx, size, {
1707
+ rng: rng
1708
+ });
1403
1709
  $9beb8f41637c29fd$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
1404
1710
  }
1405
1711
  // Reset shadow so patterns aren't double-glowed
@@ -1416,6 +1722,675 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1416
1722
 
1417
1723
 
1418
1724
 
1725
+ /**
1726
+ * Shape affinity system — controls which shapes look good together,
1727
+ * quality tiers for different rendering contexts, and size preferences.
1728
+ *
1729
+ * This replaces the naive "pick any shape" approach with intentional
1730
+ * curation that produces more cohesive compositions.
1731
+ */ // ── Quality tiers ───────────────────────────────────────────────────
1732
+ // Not all shapes render equally well at all sizes or in all contexts.
1733
+ // Tier 1 shapes are visually strong at any size; Tier 3 shapes need
1734
+ // specific conditions to look good.
1735
+ const $24064302523652b1$export$4343b39fe47bd82c = {
1736
+ // ── Basic shapes ──────────────────────────────────────────────
1737
+ circle: {
1738
+ tier: 1,
1739
+ minSizeFraction: 0.05,
1740
+ maxSizeFraction: 1.0,
1741
+ affinities: [
1742
+ "circle",
1743
+ "blob",
1744
+ "hexagon",
1745
+ "flowerOfLife",
1746
+ "seedOfLife"
1747
+ ],
1748
+ category: "basic",
1749
+ heroCandidate: false,
1750
+ bestStyles: [
1751
+ "fill-only",
1752
+ "watercolor",
1753
+ "fill-and-stroke"
1754
+ ]
1755
+ },
1756
+ square: {
1757
+ tier: 2,
1758
+ minSizeFraction: 0.08,
1759
+ maxSizeFraction: 0.7,
1760
+ affinities: [
1761
+ "square",
1762
+ "diamond",
1763
+ "superellipse",
1764
+ "islamicPattern"
1765
+ ],
1766
+ category: "basic",
1767
+ heroCandidate: false,
1768
+ bestStyles: [
1769
+ "fill-and-stroke",
1770
+ "stroke-only",
1771
+ "hatched"
1772
+ ]
1773
+ },
1774
+ triangle: {
1775
+ tier: 1,
1776
+ minSizeFraction: 0.06,
1777
+ maxSizeFraction: 0.9,
1778
+ affinities: [
1779
+ "triangle",
1780
+ "diamond",
1781
+ "hexagon",
1782
+ "merkaba",
1783
+ "sriYantra"
1784
+ ],
1785
+ category: "basic",
1786
+ heroCandidate: false,
1787
+ bestStyles: [
1788
+ "fill-and-stroke",
1789
+ "fill-only",
1790
+ "watercolor"
1791
+ ]
1792
+ },
1793
+ hexagon: {
1794
+ tier: 1,
1795
+ minSizeFraction: 0.05,
1796
+ maxSizeFraction: 1.0,
1797
+ affinities: [
1798
+ "hexagon",
1799
+ "circle",
1800
+ "flowerOfLife",
1801
+ "metatronsCube",
1802
+ "triangle"
1803
+ ],
1804
+ category: "basic",
1805
+ heroCandidate: false,
1806
+ bestStyles: [
1807
+ "fill-only",
1808
+ "fill-and-stroke",
1809
+ "watercolor"
1810
+ ]
1811
+ },
1812
+ star: {
1813
+ tier: 2,
1814
+ minSizeFraction: 0.08,
1815
+ maxSizeFraction: 0.6,
1816
+ affinities: [
1817
+ "star",
1818
+ "circle",
1819
+ "mandala",
1820
+ "spirograph"
1821
+ ],
1822
+ category: "basic",
1823
+ heroCandidate: false,
1824
+ bestStyles: [
1825
+ "fill-and-stroke",
1826
+ "stroke-only",
1827
+ "dashed"
1828
+ ]
1829
+ },
1830
+ "jacked-star": {
1831
+ tier: 3,
1832
+ minSizeFraction: 0.1,
1833
+ maxSizeFraction: 0.4,
1834
+ affinities: [
1835
+ "star",
1836
+ "circle"
1837
+ ],
1838
+ category: "basic",
1839
+ heroCandidate: false,
1840
+ bestStyles: [
1841
+ "stroke-only",
1842
+ "dashed"
1843
+ ]
1844
+ },
1845
+ heart: {
1846
+ tier: 3,
1847
+ minSizeFraction: 0.1,
1848
+ maxSizeFraction: 0.5,
1849
+ affinities: [
1850
+ "circle",
1851
+ "blob"
1852
+ ],
1853
+ category: "basic",
1854
+ heroCandidate: false,
1855
+ bestStyles: [
1856
+ "fill-only",
1857
+ "watercolor"
1858
+ ]
1859
+ },
1860
+ diamond: {
1861
+ tier: 2,
1862
+ minSizeFraction: 0.06,
1863
+ maxSizeFraction: 0.8,
1864
+ affinities: [
1865
+ "diamond",
1866
+ "triangle",
1867
+ "square",
1868
+ "merkaba"
1869
+ ],
1870
+ category: "basic",
1871
+ heroCandidate: false,
1872
+ bestStyles: [
1873
+ "fill-and-stroke",
1874
+ "fill-only",
1875
+ "double-stroke"
1876
+ ]
1877
+ },
1878
+ cube: {
1879
+ tier: 3,
1880
+ minSizeFraction: 0.08,
1881
+ maxSizeFraction: 0.5,
1882
+ affinities: [
1883
+ "square",
1884
+ "diamond"
1885
+ ],
1886
+ category: "basic",
1887
+ heroCandidate: false,
1888
+ bestStyles: [
1889
+ "stroke-only",
1890
+ "fill-and-stroke"
1891
+ ]
1892
+ },
1893
+ // ── Complex shapes ────────────────────────────────────────────
1894
+ platonicSolid: {
1895
+ tier: 2,
1896
+ minSizeFraction: 0.15,
1897
+ maxSizeFraction: 0.8,
1898
+ affinities: [
1899
+ "metatronsCube",
1900
+ "merkaba",
1901
+ "hexagon",
1902
+ "triangle"
1903
+ ],
1904
+ category: "complex",
1905
+ heroCandidate: true,
1906
+ bestStyles: [
1907
+ "stroke-only",
1908
+ "double-stroke",
1909
+ "dashed"
1910
+ ]
1911
+ },
1912
+ fibonacciSpiral: {
1913
+ tier: 1,
1914
+ minSizeFraction: 0.2,
1915
+ maxSizeFraction: 1.0,
1916
+ affinities: [
1917
+ "circle",
1918
+ "rose",
1919
+ "spirograph",
1920
+ "flowerOfLife"
1921
+ ],
1922
+ category: "complex",
1923
+ heroCandidate: true,
1924
+ bestStyles: [
1925
+ "stroke-only",
1926
+ "incomplete",
1927
+ "watercolor"
1928
+ ]
1929
+ },
1930
+ islamicPattern: {
1931
+ tier: 2,
1932
+ minSizeFraction: 0.25,
1933
+ maxSizeFraction: 0.9,
1934
+ affinities: [
1935
+ "square",
1936
+ "hexagon",
1937
+ "star",
1938
+ "mandala"
1939
+ ],
1940
+ category: "complex",
1941
+ heroCandidate: true,
1942
+ bestStyles: [
1943
+ "stroke-only",
1944
+ "dashed",
1945
+ "hatched"
1946
+ ]
1947
+ },
1948
+ celticKnot: {
1949
+ tier: 2,
1950
+ minSizeFraction: 0.2,
1951
+ maxSizeFraction: 0.7,
1952
+ affinities: [
1953
+ "circle",
1954
+ "lissajous",
1955
+ "spirograph"
1956
+ ],
1957
+ category: "complex",
1958
+ heroCandidate: true,
1959
+ bestStyles: [
1960
+ "stroke-only",
1961
+ "double-stroke"
1962
+ ]
1963
+ },
1964
+ merkaba: {
1965
+ tier: 1,
1966
+ minSizeFraction: 0.15,
1967
+ maxSizeFraction: 1.0,
1968
+ affinities: [
1969
+ "triangle",
1970
+ "diamond",
1971
+ "sriYantra",
1972
+ "metatronsCube"
1973
+ ],
1974
+ category: "complex",
1975
+ heroCandidate: true,
1976
+ bestStyles: [
1977
+ "stroke-only",
1978
+ "fill-and-stroke",
1979
+ "double-stroke"
1980
+ ]
1981
+ },
1982
+ mandala: {
1983
+ tier: 1,
1984
+ minSizeFraction: 0.2,
1985
+ maxSizeFraction: 1.0,
1986
+ affinities: [
1987
+ "circle",
1988
+ "flowerOfLife",
1989
+ "spirograph",
1990
+ "rose"
1991
+ ],
1992
+ category: "complex",
1993
+ heroCandidate: true,
1994
+ bestStyles: [
1995
+ "stroke-only",
1996
+ "dashed",
1997
+ "incomplete"
1998
+ ]
1999
+ },
2000
+ fractal: {
2001
+ tier: 2,
2002
+ minSizeFraction: 0.2,
2003
+ maxSizeFraction: 0.8,
2004
+ affinities: [
2005
+ "blob",
2006
+ "lissajous",
2007
+ "circle"
2008
+ ],
2009
+ category: "complex",
2010
+ heroCandidate: true,
2011
+ bestStyles: [
2012
+ "stroke-only",
2013
+ "incomplete"
2014
+ ]
2015
+ },
2016
+ // ── Sacred shapes ─────────────────────────────────────────────
2017
+ flowerOfLife: {
2018
+ tier: 1,
2019
+ minSizeFraction: 0.2,
2020
+ maxSizeFraction: 1.0,
2021
+ affinities: [
2022
+ "circle",
2023
+ "hexagon",
2024
+ "seedOfLife",
2025
+ "eggOfLife",
2026
+ "metatronsCube"
2027
+ ],
2028
+ category: "sacred",
2029
+ heroCandidate: true,
2030
+ bestStyles: [
2031
+ "stroke-only",
2032
+ "watercolor",
2033
+ "incomplete"
2034
+ ]
2035
+ },
2036
+ treeOfLife: {
2037
+ tier: 2,
2038
+ minSizeFraction: 0.25,
2039
+ maxSizeFraction: 0.9,
2040
+ affinities: [
2041
+ "circle",
2042
+ "flowerOfLife",
2043
+ "metatronsCube"
2044
+ ],
2045
+ category: "sacred",
2046
+ heroCandidate: true,
2047
+ bestStyles: [
2048
+ "stroke-only",
2049
+ "double-stroke"
2050
+ ]
2051
+ },
2052
+ metatronsCube: {
2053
+ tier: 1,
2054
+ minSizeFraction: 0.2,
2055
+ maxSizeFraction: 1.0,
2056
+ affinities: [
2057
+ "hexagon",
2058
+ "flowerOfLife",
2059
+ "platonicSolid",
2060
+ "merkaba"
2061
+ ],
2062
+ category: "sacred",
2063
+ heroCandidate: true,
2064
+ bestStyles: [
2065
+ "stroke-only",
2066
+ "dashed",
2067
+ "incomplete"
2068
+ ]
2069
+ },
2070
+ sriYantra: {
2071
+ tier: 1,
2072
+ minSizeFraction: 0.2,
2073
+ maxSizeFraction: 1.0,
2074
+ affinities: [
2075
+ "triangle",
2076
+ "merkaba",
2077
+ "mandala",
2078
+ "diamond"
2079
+ ],
2080
+ category: "sacred",
2081
+ heroCandidate: true,
2082
+ bestStyles: [
2083
+ "stroke-only",
2084
+ "fill-and-stroke",
2085
+ "double-stroke"
2086
+ ]
2087
+ },
2088
+ seedOfLife: {
2089
+ tier: 1,
2090
+ minSizeFraction: 0.15,
2091
+ maxSizeFraction: 0.9,
2092
+ affinities: [
2093
+ "circle",
2094
+ "flowerOfLife",
2095
+ "eggOfLife",
2096
+ "hexagon"
2097
+ ],
2098
+ category: "sacred",
2099
+ heroCandidate: true,
2100
+ bestStyles: [
2101
+ "stroke-only",
2102
+ "watercolor",
2103
+ "fill-only"
2104
+ ]
2105
+ },
2106
+ vesicaPiscis: {
2107
+ tier: 2,
2108
+ minSizeFraction: 0.15,
2109
+ maxSizeFraction: 0.7,
2110
+ affinities: [
2111
+ "circle",
2112
+ "seedOfLife",
2113
+ "flowerOfLife"
2114
+ ],
2115
+ category: "sacred",
2116
+ heroCandidate: false,
2117
+ bestStyles: [
2118
+ "stroke-only",
2119
+ "watercolor"
2120
+ ]
2121
+ },
2122
+ torus: {
2123
+ tier: 3,
2124
+ minSizeFraction: 0.2,
2125
+ maxSizeFraction: 0.6,
2126
+ affinities: [
2127
+ "circle",
2128
+ "spirograph",
2129
+ "waveRing"
2130
+ ],
2131
+ category: "sacred",
2132
+ heroCandidate: false,
2133
+ bestStyles: [
2134
+ "stroke-only",
2135
+ "dashed"
2136
+ ]
2137
+ },
2138
+ eggOfLife: {
2139
+ tier: 2,
2140
+ minSizeFraction: 0.15,
2141
+ maxSizeFraction: 0.8,
2142
+ affinities: [
2143
+ "circle",
2144
+ "seedOfLife",
2145
+ "flowerOfLife"
2146
+ ],
2147
+ category: "sacred",
2148
+ heroCandidate: true,
2149
+ bestStyles: [
2150
+ "stroke-only",
2151
+ "watercolor"
2152
+ ]
2153
+ },
2154
+ // ── Procedural shapes ─────────────────────────────────────────
2155
+ blob: {
2156
+ tier: 1,
2157
+ minSizeFraction: 0.05,
2158
+ maxSizeFraction: 1.0,
2159
+ affinities: [
2160
+ "blob",
2161
+ "circle",
2162
+ "superellipse",
2163
+ "waveRing"
2164
+ ],
2165
+ category: "procedural",
2166
+ heroCandidate: false,
2167
+ bestStyles: [
2168
+ "fill-only",
2169
+ "watercolor",
2170
+ "fill-and-stroke"
2171
+ ]
2172
+ },
2173
+ ngon: {
2174
+ tier: 2,
2175
+ minSizeFraction: 0.06,
2176
+ maxSizeFraction: 0.8,
2177
+ affinities: [
2178
+ "hexagon",
2179
+ "triangle",
2180
+ "diamond",
2181
+ "superellipse"
2182
+ ],
2183
+ category: "procedural",
2184
+ heroCandidate: false,
2185
+ bestStyles: [
2186
+ "fill-and-stroke",
2187
+ "fill-only",
2188
+ "hatched"
2189
+ ]
2190
+ },
2191
+ lissajous: {
2192
+ tier: 2,
2193
+ minSizeFraction: 0.15,
2194
+ maxSizeFraction: 0.8,
2195
+ affinities: [
2196
+ "spirograph",
2197
+ "rose",
2198
+ "celticKnot",
2199
+ "fibonacciSpiral"
2200
+ ],
2201
+ category: "procedural",
2202
+ heroCandidate: false,
2203
+ bestStyles: [
2204
+ "stroke-only",
2205
+ "incomplete",
2206
+ "dashed"
2207
+ ]
2208
+ },
2209
+ superellipse: {
2210
+ tier: 1,
2211
+ minSizeFraction: 0.05,
2212
+ maxSizeFraction: 1.0,
2213
+ affinities: [
2214
+ "circle",
2215
+ "square",
2216
+ "blob",
2217
+ "hexagon"
2218
+ ],
2219
+ category: "procedural",
2220
+ heroCandidate: false,
2221
+ bestStyles: [
2222
+ "fill-only",
2223
+ "watercolor",
2224
+ "fill-and-stroke"
2225
+ ]
2226
+ },
2227
+ spirograph: {
2228
+ tier: 1,
2229
+ minSizeFraction: 0.15,
2230
+ maxSizeFraction: 0.9,
2231
+ affinities: [
2232
+ "rose",
2233
+ "lissajous",
2234
+ "mandala",
2235
+ "flowerOfLife"
2236
+ ],
2237
+ category: "procedural",
2238
+ heroCandidate: true,
2239
+ bestStyles: [
2240
+ "stroke-only",
2241
+ "incomplete",
2242
+ "dashed"
2243
+ ]
2244
+ },
2245
+ waveRing: {
2246
+ tier: 2,
2247
+ minSizeFraction: 0.1,
2248
+ maxSizeFraction: 0.8,
2249
+ affinities: [
2250
+ "circle",
2251
+ "blob",
2252
+ "torus",
2253
+ "spirograph"
2254
+ ],
2255
+ category: "procedural",
2256
+ heroCandidate: false,
2257
+ bestStyles: [
2258
+ "stroke-only",
2259
+ "dashed",
2260
+ "incomplete"
2261
+ ]
2262
+ },
2263
+ rose: {
2264
+ tier: 1,
2265
+ minSizeFraction: 0.1,
2266
+ maxSizeFraction: 0.9,
2267
+ affinities: [
2268
+ "spirograph",
2269
+ "mandala",
2270
+ "flowerOfLife",
2271
+ "circle"
2272
+ ],
2273
+ category: "procedural",
2274
+ heroCandidate: true,
2275
+ bestStyles: [
2276
+ "stroke-only",
2277
+ "fill-only",
2278
+ "watercolor"
2279
+ ]
2280
+ }
2281
+ };
2282
+ function $24064302523652b1$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
2283
+ const available = shapeNames.filter((s)=>$24064302523652b1$export$4343b39fe47bd82c[s]);
2284
+ // Pick a seed shape — tier 1 shapes that are hero candidates
2285
+ const heroPool = available.filter((s)=>$24064302523652b1$export$4343b39fe47bd82c[s].tier === 1 && $24064302523652b1$export$4343b39fe47bd82c[s].heroCandidate);
2286
+ const seedShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : available[Math.floor(rng() * available.length)];
2287
+ const seedProfile = $24064302523652b1$export$4343b39fe47bd82c[seedShape];
2288
+ // Primary: seed shape + its direct affinities (tier 1-2 only)
2289
+ const primaryCandidates = [
2290
+ seedShape,
2291
+ ...seedProfile.affinities
2292
+ ].filter((s)=>available.includes(s)).filter((s)=>$24064302523652b1$export$4343b39fe47bd82c[s].tier <= 2);
2293
+ const primary = [
2294
+ ...new Set(primaryCandidates)
2295
+ ].slice(0, 5);
2296
+ // Supporting: affinities of affinities, plus same-category shapes
2297
+ const supportingSet = new Set();
2298
+ for (const p of primary){
2299
+ const profile = $24064302523652b1$export$4343b39fe47bd82c[p];
2300
+ if (!profile) continue;
2301
+ for (const aff of profile.affinities)if (available.includes(aff) && !primary.includes(aff)) supportingSet.add(aff);
2302
+ }
2303
+ // Add same-category tier 1-2 shapes
2304
+ for (const s of available){
2305
+ const p = $24064302523652b1$export$4343b39fe47bd82c[s];
2306
+ if (p.category === seedProfile.category && p.tier <= 2 && !primary.includes(s)) supportingSet.add(s);
2307
+ }
2308
+ const supporting = [
2309
+ ...supportingSet
2310
+ ].slice(0, 6);
2311
+ // Accents: tier 1 shapes from other categories for contrast
2312
+ const usedCategories = new Set([
2313
+ ...primary,
2314
+ ...supporting
2315
+ ].map((s)=>$24064302523652b1$export$4343b39fe47bd82c[s]?.category));
2316
+ const accentCandidates = available.filter((s)=>!primary.includes(s) && !supporting.includes(s) && $24064302523652b1$export$4343b39fe47bd82c[s].tier <= 2 && !usedCategories.has($24064302523652b1$export$4343b39fe47bd82c[s].category));
2317
+ // Shuffle and take a few
2318
+ const accents = [];
2319
+ const shuffled = [
2320
+ ...accentCandidates
2321
+ ];
2322
+ for(let i = shuffled.length - 1; i > 0; i--){
2323
+ const j = Math.floor(rng() * (i + 1));
2324
+ [shuffled[i], shuffled[j]] = [
2325
+ shuffled[j],
2326
+ shuffled[i]
2327
+ ];
2328
+ }
2329
+ accents.push(...shuffled.slice(0, 3));
2330
+ // For certain archetypes, bias the palette
2331
+ if (archetypeName === "geometric-precision") // Remove blobs and organic shapes from primary
2332
+ return {
2333
+ primary: primary.filter((s)=>$24064302523652b1$export$4343b39fe47bd82c[s]?.category !== "procedural" || s === "ngon"),
2334
+ supporting: supporting.filter((s)=>s !== "blob"),
2335
+ accents: accents
2336
+ };
2337
+ if (archetypeName === "organic-flow") {
2338
+ // Boost procedural/organic shapes
2339
+ const organicBoost = available.filter((s)=>[
2340
+ "blob",
2341
+ "superellipse",
2342
+ "waveRing",
2343
+ "rose"
2344
+ ].includes(s) && !primary.includes(s));
2345
+ return {
2346
+ primary: [
2347
+ ...primary,
2348
+ ...organicBoost.slice(0, 2)
2349
+ ],
2350
+ supporting: supporting,
2351
+ accents: accents
2352
+ };
2353
+ }
2354
+ return {
2355
+ primary: primary,
2356
+ supporting: supporting,
2357
+ accents: accents
2358
+ };
2359
+ }
2360
+ function $24064302523652b1$export$3c37d9a045754d0e(palette, rng, sizeFraction) {
2361
+ // Filter each tier by size constraints
2362
+ const validPrimary = palette.primary.filter((s)=>{
2363
+ const p = $24064302523652b1$export$4343b39fe47bd82c[s];
2364
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2365
+ });
2366
+ const validSupporting = palette.supporting.filter((s)=>{
2367
+ const p = $24064302523652b1$export$4343b39fe47bd82c[s];
2368
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2369
+ });
2370
+ const validAccents = palette.accents.filter((s)=>{
2371
+ const p = $24064302523652b1$export$4343b39fe47bd82c[s];
2372
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
2373
+ });
2374
+ const roll = rng();
2375
+ if (roll < 0.60 && validPrimary.length > 0) return validPrimary[Math.floor(rng() * validPrimary.length)];
2376
+ if (roll < 0.90 && validSupporting.length > 0) return validSupporting[Math.floor(rng() * validSupporting.length)];
2377
+ if (validAccents.length > 0) return validAccents[Math.floor(rng() * validAccents.length)];
2378
+ // Fallback: any valid primary or supporting
2379
+ const fallback = [
2380
+ ...validPrimary,
2381
+ ...validSupporting
2382
+ ];
2383
+ if (fallback.length > 0) return fallback[Math.floor(rng() * fallback.length)];
2384
+ // Ultimate fallback
2385
+ return palette.primary[0] || "circle";
2386
+ }
2387
+ function $24064302523652b1$export$ab873bb6fb56c1a8(shapeName, layerStyle, rng) {
2388
+ const profile = $24064302523652b1$export$4343b39fe47bd82c[shapeName];
2389
+ if (!profile || rng() > 0.7) return layerStyle;
2390
+ return profile.bestStyles[Math.floor(rng() * profile.bestStyles.length)];
2391
+ }
2392
+
2393
+
1419
2394
 
1420
2395
  /**
1421
2396
  * Configuration options for image generation.
@@ -1630,6 +2605,69 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
1630
2605
  sizePower: 2.5,
1631
2606
  invertForeground: false
1632
2607
  },
2608
+ {
2609
+ name: "watercolor-wash",
2610
+ gridSize: 3,
2611
+ layers: 3,
2612
+ baseOpacity: 0.25,
2613
+ opacityReduction: 0.03,
2614
+ minShapeSize: 200,
2615
+ maxShapeSize: 700,
2616
+ backgroundStyle: "radial-light",
2617
+ paletteMode: "harmonious",
2618
+ preferredStyles: [
2619
+ "watercolor",
2620
+ "fill-only",
2621
+ "incomplete"
2622
+ ],
2623
+ flowLineMultiplier: 0.5,
2624
+ heroShape: false,
2625
+ glowMultiplier: 0.3,
2626
+ sizePower: 0.6,
2627
+ invertForeground: false
2628
+ },
2629
+ {
2630
+ name: "op-art",
2631
+ gridSize: 8,
2632
+ layers: 2,
2633
+ baseOpacity: 0.95,
2634
+ opacityReduction: 0.05,
2635
+ minShapeSize: 20,
2636
+ maxShapeSize: 200,
2637
+ backgroundStyle: "solid-light",
2638
+ paletteMode: "high-contrast",
2639
+ preferredStyles: [
2640
+ "fill-and-stroke",
2641
+ "stroke-only",
2642
+ "dashed"
2643
+ ],
2644
+ flowLineMultiplier: 0,
2645
+ heroShape: false,
2646
+ glowMultiplier: 0,
2647
+ sizePower: 0.4,
2648
+ invertForeground: false
2649
+ },
2650
+ {
2651
+ name: "collage",
2652
+ gridSize: 4,
2653
+ layers: 3,
2654
+ baseOpacity: 0.9,
2655
+ opacityReduction: 0.08,
2656
+ minShapeSize: 80,
2657
+ maxShapeSize: 500,
2658
+ backgroundStyle: "solid-light",
2659
+ paletteMode: "duotone",
2660
+ preferredStyles: [
2661
+ "fill-and-stroke",
2662
+ "fill-only",
2663
+ "double-stroke"
2664
+ ],
2665
+ flowLineMultiplier: 0,
2666
+ heroShape: true,
2667
+ glowMultiplier: 0,
2668
+ sizePower: 0.7,
2669
+ invertForeground: false
2670
+ },
1633
2671
  {
1634
2672
  name: "classic",
1635
2673
  gridSize: 5,
@@ -1657,26 +2695,7 @@ function $3faa2521b78398cf$export$f1142fd7da4d6590(rng) {
1657
2695
  }
1658
2696
 
1659
2697
 
1660
- // ── Shape categories for weighted selection ─────────────────────────
1661
- const $b623126c6e9cbb71$var$BASIC_SHAPES = [
1662
- "circle",
1663
- "square",
1664
- "triangle",
1665
- "hexagon",
1666
- "diamond",
1667
- "cube"
1668
- ];
1669
- const $b623126c6e9cbb71$var$COMPLEX_SHAPES = [
1670
- "star",
1671
- "jacked-star",
1672
- "heart",
1673
- "platonicSolid",
1674
- "fibonacciSpiral",
1675
- "islamicPattern",
1676
- "celticKnot",
1677
- "merkaba",
1678
- "fractal"
1679
- ];
2698
+ // ── Shape categories for weighted selection (legacy fallback) ───────
1680
2699
  const $b623126c6e9cbb71$var$SACRED_SHAPES = [
1681
2700
  "mandala",
1682
2701
  "flowerOfLife",
@@ -1695,21 +2714,6 @@ const $b623126c6e9cbb71$var$COMPOSITION_MODES = [
1695
2714
  "grid-subdivision",
1696
2715
  "clustered"
1697
2716
  ];
1698
- // ── Helper: pick shape with layer-aware weighting ───────────────────
1699
- function $b623126c6e9cbb71$var$pickShape(rng, layerRatio, shapeNames) {
1700
- const basicW = 1 - layerRatio * 0.6;
1701
- const complexW = 0.3 + layerRatio * 0.3;
1702
- const sacredW = 0.1 + layerRatio * 0.4;
1703
- const total = basicW + complexW + sacredW;
1704
- const roll = rng() * total;
1705
- let pool;
1706
- if (roll < basicW) pool = $b623126c6e9cbb71$var$BASIC_SHAPES;
1707
- else if (roll < basicW + complexW) pool = $b623126c6e9cbb71$var$COMPLEX_SHAPES;
1708
- else pool = $b623126c6e9cbb71$var$SACRED_SHAPES;
1709
- const available = pool.filter((s)=>shapeNames.includes(s));
1710
- if (available.length === 0) return shapeNames[Math.floor(rng() * shapeNames.length)];
1711
- return available[Math.floor(rng() * available.length)];
1712
- }
1713
2717
  // ── Helper: get position based on composition mode ──────────────────
1714
2718
  function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
1715
2719
  switch(mode){
@@ -1768,22 +2772,28 @@ function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height,
1768
2772
  };
1769
2773
  }
1770
2774
  }
1771
- // ── Helper: positional color blending ───────────────────────────────
1772
- function $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, colors, rng) {
1773
- const nx = x / width;
1774
- const ny = y / height;
1775
- const posIndex = (nx * 0.6 + ny * 0.4) * (colors.length - 1);
1776
- const baseIdx = Math.floor(posIndex) % colors.length;
1777
- return (0, $9d614e7d77fc2947$export$59539d800dbe6858)(colors[baseIdx], rng, 0.08);
2775
+ // ── Helper: positional color from hierarchy ─────────────────────────
2776
+ function $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, hierarchy, rng) {
2777
+ // Blend position into color selection — shapes near center lean dominant
2778
+ const distFromCenter = Math.hypot(x - width / 2, y - height / 2) / Math.hypot(width / 2, height / 2);
2779
+ // Center = more dominant, edges = more accent
2780
+ if (distFromCenter < 0.35) return (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(hierarchy.dominant, rng, 10, 0.08);
2781
+ else if (distFromCenter < 0.7) return (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(hierarchy, rng), rng, 8, 0.06);
2782
+ else {
2783
+ // Edges: bias toward secondary/accent
2784
+ const roll = rng();
2785
+ const color = roll < 0.4 ? hierarchy.secondary : roll < 0.75 ? hierarchy.accent : hierarchy.dominant;
2786
+ return (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(color, rng, 12, 0.08);
2787
+ }
1778
2788
  }
1779
- // ── Helper: check if a position is inside a void zone (Feature E) ───
2789
+ // ── Helper: check if a position is inside a void zone ───────────────
1780
2790
  function $b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones) {
1781
2791
  for (const zone of voidZones){
1782
2792
  if (Math.hypot(x - zone.x, y - zone.y) < zone.radius) return true;
1783
2793
  }
1784
2794
  return false;
1785
2795
  }
1786
- // ── Helper: density check for negative space (Feature E) ────────────
2796
+ // ── Helper: density check ───────────────────────────────────────────
1787
2797
  function $b623126c6e9cbb71$var$localDensity(x, y, positions, radius) {
1788
2798
  let count = 0;
1789
2799
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -1879,7 +2889,15 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1879
2889
  const [bgStart, bgEnd] = colorScheme.getBackgroundColorsByMode(archetype.paletteMode);
1880
2890
  const tempMode = colorScheme.getTemperatureMode();
1881
2891
  const fgTempTarget = tempMode === "warm-bg" ? "cool" : tempMode === "cool-bg" ? "warm" : null;
2892
+ // ── 0b. Color hierarchy — dominant/secondary/accent weighting ──
2893
+ const colorHierarchy = (0, $9d614e7d77fc2947$export$fabac4600b87056)(colors, rng);
2894
+ // ── 0c. Shape palette — curated shapes that work well together ──
1882
2895
  const shapeNames = Object.keys((0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5));
2896
+ const shapePalette = (0, $24064302523652b1$export$4a95df8944b5033b)(rng, shapeNames, archetype.name);
2897
+ // ── 0d. Color grading — unified tone for the whole image ───────
2898
+ const colorGrade = (0, $9d614e7d77fc2947$export$6d1620b367f86f7a)(rng);
2899
+ // ── 0e. Light direction — consistent shadow angle ──────────────
2900
+ const lightAngle = rng() * Math.PI * 2;
1883
2901
  const scaleFactor = Math.min(width, height) / 1024;
1884
2902
  const adjustedMinSize = minShapeSize * scaleFactor;
1885
2903
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -1888,25 +2906,45 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1888
2906
  // ── 1. Background ──────────────────────────────────────────────
1889
2907
  const bgRadius = Math.hypot(cx, cy);
1890
2908
  $b623126c6e9cbb71$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
1891
- // ── 1b. Layered background (Feature G) ─────────────────────────
1892
- // Draw large, very faint shapes to give the background texture
2909
+ // Gradient mesh overlay 3-4 color control points for richer backgrounds
2910
+ const meshPoints = 3 + Math.floor(rng() * 2);
2911
+ ctx.globalCompositeOperation = "soft-light";
2912
+ for(let i = 0; i < meshPoints; i++){
2913
+ const mx = rng() * width;
2914
+ const my = rng() * height;
2915
+ const mRadius = Math.min(width, height) * (0.3 + rng() * 0.4);
2916
+ const mColor = (0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng);
2917
+ const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
2918
+ grad.addColorStop(0, (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
2919
+ grad.addColorStop(1, "rgba(0,0,0,0)");
2920
+ ctx.globalAlpha = 1;
2921
+ ctx.fillStyle = grad;
2922
+ ctx.fillRect(0, 0, width, height);
2923
+ }
2924
+ ctx.globalCompositeOperation = "source-over";
2925
+ // Compute average background luminance for contrast enforcement
2926
+ const bgLum = ((0, $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
2927
+ // ── 1b. Layered background — archetype-coherent shapes ─────────
1893
2928
  const bgShapeCount = 3 + Math.floor(rng() * 4);
1894
2929
  ctx.globalCompositeOperation = "soft-light";
1895
2930
  for(let i = 0; i < bgShapeCount; i++){
1896
2931
  const bx = rng() * width;
1897
2932
  const by = rng() * height;
1898
2933
  const bSize = width * 0.3 + rng() * width * 0.5;
1899
- const bColor = colors[Math.floor(rng() * colors.length)];
2934
+ const bColor = (0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng);
1900
2935
  ctx.globalAlpha = 0.03 + rng() * 0.05;
1901
2936
  ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(bColor, 0.15);
1902
2937
  ctx.beginPath();
1903
- ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
2938
+ // Use archetype-appropriate background shapes
2939
+ if (archetype.name === "geometric-precision" || archetype.name === "op-art") // Rectangular shapes for geometric archetypes
2940
+ ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
2941
+ else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
1904
2942
  ctx.fill();
1905
2943
  }
1906
2944
  // Subtle concentric rings from center
1907
2945
  const ringCount = 2 + Math.floor(rng() * 3);
1908
2946
  ctx.globalAlpha = 0.02 + rng() * 0.03;
1909
- ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colors[0], 0.1);
2947
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
1910
2948
  ctx.lineWidth = 1 * scaleFactor;
1911
2949
  for(let i = 1; i <= ringCount; i++){
1912
2950
  const r = Math.min(width, height) * 0.15 * i;
@@ -1920,7 +2958,6 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1920
2958
  const symRoll = rng();
1921
2959
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
1922
2960
  // ── 3. Focal points + void zones ───────────────────────────────
1923
- // Rule-of-thirds intersection points for intentional composition
1924
2961
  const THIRDS_POINTS = [
1925
2962
  {
1926
2963
  x: 1 / 3,
@@ -1941,10 +2978,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1941
2978
  ];
1942
2979
  const numFocal = 1 + Math.floor(rng() * 2);
1943
2980
  const focalPoints = [];
1944
- for(let f = 0; f < numFocal; f++)// 70% chance to snap to a rule-of-thirds point, 30% free placement
1945
- if (rng() < 0.7) {
2981
+ for(let f = 0; f < numFocal; f++)if (rng() < 0.7) {
1946
2982
  const tp = THIRDS_POINTS[Math.floor(rng() * THIRDS_POINTS.length)];
1947
- // Small jitter around the thirds point so it's not robotic
1948
2983
  focalPoints.push({
1949
2984
  x: width * (tp.x + (rng() - 0.5) * 0.08),
1950
2985
  y: height * (tp.y + (rng() - 0.5) * 0.08),
@@ -1955,7 +2990,6 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1955
2990
  y: height * (0.2 + rng() * 0.6),
1956
2991
  strength: 0.3 + rng() * 0.4
1957
2992
  });
1958
- // Feature E: 1-2 void zones where shapes are sparse (negative space)
1959
2993
  const numVoids = Math.floor(rng() * 2) + 1;
1960
2994
  const voidZones = [];
1961
2995
  for(let v = 0; v < numVoids; v++)voidZones.push({
@@ -1987,20 +3021,24 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1987
3021
  }
1988
3022
  // Track all placed shapes for density checks and connecting curves
1989
3023
  const shapePositions = [];
3024
+ // Hero avoidance radius — shapes near the hero orient toward it
3025
+ let heroCenter = null;
1990
3026
  // ── 4b. Hero shape — a dominant focal element ───────────────────
1991
3027
  if (archetype.heroShape && rng() < 0.6) {
1992
3028
  const heroFocal = focalPoints[0];
3029
+ // Use shape palette hero candidates
1993
3030
  const heroPool = [
1994
- ...$b623126c6e9cbb71$var$SACRED_SHAPES,
1995
- "fibonacciSpiral",
1996
- "merkaba",
1997
- "fractal"
1998
- ];
1999
- const heroShape = heroPool.filter((s)=>shapeNames.includes(s))[Math.floor(rng() * heroPool.filter((s)=>shapeNames.includes(s)).length)] || shapeNames[Math.floor(rng() * shapeNames.length)];
3031
+ ...shapePalette.primary,
3032
+ ...shapePalette.supporting
3033
+ ].filter((s)=>(0, $24064302523652b1$export$4343b39fe47bd82c)[s]?.heroCandidate && shapeNames.includes(s));
3034
+ const heroShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : shapeNames[Math.floor(rng() * shapeNames.length)];
2000
3035
  const heroSize = adjustedMaxSize * (0.8 + rng() * 0.5);
2001
3036
  const heroRotation = rng() * 360;
2002
- const heroFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05), 0.15 + rng() * 0.2);
2003
- const heroStroke = (0, $9d614e7d77fc2947$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05);
3037
+ const heroFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(colorHierarchy.dominant, rng, 6, 0.05), bgLum), 0.15 + rng() * 0.2);
3038
+ const heroStroke = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(colorHierarchy.accent, rng, 6, 0.05), bgLum);
3039
+ // Get best style for this hero shape
3040
+ const heroProfile = (0, $24064302523652b1$export$4343b39fe47bd82c)[heroShape];
3041
+ const heroStyle = heroProfile ? heroProfile.bestStyles[Math.floor(rng() * heroProfile.bestStyles.length)] : rng() < 0.4 ? "watercolor" : "fill-and-stroke";
2004
3042
  ctx.globalAlpha = 0.5 + rng() * 0.2;
2005
3043
  (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, heroShape, heroFocal.x, heroFocal.y, {
2006
3044
  fillColor: heroFill,
@@ -2011,14 +3049,20 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2011
3049
  proportionType: "GOLDEN_RATIO",
2012
3050
  glowRadius: (12 + rng() * 20) * scaleFactor,
2013
3051
  glowColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(heroStroke, 0.4),
2014
- gradientFillEnd: (0, $9d614e7d77fc2947$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1),
2015
- renderStyle: rng() < 0.4 ? "watercolor" : "fill-and-stroke",
3052
+ gradientFillEnd: (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
3053
+ renderStyle: heroStyle,
2016
3054
  rng: rng
2017
3055
  });
2018
- shapePositions.push({
3056
+ heroCenter = {
2019
3057
  x: heroFocal.x,
2020
3058
  y: heroFocal.y,
2021
3059
  size: heroSize
3060
+ };
3061
+ shapePositions.push({
3062
+ x: heroFocal.x,
3063
+ y: heroFocal.y,
3064
+ size: heroSize,
3065
+ shape: heroShape
2022
3066
  });
2023
3067
  }
2024
3068
  // ── 5. Shape layers ────────────────────────────────────────────
@@ -2029,41 +3073,52 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2029
3073
  const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
2030
3074
  const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
2031
3075
  const layerSizeScale = 1 - layer * 0.15;
2032
- // Feature B: per-layer blend mode
3076
+ // Per-layer blend mode
2033
3077
  const layerBlend = (0, $9beb8f41637c29fd$export$7bb7bff4e26fa06b)(rng);
2034
3078
  ctx.globalCompositeOperation = layerBlend;
2035
- // Feature C: per-layer render style bias — prefer archetype styles
3079
+ // Per-layer render style bias — prefer archetype styles
2036
3080
  const layerRenderStyle = rng() < 0.6 ? archetype.preferredStyles[Math.floor(rng() * archetype.preferredStyles.length)] : (0, $9beb8f41637c29fd$export$9fd4e64b2acd410e)(rng);
2037
- // Feature D: atmospheric desaturation for later layers
2038
- const atmosphericDesat = layerRatio * 0.3; // 0 for first layer, up to 0.3 for last
3081
+ // Atmospheric desaturation for later layers
3082
+ const atmosphericDesat = layerRatio * 0.3;
2039
3083
  for(let i = 0; i < numShapes; i++){
2040
3084
  // Position from composition mode, then focal bias
2041
3085
  const rawPos = $b623126c6e9cbb71$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
2042
3086
  const [x, y] = applyFocalBias(rawPos.x, rawPos.y);
2043
- // Feature E: skip shapes in void zones, reduce in dense areas
3087
+ // Skip shapes in void zones, reduce in dense areas
2044
3088
  if ($b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones)) {
2045
- // 85% chance to skip — allows a few shapes to bleed in
2046
3089
  if (rng() < 0.85) continue;
2047
3090
  }
2048
3091
  if ($b623126c6e9cbb71$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
2049
- if (rng() < 0.6) continue; // thin out dense areas
3092
+ if (rng() < 0.6) continue;
2050
3093
  }
2051
- // Weighted shape selection
2052
- const shape = $b623126c6e9cbb71$var$pickShape(rng, layerRatio, shapeNames);
2053
3094
  // Power distribution for size — archetype controls the curve
2054
3095
  const sizeT = Math.pow(rng(), archetype.sizePower);
2055
3096
  const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
3097
+ // Size fraction for affinity-aware shape selection
3098
+ const sizeFraction = size / adjustedMaxSize;
3099
+ // Palette-driven shape selection (replaces naive pickShape)
3100
+ const shape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, sizeFraction);
2056
3101
  // Flow-field rotation in flow-field mode, random otherwise
2057
- const rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
2058
- // Positional color blending + jitter
2059
- let fillBase = $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, colors, rng);
2060
- const strokeBase = colors[Math.floor(rng() * colors.length)];
2061
- // Feature D: desaturate colors on later layers for depth
3102
+ let rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
3103
+ // Hero avoidance: shapes near the hero orient toward it
3104
+ if (heroCenter) {
3105
+ const distToHero = Math.hypot(x - heroCenter.x, y - heroCenter.y);
3106
+ const heroInfluence = heroCenter.size * 1.5;
3107
+ if (distToHero < heroInfluence && distToHero > 0) {
3108
+ const angleToHero = Math.atan2(heroCenter.y - y, heroCenter.x - x) * 180 / Math.PI;
3109
+ const blendFactor = 1 - distToHero / heroInfluence;
3110
+ rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
3111
+ }
3112
+ }
3113
+ // Positional color from hierarchy + jitter
3114
+ let fillBase = $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
3115
+ const strokeBase = (0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng);
3116
+ // Desaturate colors on later layers for depth
2062
3117
  if (atmosphericDesat > 0) fillBase = (0, $9d614e7d77fc2947$export$fb75607d98509d9)(fillBase, atmosphericDesat);
2063
3118
  // Temperature contrast: shift foreground shapes opposite to background
2064
3119
  if (fgTempTarget) fillBase = (0, $9d614e7d77fc2947$export$51ea55f869b7e0d3)(fillBase, fgTempTarget, 0.15 + layerRatio * 0.1);
2065
- const fillColor = (0, $9d614e7d77fc2947$export$59539d800dbe6858)(fillBase, rng, 0.06);
2066
- const strokeColor = (0, $9d614e7d77fc2947$export$59539d800dbe6858)(strokeBase, rng, 0.05);
3120
+ const fillColor = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(fillBase, rng, 6, 0.05), bgLum);
3121
+ const strokeColor = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
2067
3122
  // Semi-transparent fill
2068
3123
  const fillAlpha = 0.2 + rng() * 0.5;
2069
3124
  const transparentFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, fillAlpha);
@@ -2077,12 +3132,16 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2077
3132
  const glowRadius = hasGlow ? (8 + rng() * 20) * scaleFactor : 0;
2078
3133
  // Gradient fill on ~30%
2079
3134
  const hasGradient = rng() < 0.3;
2080
- const gradientEnd = hasGradient ? (0, $9d614e7d77fc2947$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1) : undefined;
2081
- // Feature C: per-shape render style (70% use layer style, 30% pick their own)
2082
- const shapeRenderStyle = rng() < 0.7 ? layerRenderStyle : (0, $9beb8f41637c29fd$export$9fd4e64b2acd410e)(rng);
2083
- // Feature F: organic edge jitter — applied via watercolor style on ~15% of shapes
3135
+ const gradientEnd = hasGradient ? (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1) : undefined;
3136
+ // Affinity-aware render style selection
3137
+ const shapeRenderStyle = (0, $24064302523652b1$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
3138
+ // Organic edge jitter — applied via watercolor style on ~15% of shapes
2084
3139
  const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
2085
3140
  const finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
3141
+ // Consistent light direction — subtle shadow offset
3142
+ const shadowDist = hasGlow ? 0 : size * 0.02;
3143
+ const shadowOffX = shadowDist * Math.cos(lightAngle);
3144
+ const shadowOffY = shadowDist * Math.sin(lightAngle);
2086
3145
  (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
2087
3146
  fillColor: transparentFill,
2088
3147
  strokeColor: strokeColor,
@@ -2090,8 +3149,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2090
3149
  size: size,
2091
3150
  rotation: rotation,
2092
3151
  proportionType: "GOLDEN_RATIO",
2093
- glowRadius: glowRadius,
2094
- glowColor: hasGlow ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.6) : undefined,
3152
+ glowRadius: glowRadius || (shadowDist > 0 ? shadowDist * 2 : 0),
3153
+ glowColor: hasGlow ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
2095
3154
  gradientFillEnd: gradientEnd,
2096
3155
  renderStyle: finalRenderStyle,
2097
3156
  rng: rng
@@ -2099,18 +3158,51 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2099
3158
  shapePositions.push({
2100
3159
  x: x,
2101
3160
  y: y,
2102
- size: size
3161
+ size: size,
3162
+ shape: shape
2103
3163
  });
2104
- // ── 5b. Recursive nesting ──────────────────────────────────
3164
+ // ── 5b. Size echo — large shapes spawn trailing smaller copies ──
3165
+ if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
3166
+ const echoCount = 2 + Math.floor(rng() * 2);
3167
+ const echoAngle = rng() * Math.PI * 2;
3168
+ for(let e = 0; e < echoCount; e++){
3169
+ const echoScale = 0.3 - e * 0.08;
3170
+ const echoDist = size * (0.6 + e * 0.4);
3171
+ const echoX = x + Math.cos(echoAngle) * echoDist;
3172
+ const echoY = y + Math.sin(echoAngle) * echoDist;
3173
+ const echoSize = size * Math.max(0.1, echoScale);
3174
+ if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
3175
+ ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
3176
+ (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
3177
+ fillColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
3178
+ strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.4),
3179
+ strokeWidth: strokeWidth * 0.6,
3180
+ size: echoSize,
3181
+ rotation: rotation + (e + 1) * 15,
3182
+ proportionType: "GOLDEN_RATIO",
3183
+ renderStyle: finalRenderStyle,
3184
+ rng: rng
3185
+ });
3186
+ shapePositions.push({
3187
+ x: echoX,
3188
+ y: echoY,
3189
+ size: echoSize,
3190
+ shape: shape
3191
+ });
3192
+ }
3193
+ }
3194
+ // ── 5c. Recursive nesting ──────────────────────────────────
2105
3195
  if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
2106
3196
  const innerCount = 1 + Math.floor(rng() * 3);
2107
3197
  for(let n = 0; n < innerCount; n++){
2108
- const innerShape = $b623126c6e9cbb71$var$pickShape(rng, Math.min(1, layerRatio + 0.3), shapeNames);
3198
+ // Pick inner shape from palette affinities
3199
+ const innerSizeFraction = size * 0.25 / adjustedMaxSize;
3200
+ const innerShape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
2109
3201
  const innerSize = size * (0.15 + rng() * 0.25);
2110
3202
  const innerOffX = (rng() - 0.5) * size * 0.4;
2111
3203
  const innerOffY = (rng() - 0.5) * size * 0.4;
2112
3204
  const innerRot = rng() * 360;
2113
- const innerFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1), 0.3 + rng() * 0.4);
3205
+ const innerFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
2114
3206
  ctx.globalAlpha = layerOpacity * 0.7;
2115
3207
  (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, innerShape, x + innerOffX, y + innerOffY, {
2116
3208
  fillColor: innerFill,
@@ -2119,7 +3211,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2119
3211
  size: innerSize,
2120
3212
  rotation: innerRot,
2121
3213
  proportionType: "GOLDEN_RATIO",
2122
- renderStyle: shapeRenderStyle,
3214
+ renderStyle: (0, $24064302523652b1$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng),
2123
3215
  rng: rng
2124
3216
  });
2125
3217
  }
@@ -2128,7 +3220,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2128
3220
  }
2129
3221
  // Reset blend mode for post-processing passes
2130
3222
  ctx.globalCompositeOperation = "source-over";
2131
- // ── 6. Flow-line pass (Feature H: tapered brush strokes) ───────
3223
+ // ── 6. Flow-line pass variable color, branching, pressure ────
2132
3224
  const baseFlowLines = 6 + Math.floor(rng() * 10);
2133
3225
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
2134
3226
  for(let i = 0; i < numFlowLines; i++){
@@ -2137,9 +3229,13 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2137
3229
  const steps = 30 + Math.floor(rng() * 40);
2138
3230
  const stepLen = (3 + rng() * 5) * scaleFactor;
2139
3231
  const startWidth = (1 + rng() * 3) * scaleFactor;
2140
- const lineColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colors[Math.floor(rng() * colors.length)], 0.4);
3232
+ // Variable color: interpolate between two hierarchy colors along the stroke
3233
+ const lineColorStart = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
3234
+ const lineColorEnd = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
2141
3235
  const lineAlpha = 0.06 + rng() * 0.1;
2142
- // Draw as individual segments with tapering width
3236
+ // Pressure simulation: sinusoidal width variation
3237
+ const pressureFreq = 2 + rng() * 4;
3238
+ const pressurePhase = rng() * Math.PI * 2;
2143
3239
  let prevX = fx;
2144
3240
  let prevY = fy;
2145
3241
  for(let s = 0; s < steps; s++){
@@ -2147,37 +3243,61 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2147
3243
  fx += Math.cos(angle) * stepLen;
2148
3244
  fy += Math.sin(angle) * stepLen;
2149
3245
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
2150
- // Taper: thick at start, thin at end
2151
- const taper = 1 - s / steps * 0.8;
3246
+ const t = s / steps;
3247
+ // Taper + pressure
3248
+ const taper = 1 - t * 0.8;
3249
+ const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
2152
3250
  ctx.globalAlpha = lineAlpha * taper;
3251
+ // Interpolate color along stroke
3252
+ const lineColor = t < 0.5 ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
2153
3253
  ctx.strokeStyle = lineColor;
2154
- ctx.lineWidth = startWidth * taper;
3254
+ ctx.lineWidth = startWidth * taper * pressure;
2155
3255
  ctx.lineCap = "round";
2156
3256
  ctx.beginPath();
2157
3257
  ctx.moveTo(prevX, prevY);
2158
3258
  ctx.lineTo(fx, fy);
2159
3259
  ctx.stroke();
3260
+ // Branching: ~12% chance per step to spawn a thinner child stroke
3261
+ if (rng() < 0.12 && s > 5 && s < steps - 10) {
3262
+ const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
3263
+ let bx = fx;
3264
+ let by = fy;
3265
+ let bPrevX = fx;
3266
+ let bPrevY = fy;
3267
+ const branchSteps = 5 + Math.floor(rng() * 10);
3268
+ const branchWidth = startWidth * taper * 0.4;
3269
+ for(let bs = 0; bs < branchSteps; bs++){
3270
+ const bAngle = branchAngle + (rng() - 0.5) * 0.2;
3271
+ bx += Math.cos(bAngle) * stepLen * 0.8;
3272
+ by += Math.sin(bAngle) * stepLen * 0.8;
3273
+ if (bx < 0 || bx > width || by < 0 || by > height) break;
3274
+ const bTaper = 1 - bs / branchSteps * 0.9;
3275
+ ctx.globalAlpha = lineAlpha * taper * bTaper * 0.6;
3276
+ ctx.lineWidth = branchWidth * bTaper;
3277
+ ctx.beginPath();
3278
+ ctx.moveTo(bPrevX, bPrevY);
3279
+ ctx.lineTo(bx, by);
3280
+ ctx.stroke();
3281
+ bPrevX = bx;
3282
+ bPrevY = by;
3283
+ }
3284
+ }
2160
3285
  prevX = fx;
2161
3286
  prevY = fy;
2162
3287
  }
2163
3288
  }
2164
3289
  // ── 6b. Apply symmetry mirroring ─────────────────────────────────
2165
- // Mirror the rendered content (shapes + flow lines) before post-processing.
2166
- // Uses ctx.canvas which is available in both Node (@napi-rs/canvas) and browsers.
2167
3290
  if (symmetryMode !== "none") {
2168
3291
  const canvas = ctx.canvas;
2169
3292
  ctx.save();
2170
3293
  if (symmetryMode === "bilateral-x" || symmetryMode === "quad") {
2171
- // Mirror left half onto right half
2172
3294
  ctx.save();
2173
3295
  ctx.translate(width, 0);
2174
3296
  ctx.scale(-1, 1);
2175
- // Draw the left half (0 to cx) onto the mirrored right side
2176
3297
  ctx.drawImage(canvas, 0, 0, Math.ceil(cx), height, 0, 0, Math.ceil(cx), height);
2177
3298
  ctx.restore();
2178
3299
  }
2179
3300
  if (symmetryMode === "bilateral-y" || symmetryMode === "quad") {
2180
- // Mirror top half onto bottom half
2181
3301
  ctx.save();
2182
3302
  ctx.translate(0, height);
2183
3303
  ctx.scale(1, -1);
@@ -2200,7 +3320,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2200
3320
  }
2201
3321
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
2202
3322
  ctx.globalAlpha = 1;
2203
- const vignetteStrength = 0.25 + rng() * 0.2; // 25-45% edge darkening
3323
+ const vignetteStrength = 0.25 + rng() * 0.2;
2204
3324
  const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
2205
3325
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
2206
3326
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
@@ -2226,13 +3346,57 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
2226
3346
  const cpx = mx + -dy / (dist || 1) * bulge;
2227
3347
  const cpy = my + dx / (dist || 1) * bulge;
2228
3348
  ctx.globalAlpha = 0.06 + rng() * 0.1;
2229
- ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colors[Math.floor(rng() * colors.length)], 0.3);
3349
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
2230
3350
  ctx.beginPath();
2231
3351
  ctx.moveTo(a.x, a.y);
2232
3352
  ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);
2233
3353
  ctx.stroke();
2234
3354
  }
2235
3355
  }
3356
+ // ── 10. Post-processing ────────────────────────────────────────
3357
+ // 10a. Color grading — unified tone across the whole image
3358
+ // Apply as a semi-transparent overlay in the grade hue
3359
+ ctx.globalAlpha = colorGrade.intensity * 0.25;
3360
+ ctx.globalCompositeOperation = "soft-light";
3361
+ const gradeHsl = `hsl(${Math.round(colorGrade.hue)}, 40%, 50%)`;
3362
+ ctx.fillStyle = gradeHsl;
3363
+ ctx.fillRect(0, 0, width, height);
3364
+ ctx.globalCompositeOperation = "source-over";
3365
+ // 10b. Chromatic aberration — subtle RGB channel offset at edges
3366
+ // Only apply for neon/cosmic/ethereal archetypes where it fits
3367
+ const chromaArchetypes = [
3368
+ "neon-glow",
3369
+ "cosmic",
3370
+ "ethereal"
3371
+ ];
3372
+ if (chromaArchetypes.includes(archetype.name)) {
3373
+ const chromaOffset = Math.ceil(2 * scaleFactor);
3374
+ const canvas = ctx.canvas;
3375
+ // Shift red channel slightly
3376
+ ctx.globalAlpha = 0.03;
3377
+ ctx.globalCompositeOperation = "screen";
3378
+ ctx.drawImage(canvas, chromaOffset, 0, width, height, 0, 0, width, height);
3379
+ // Shift blue channel opposite
3380
+ ctx.drawImage(canvas, -chromaOffset, 0, width, height, 0, 0, width, height);
3381
+ ctx.globalCompositeOperation = "source-over";
3382
+ }
3383
+ // 10c. Bloom — soft glow on bright areas for neon/cosmic archetypes
3384
+ const bloomArchetypes = [
3385
+ "neon-glow",
3386
+ "cosmic"
3387
+ ];
3388
+ if (bloomArchetypes.includes(archetype.name)) {
3389
+ const canvas = ctx.canvas;
3390
+ ctx.globalAlpha = 0.08;
3391
+ ctx.globalCompositeOperation = "screen";
3392
+ // Draw the image slightly scaled up and blurred via shadow
3393
+ ctx.save();
3394
+ ctx.shadowBlur = 30 * scaleFactor;
3395
+ ctx.shadowColor = "rgba(255,255,255,0.3)";
3396
+ ctx.drawImage(canvas, 0, 0, width, height);
3397
+ ctx.restore();
3398
+ ctx.globalCompositeOperation = "source-over";
3399
+ }
2236
3400
  ctx.globalAlpha = 1;
2237
3401
  }
2238
3402