git-hash-art 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -27,17 +27,22 @@ $parcel$export(module.exports, "DEFAULT_CONFIG", () => $93cf69256c93baa9$export$
27
27
  * identically in Node (@napi-rs/canvas) and browsers.
28
28
  *
29
29
  * Generation pipeline:
30
- * 1. Background radial gradient from hash-derived dark palette
31
- * 1b. Layered background large faint shapes / subtle pattern for depth
32
- * 2. Composition modehash selects: radial, flow-field, spiral, grid-subdivision, or clustered
33
- * 3. Focal points + void zones (negative space)
34
- * 4. Flow field seed values
35
- * 5. Shape layers — blend modes, render styles, weighted selection,
36
- * focal-point placement, atmospheric depth, organic edges
30
+ * 0. Archetype selection + shape palette + color hierarchy
31
+ * 1. Backgroundstyle from archetype, gradient mesh for depth
32
+ * 1b. Layered background archetype-coherent shapes
33
+ * 2. Composition mode + symmetry
34
+ * 3. Focal points + void zones + hero avoidance field
35
+ * 4. Flow field
36
+ * 4b. Hero shape
37
+ * 5. Shape layers — palette-driven selection, affinity-aware styles,
38
+ * size echo, tangent placement, atmospheric depth
37
39
  * 5b. Recursive nesting
38
- * 6. Flow-line passtapered brush-stroke curves
39
- * 7. Noise texture overlay
40
- * 8. Organic connecting curves
40
+ * 6. Flow linesvariable color, branching, pressure simulation
41
+ * 6b. Symmetry mirroring
42
+ * 7. Noise texture
43
+ * 8. Vignette
44
+ * 9. Organic connecting curves
45
+ * 10. Post-processing — color grading, chromatic aberration, bloom
41
46
  */
42
47
  // declare module 'color-scheme';
43
48
 
@@ -411,6 +416,67 @@ function $d016ad53434219a1$export$f2121afcad3d553f(hex, alpha) {
411
416
  const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex);
412
417
  return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
413
418
  }
419
+ function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
420
+ if (colors.length < 3) return {
421
+ dominant: colors[0] || "#888888",
422
+ secondary: colors[1] || colors[0] || "#888888",
423
+ accent: colors[colors.length - 1] || "#888888",
424
+ all: colors
425
+ };
426
+ // Pick dominant as the color closest to the palette's average hue
427
+ const hsls = colors.map((c)=>$d016ad53434219a1$var$hexToHsl(c));
428
+ const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
429
+ let dominantIdx = 0;
430
+ let minDist = 360;
431
+ for(let i = 0; i < hsls.length; i++){
432
+ const d = Math.min(Math.abs(hsls[i][0] - avgHue), 360 - Math.abs(hsls[i][0] - avgHue));
433
+ if (d < minDist) {
434
+ minDist = d;
435
+ dominantIdx = i;
436
+ }
437
+ }
438
+ // Accent is the color most distant from dominant in hue
439
+ let accentIdx = 0;
440
+ let maxDist = 0;
441
+ for(let i = 0; i < hsls.length; i++){
442
+ if (i === dominantIdx) continue;
443
+ const d = Math.min(Math.abs(hsls[i][0] - hsls[dominantIdx][0]), 360 - Math.abs(hsls[i][0] - hsls[dominantIdx][0]));
444
+ if (d > maxDist) {
445
+ maxDist = d;
446
+ accentIdx = i;
447
+ }
448
+ }
449
+ // Secondary is the remaining color with highest saturation
450
+ let secondaryIdx = 0;
451
+ let maxSat = -1;
452
+ for(let i = 0; i < hsls.length; i++){
453
+ if (i === dominantIdx || i === accentIdx) continue;
454
+ if (hsls[i][1] > maxSat) {
455
+ maxSat = hsls[i][1];
456
+ secondaryIdx = i;
457
+ }
458
+ }
459
+ if (secondaryIdx === dominantIdx) secondaryIdx = accentIdx === 0 ? 1 : 0;
460
+ return {
461
+ dominant: colors[dominantIdx],
462
+ secondary: colors[secondaryIdx],
463
+ accent: colors[accentIdx],
464
+ all: colors
465
+ };
466
+ }
467
+ function $d016ad53434219a1$export$b49f62f0a99da0e8(hierarchy, rng) {
468
+ const roll = rng();
469
+ if (roll < 0.60) return hierarchy.dominant;
470
+ if (roll < 0.85) return hierarchy.secondary;
471
+ return hierarchy.accent;
472
+ }
473
+ function $d016ad53434219a1$export$18a34c25ea7e724b(hex, rng, hueAmount = 8, slAmount = 0.06) {
474
+ const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
475
+ const newH = (h + (rng() - 0.5) * hueAmount * 2 + 360) % 360;
476
+ const newS = Math.max(0, Math.min(1, s + (rng() - 0.5) * slAmount * 2));
477
+ const newL = Math.max(0, Math.min(1, l + (rng() - 0.5) * slAmount * 2));
478
+ return $d016ad53434219a1$var$hslToHex(newH, newS, newL);
479
+ }
414
480
  function $d016ad53434219a1$export$59539d800dbe6858(hex, rng, amount = 0.1) {
415
481
  const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex);
416
482
  const jit = ()=>(rng() - 0.5) * 2 * amount * 255;
@@ -450,6 +516,31 @@ function $d016ad53434219a1$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContra
450
516
  return $d016ad53434219a1$var$hslToHex(h, targetS, targetL);
451
517
  }
452
518
  }
519
+ function $d016ad53434219a1$export$4a3734b8c4b5c0e(hex, gradeHue, intensity) {
520
+ const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
521
+ // Blend hue toward the grade hue
522
+ const hueDiff = (gradeHue - h + 540) % 360 - 180;
523
+ const newH = (h + hueDiff * intensity * 0.3 + 360) % 360;
524
+ // Slightly unify saturation
525
+ const newS = Math.max(0, Math.min(1, s + (0.5 - s) * intensity * 0.15));
526
+ return $d016ad53434219a1$var$hslToHex(newH, newS, l);
527
+ }
528
+ function $d016ad53434219a1$export$6d1620b367f86f7a(rng) {
529
+ // Warm golden, cool blue, rosy, teal, amber
530
+ const GRADE_HUES = [
531
+ 40,
532
+ 220,
533
+ 340,
534
+ 175,
535
+ 30
536
+ ];
537
+ const hue = GRADE_HUES[Math.floor(rng() * GRADE_HUES.length)] + (rng() - 0.5) * 20;
538
+ const intensity = 0.15 + rng() * 0.25;
539
+ return {
540
+ hue: (hue + 360) % 360,
541
+ intensity: intensity
542
+ };
543
+ }
453
544
 
454
545
 
455
546
 
@@ -1240,35 +1331,32 @@ const $dd5df256f00f6199$export$c2fc138f94dd4b2a = {
1240
1331
  */ const $6222456bc073291c$export$580f80cfb9de73bc = (ctx, size, config)=>{
1241
1332
  const rng = config?.rng ?? Math.random;
1242
1333
  const r = size / 2;
1243
- const numPoints = 5 + Math.floor(rng() * 5); // 5-9 lobes
1334
+ const numPoints = 5 + Math.floor(rng() * 5);
1244
1335
  const points = [];
1245
1336
  for(let i = 0; i < numPoints; i++){
1246
1337
  const angle = i / numPoints * Math.PI * 2;
1247
- const jitter = 0.5 + rng() * 0.5; // radius varies 50-100%
1338
+ const jitter = 0.5 + rng() * 0.5;
1248
1339
  points.push({
1249
1340
  x: Math.cos(angle) * r * jitter,
1250
1341
  y: Math.sin(angle) * r * jitter
1251
1342
  });
1252
1343
  }
1253
1344
  ctx.beginPath();
1254
- // Start at midpoint between last and first point
1255
1345
  const last = points[points.length - 1];
1256
1346
  const first = points[0];
1257
1347
  ctx.moveTo((last.x + first.x) / 2, (last.y + first.y) / 2);
1258
1348
  for(let i = 0; i < numPoints; i++){
1259
1349
  const curr = points[i];
1260
1350
  const next = points[(i + 1) % numPoints];
1261
- const midX = (curr.x + next.x) / 2;
1262
- const midY = (curr.y + next.y) / 2;
1263
- ctx.quadraticCurveTo(curr.x, curr.y, midX, midY);
1351
+ ctx.quadraticCurveTo(curr.x, curr.y, (curr.x + next.x) / 2, (curr.y + next.y) / 2);
1264
1352
  }
1265
1353
  ctx.closePath();
1266
1354
  };
1267
1355
  const $6222456bc073291c$export$7a6094023f0902a6 = (ctx, size, config)=>{
1268
1356
  const rng = config?.rng ?? Math.random;
1269
1357
  const r = size / 2;
1270
- const sides = 3 + Math.floor(rng() * 10); // 3-12 sides
1271
- const jitterAmount = 0.1 + rng() * 0.4; // 10-50% vertex displacement
1358
+ const sides = 3 + Math.floor(rng() * 10);
1359
+ const jitterAmount = 0.1 + rng() * 0.4;
1272
1360
  ctx.beginPath();
1273
1361
  for(let i = 0; i < sides; i++){
1274
1362
  const angle = i / sides * Math.PI * 2 - Math.PI / 2;
@@ -1283,10 +1371,9 @@ const $6222456bc073291c$export$7a6094023f0902a6 = (ctx, size, config)=>{
1283
1371
  const $6222456bc073291c$export$ef56b4a8316e47d5 = (ctx, size, config)=>{
1284
1372
  const rng = config?.rng ?? Math.random;
1285
1373
  const r = size / 2;
1286
- // Frequency ratios small integers produce recognizable patterns
1287
- const freqA = 1 + Math.floor(rng() * 5); // 1-5
1288
- const freqB = 1 + Math.floor(rng() * 5); // 1-5
1289
- const phase = rng() * Math.PI; // phase offset
1374
+ const freqA = 1 + Math.floor(rng() * 5);
1375
+ const freqB = 1 + Math.floor(rng() * 5);
1376
+ const phase = rng() * Math.PI;
1290
1377
  const steps = 120;
1291
1378
  ctx.beginPath();
1292
1379
  for(let i = 0; i <= steps; i++){
@@ -1301,7 +1388,6 @@ const $6222456bc073291c$export$ef56b4a8316e47d5 = (ctx, size, config)=>{
1301
1388
  const $6222456bc073291c$export$1db9219b4f34658c = (ctx, size, config)=>{
1302
1389
  const rng = config?.rng ?? Math.random;
1303
1390
  const r = size / 2;
1304
- // Exponent range: 0.3 (spiky astroid) to 5 (rounded rectangle)
1305
1391
  const n = 0.3 + rng() * 4.7;
1306
1392
  const steps = 120;
1307
1393
  ctx.beginPath();
@@ -1309,7 +1395,6 @@ const $6222456bc073291c$export$1db9219b4f34658c = (ctx, size, config)=>{
1309
1395
  const t = i / steps * Math.PI * 2;
1310
1396
  const cosT = Math.cos(t);
1311
1397
  const sinT = Math.sin(t);
1312
- // Superellipse parametric form
1313
1398
  const x = Math.sign(cosT) * Math.pow(Math.abs(cosT), 2 / n) * r;
1314
1399
  const y = Math.sign(sinT) * Math.pow(Math.abs(sinT), 2 / n) * r;
1315
1400
  if (i === 0) ctx.moveTo(x, y);
@@ -1320,11 +1405,9 @@ const $6222456bc073291c$export$1db9219b4f34658c = (ctx, size, config)=>{
1320
1405
  const $6222456bc073291c$export$b027c64d22b01985 = (ctx, size, config)=>{
1321
1406
  const rng = config?.rng ?? Math.random;
1322
1407
  const scale = size / 2;
1323
- // R = outer radius, r = inner radius, d = pen distance from inner center
1324
1408
  const R = 1;
1325
- const r = 0.2 + rng() * 0.6; // 0.2-0.8
1326
- const d = 0.3 + rng() * 0.7; // 0.3-1.0
1327
- // Number of full rotations needed to close the curve
1409
+ const r = 0.2 + rng() * 0.6;
1410
+ const d = 0.3 + rng() * 0.7;
1328
1411
  const gcd = (a, b)=>{
1329
1412
  const ai = Math.round(a * 1000);
1330
1413
  const bi = Math.round(b * 1000);
@@ -1332,7 +1415,7 @@ const $6222456bc073291c$export$b027c64d22b01985 = (ctx, size, config)=>{
1332
1415
  return g(ai, bi) / 1000;
1333
1416
  };
1334
1417
  const period = r / gcd(R, r);
1335
- const maxT = Math.min(period, 10) * Math.PI * 2; // cap at 10 rotations
1418
+ const maxT = Math.min(period, 10) * Math.PI * 2;
1336
1419
  const steps = Math.min(600, Math.floor(maxT * 20));
1337
1420
  ctx.beginPath();
1338
1421
  for(let i = 0; i <= steps; i++){
@@ -1347,9 +1430,9 @@ const $6222456bc073291c$export$b027c64d22b01985 = (ctx, size, config)=>{
1347
1430
  const $6222456bc073291c$export$7608ccd03bfb705d = (ctx, size, config)=>{
1348
1431
  const rng = config?.rng ?? Math.random;
1349
1432
  const r = size / 2;
1350
- const rings = 2 + Math.floor(rng() * 4); // 2-5 rings
1351
- const freq = 3 + Math.floor(rng() * 12); // 3-14 waves per ring
1352
- const amp = 0.05 + rng() * 0.15; // 5-20% of radius
1433
+ const rings = 2 + Math.floor(rng() * 4);
1434
+ const freq = 3 + Math.floor(rng() * 12);
1435
+ const amp = 0.05 + rng() * 0.15;
1353
1436
  ctx.beginPath();
1354
1437
  for(let ring = 0; ring < rings; ring++){
1355
1438
  const baseR = r * (0.3 + ring / rings * 0.7);
@@ -1367,7 +1450,7 @@ const $6222456bc073291c$export$7608ccd03bfb705d = (ctx, size, config)=>{
1367
1450
  const $6222456bc073291c$export$11a377e7498bb523 = (ctx, size, config)=>{
1368
1451
  const rng = config?.rng ?? Math.random;
1369
1452
  const r = size / 2;
1370
- const k = 2 + Math.floor(rng() * 6); // 2-7 petal parameter
1453
+ const k = 2 + Math.floor(rng() * 6);
1371
1454
  const steps = 200;
1372
1455
  ctx.beginPath();
1373
1456
  for(let i = 0; i <= steps; i++){
@@ -1380,6 +1463,308 @@ const $6222456bc073291c$export$11a377e7498bb523 = (ctx, size, config)=>{
1380
1463
  }
1381
1464
  ctx.closePath();
1382
1465
  };
1466
+ const $6222456bc073291c$export$76b6526575ea179b = (ctx, size, config)=>{
1467
+ const rng = config?.rng ?? Math.random;
1468
+ const r = size / 2;
1469
+ const shardCount = 4 + Math.floor(rng() * 5); // 4-8 shards
1470
+ ctx.beginPath();
1471
+ for(let s = 0; s < shardCount; s++){
1472
+ const baseAngle = s / shardCount * Math.PI * 2 + (rng() - 0.5) * 0.3;
1473
+ const dist = r * (0.15 + rng() * 0.35);
1474
+ const cx = Math.cos(baseAngle) * dist;
1475
+ const cy = Math.sin(baseAngle) * dist;
1476
+ const shardSize = r * (0.2 + rng() * 0.4);
1477
+ const verts = 3 + Math.floor(rng() * 3); // 3-5 vertices per shard
1478
+ const shardAngleOffset = rng() * Math.PI * 2;
1479
+ for(let v = 0; v < verts; v++){
1480
+ const angle = shardAngleOffset + v / verts * Math.PI * 2;
1481
+ // Elongate shards along their radial direction
1482
+ const stretch = v % 2 === 0 ? 1.0 : 0.3 + rng() * 0.4;
1483
+ const px = cx + Math.cos(angle) * shardSize * stretch;
1484
+ const py = cy + Math.sin(angle) * shardSize * stretch;
1485
+ if (v === 0) ctx.moveTo(px, py);
1486
+ else ctx.lineTo(px, py);
1487
+ }
1488
+ ctx.closePath();
1489
+ }
1490
+ };
1491
+ const $6222456bc073291c$export$ed9ff98da5b05073 = (ctx, size, config)=>{
1492
+ const rng = config?.rng ?? Math.random;
1493
+ const r = size / 2;
1494
+ const edgeCount = 5 + Math.floor(rng() * 4); // 5-8 edges
1495
+ const points = [];
1496
+ // Generate edge midpoints at varying distances
1497
+ for(let i = 0; i < edgeCount; i++){
1498
+ const angle = i / edgeCount * Math.PI * 2 + (rng() - 0.5) * 0.4;
1499
+ const dist = r * (0.6 + rng() * 0.4);
1500
+ points.push({
1501
+ angle: angle,
1502
+ x: Math.cos(angle) * dist,
1503
+ y: Math.sin(angle) * dist
1504
+ });
1505
+ }
1506
+ // Sort by angle for proper winding
1507
+ points.sort((a, b)=>a.angle - b.angle);
1508
+ ctx.beginPath();
1509
+ ctx.moveTo(points[0].x, points[0].y);
1510
+ for(let i = 1; i < points.length; i++)ctx.lineTo(points[i].x, points[i].y);
1511
+ ctx.closePath();
1512
+ };
1513
+ const $6222456bc073291c$export$e0452d9a794fe7e5 = (ctx, size, config)=>{
1514
+ const rng = config?.rng ?? Math.random;
1515
+ const r = size / 2;
1516
+ const biteSize = 0.6 + rng() * 0.3; // 60-90% of radius
1517
+ const biteOffset = r * (0.3 + rng() * 0.4);
1518
+ const biteAngle = rng() * Math.PI * 2;
1519
+ // Outer circle
1520
+ ctx.beginPath();
1521
+ ctx.arc(0, 0, r, 0, Math.PI * 2);
1522
+ // Subtract inner circle using even-odd rule
1523
+ const bx = Math.cos(biteAngle) * biteOffset;
1524
+ const by = Math.sin(biteAngle) * biteOffset;
1525
+ // Draw inner circle counter-clockwise for subtraction
1526
+ ctx.moveTo(bx + r * biteSize, by);
1527
+ ctx.arc(bx, by, r * biteSize, 0, Math.PI * 2, true);
1528
+ };
1529
+ const $6222456bc073291c$export$38bfe5eb52137e01 = (ctx, size, config)=>{
1530
+ const rng = config?.rng ?? Math.random;
1531
+ const r = size / 2;
1532
+ const segments = 12 + Math.floor(rng() * 8);
1533
+ const startAngle = rng() * Math.PI * 2;
1534
+ const curvature = (rng() - 0.5) * 0.4;
1535
+ // Build spine points
1536
+ const spine = [];
1537
+ let angle = startAngle;
1538
+ let px = 0, py = 0;
1539
+ for(let i = 0; i <= segments; i++){
1540
+ spine.push({
1541
+ x: px,
1542
+ y: py
1543
+ });
1544
+ const stepLen = r / segments * (1.5 + rng() * 0.5);
1545
+ angle += curvature + (rng() - 0.5) * 0.6;
1546
+ px += Math.cos(angle) * stepLen;
1547
+ py += Math.sin(angle) * stepLen;
1548
+ }
1549
+ // Build tapered outline by offsetting perpendicular to spine
1550
+ ctx.beginPath();
1551
+ const leftSide = [];
1552
+ const rightSide = [];
1553
+ for(let i = 0; i < spine.length; i++){
1554
+ const t = i / (spine.length - 1);
1555
+ const width = r * 0.12 * (1 - t * 0.9); // taper from thick to thin
1556
+ const next = spine[Math.min(i + 1, spine.length - 1)];
1557
+ const dx = next.x - spine[i].x;
1558
+ const dy = next.y - spine[i].y;
1559
+ const len = Math.hypot(dx, dy) || 1;
1560
+ const nx = -dy / len;
1561
+ const ny = dx / len;
1562
+ leftSide.push({
1563
+ x: spine[i].x + nx * width,
1564
+ y: spine[i].y + ny * width
1565
+ });
1566
+ rightSide.push({
1567
+ x: spine[i].x - nx * width,
1568
+ y: spine[i].y - ny * width
1569
+ });
1570
+ }
1571
+ ctx.moveTo(leftSide[0].x, leftSide[0].y);
1572
+ for(let i = 1; i < leftSide.length; i++)ctx.lineTo(leftSide[i].x, leftSide[i].y);
1573
+ for(let i = rightSide.length - 1; i >= 0; i--)ctx.lineTo(rightSide[i].x, rightSide[i].y);
1574
+ ctx.closePath();
1575
+ };
1576
+ const $6222456bc073291c$export$105caa8cfd63c422 = (ctx, size, config)=>{
1577
+ const rng = config?.rng ?? Math.random;
1578
+ const r = size / 2;
1579
+ const lobeCount = 4 + Math.floor(rng() * 4); // 4-7 lobes
1580
+ const spineAngle = rng() * Math.PI * 2;
1581
+ const spineLen = r * 0.6;
1582
+ ctx.beginPath();
1583
+ for(let i = 0; i < lobeCount; i++){
1584
+ const t = i / (lobeCount - 1) - 0.5; // -0.5 to 0.5
1585
+ const sx = Math.cos(spineAngle) * spineLen * t;
1586
+ const sy = Math.sin(spineAngle) * spineLen * t;
1587
+ // Offset perpendicular for cloud shape
1588
+ const perpAngle = spineAngle + Math.PI / 2;
1589
+ const perpOff = (rng() - 0.3) * r * 0.3;
1590
+ const cx = sx + Math.cos(perpAngle) * perpOff;
1591
+ const cy = sy + Math.sin(perpAngle) * perpOff;
1592
+ const lobeR = r * (0.25 + rng() * 0.2);
1593
+ ctx.moveTo(cx + lobeR, cy);
1594
+ ctx.arc(cx, cy, lobeR, 0, Math.PI * 2);
1595
+ }
1596
+ };
1597
+ const $6222456bc073291c$export$e181e5bd3c539569 = (ctx, size, config)=>{
1598
+ const rng = config?.rng ?? Math.random;
1599
+ const r = size / 2;
1600
+ const spikeCount = 8 + Math.floor(rng() * 8); // 8-15 spikes
1601
+ const points = [];
1602
+ for(let i = 0; i < spikeCount; i++){
1603
+ const angle = i / spikeCount * Math.PI * 2;
1604
+ const isSpike = i % 2 === 0;
1605
+ const dist = isSpike ? r * (0.5 + rng() * 0.5 // spikes reach 50-100% of radius
1606
+ ) : r * (0.15 + rng() * 0.2); // valleys at 15-35%
1607
+ points.push({
1608
+ x: Math.cos(angle) * dist,
1609
+ y: Math.sin(angle) * dist
1610
+ });
1611
+ }
1612
+ ctx.beginPath();
1613
+ ctx.moveTo(points[0].x, points[0].y);
1614
+ for(let i = 0; i < points.length; i++){
1615
+ const curr = points[i];
1616
+ const next = points[(i + 1) % points.length];
1617
+ const cpx = (curr.x + next.x) / 2 + (rng() - 0.5) * r * 0.15;
1618
+ const cpy = (curr.y + next.y) / 2 + (rng() - 0.5) * r * 0.15;
1619
+ ctx.quadraticCurveTo(cpx, cpy, next.x, next.y);
1620
+ }
1621
+ ctx.closePath();
1622
+ };
1623
+ const $6222456bc073291c$export$155b4780b4c6bb7b = (ctx, size, config)=>{
1624
+ const rng = config?.rng ?? Math.random;
1625
+ const r = size / 2;
1626
+ const subdivisions = 1 + Math.floor(rng() * 3); // 1-3
1627
+ // Start with icosahedron vertices projected to 2D
1628
+ const baseVerts = 6 + subdivisions * 4;
1629
+ const points = [];
1630
+ for(let i = 0; i < baseVerts; i++){
1631
+ const angle = i / baseVerts * Math.PI * 2;
1632
+ const ring = i % 2 === 0 ? 1.0 : 0.5 + rng() * 0.3;
1633
+ points.push({
1634
+ x: Math.cos(angle) * r * ring,
1635
+ y: Math.sin(angle) * r * ring
1636
+ });
1637
+ }
1638
+ ctx.beginPath();
1639
+ // Draw triangulated mesh — connect each point to neighbors and center
1640
+ for(let i = 0; i < points.length; i++){
1641
+ const next = points[(i + 1) % points.length];
1642
+ ctx.moveTo(points[i].x, points[i].y);
1643
+ ctx.lineTo(next.x, next.y);
1644
+ // Connect to center
1645
+ ctx.moveTo(points[i].x, points[i].y);
1646
+ ctx.lineTo(0, 0);
1647
+ // Cross-connect to create triangulation
1648
+ if (i % 2 === 0 && i + 2 < points.length) {
1649
+ ctx.moveTo(points[i].x, points[i].y);
1650
+ ctx.lineTo(points[i + 2].x, points[i + 2].y);
1651
+ }
1652
+ }
1653
+ // Outer ring
1654
+ ctx.moveTo(points[0].x, points[0].y);
1655
+ for(let i = 1; i < points.length; i++)ctx.lineTo(points[i].x, points[i].y);
1656
+ ctx.closePath();
1657
+ };
1658
+ const $6222456bc073291c$export$9a7e648f11155172 = (ctx, size, config)=>{
1659
+ const rng = config?.rng ?? Math.random;
1660
+ const r = size / 2;
1661
+ const phi = (1 + Math.sqrt(5)) / 2; // golden ratio
1662
+ const isKite = rng() < 0.5;
1663
+ ctx.beginPath();
1664
+ if (isKite) {
1665
+ // Kite: two golden triangles joined at base
1666
+ const topY = -r;
1667
+ const bottomY = r * (1 / phi);
1668
+ const midY = r * (1 / phi - 1) * 0.3;
1669
+ const wingX = r * 0.6;
1670
+ ctx.moveTo(0, topY);
1671
+ ctx.lineTo(wingX, midY);
1672
+ ctx.lineTo(0, bottomY);
1673
+ ctx.lineTo(-wingX, midY);
1674
+ } else {
1675
+ // Dart: concave quadrilateral
1676
+ const topY = -r;
1677
+ const bottomY = r * 0.3;
1678
+ const midY = -r * 0.1;
1679
+ const wingX = r * 0.5;
1680
+ ctx.moveTo(0, topY);
1681
+ ctx.lineTo(wingX, midY);
1682
+ ctx.lineTo(0, bottomY);
1683
+ ctx.lineTo(-wingX, midY);
1684
+ }
1685
+ ctx.closePath();
1686
+ };
1687
+ const $6222456bc073291c$export$1fc0aedbabd73399 = (ctx, size, config)=>{
1688
+ const r = size / 2;
1689
+ // Vertices of equilateral triangle
1690
+ const verts = [];
1691
+ for(let i = 0; i < 3; i++){
1692
+ const angle = i / 3 * Math.PI * 2 - Math.PI / 2;
1693
+ verts.push({
1694
+ x: Math.cos(angle) * r * 0.7,
1695
+ y: Math.sin(angle) * r * 0.7
1696
+ });
1697
+ }
1698
+ // Side length = distance between vertices
1699
+ const sideLen = Math.hypot(verts[1].x - verts[0].x, verts[1].y - verts[0].y);
1700
+ ctx.beginPath();
1701
+ for(let i = 0; i < 3; i++){
1702
+ const from = verts[(i + 1) % 3];
1703
+ const to = verts[(i + 2) % 3];
1704
+ const center = verts[i];
1705
+ const startAngle = Math.atan2(from.y - center.y, from.x - center.x);
1706
+ const endAngle = Math.atan2(to.y - center.y, to.x - center.x);
1707
+ if (i === 0) ctx.moveTo(from.x, from.y);
1708
+ ctx.arc(center.x, center.y, sideLen, startAngle, endAngle);
1709
+ }
1710
+ ctx.closePath();
1711
+ };
1712
+ const $6222456bc073291c$export$ef7b5e0c19a21fd1 = (ctx, size, config)=>{
1713
+ const rng = config?.rng ?? Math.random;
1714
+ const r = size / 2;
1715
+ const dotCount = 15 + Math.floor(rng() * 25); // 15-39 dots
1716
+ const clusterTightness = 0.3 + rng() * 0.5;
1717
+ ctx.beginPath();
1718
+ for(let i = 0; i < dotCount; i++){
1719
+ // Gaussian-ish distribution via Box-Muller approximation
1720
+ const u1 = Math.max(0.001, rng());
1721
+ const u2 = rng();
1722
+ const mag = Math.sqrt(-2 * Math.log(u1)) * clusterTightness;
1723
+ const angle = u2 * Math.PI * 2;
1724
+ const dx = Math.cos(angle) * mag * r;
1725
+ const dy = Math.sin(angle) * mag * r;
1726
+ const dotR = r * (0.02 + rng() * 0.04);
1727
+ ctx.moveTo(dx + dotR, dy);
1728
+ ctx.arc(dx, dy, dotR, 0, Math.PI * 2);
1729
+ }
1730
+ };
1731
+ const $6222456bc073291c$export$f15df8ab60dfcc9a = (ctx, size, config)=>{
1732
+ const rng = config?.rng ?? Math.random;
1733
+ const r = size / 2;
1734
+ const angle1 = rng() * Math.PI;
1735
+ const angle2 = angle1 + Math.PI / 2 + (rng() - 0.5) * 0.3;
1736
+ const spacing = r * (0.08 + rng() * 0.08);
1737
+ const hasCross = rng() < 0.6;
1738
+ // Draw bounding shape (ellipse)
1739
+ const rx = r * (0.7 + rng() * 0.3);
1740
+ const ry = r * (0.5 + rng() * 0.3);
1741
+ // Outer boundary
1742
+ ctx.beginPath();
1743
+ ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
1744
+ // Hatch lines clipped to the ellipse
1745
+ const cos1 = Math.cos(angle1);
1746
+ const sin1 = Math.sin(angle1);
1747
+ for(let d = -r; d <= r; d += spacing){
1748
+ const lx1 = d * cos1 - r * sin1;
1749
+ const ly1 = d * sin1 + r * cos1;
1750
+ const lx2 = d * cos1 + r * sin1;
1751
+ const ly2 = d * sin1 - r * cos1;
1752
+ ctx.moveTo(lx1, ly1);
1753
+ ctx.lineTo(lx2, ly2);
1754
+ }
1755
+ if (hasCross) {
1756
+ const cos2 = Math.cos(angle2);
1757
+ const sin2 = Math.sin(angle2);
1758
+ for(let d = -r; d <= r; d += spacing * 1.3){
1759
+ const lx1 = d * cos2 - r * sin2;
1760
+ const ly1 = d * sin2 + r * cos2;
1761
+ const lx2 = d * cos2 + r * sin2;
1762
+ const ly2 = d * sin2 - r * cos2;
1763
+ ctx.moveTo(lx1, ly1);
1764
+ ctx.lineTo(lx2, ly2);
1765
+ }
1766
+ }
1767
+ };
1383
1768
  const $6222456bc073291c$export$40cfb4c637f2fbb5 = {
1384
1769
  blob: $6222456bc073291c$export$580f80cfb9de73bc,
1385
1770
  ngon: $6222456bc073291c$export$7a6094023f0902a6,
@@ -1387,7 +1772,18 @@ const $6222456bc073291c$export$40cfb4c637f2fbb5 = {
1387
1772
  superellipse: $6222456bc073291c$export$1db9219b4f34658c,
1388
1773
  spirograph: $6222456bc073291c$export$b027c64d22b01985,
1389
1774
  waveRing: $6222456bc073291c$export$7608ccd03bfb705d,
1390
- rose: $6222456bc073291c$export$11a377e7498bb523
1775
+ rose: $6222456bc073291c$export$11a377e7498bb523,
1776
+ shardField: $6222456bc073291c$export$76b6526575ea179b,
1777
+ voronoiCell: $6222456bc073291c$export$ed9ff98da5b05073,
1778
+ crescent: $6222456bc073291c$export$e0452d9a794fe7e5,
1779
+ tendril: $6222456bc073291c$export$38bfe5eb52137e01,
1780
+ cloudForm: $6222456bc073291c$export$105caa8cfd63c422,
1781
+ inkSplat: $6222456bc073291c$export$e181e5bd3c539569,
1782
+ geodesicDome: $6222456bc073291c$export$155b4780b4c6bb7b,
1783
+ penroseTile: $6222456bc073291c$export$9a7e648f11155172,
1784
+ reuleauxTriangle: $6222456bc073291c$export$1fc0aedbabd73399,
1785
+ dotCluster: $6222456bc073291c$export$ef7b5e0c19a21fd1,
1786
+ crosshatchPatch: $6222456bc073291c$export$f15df8ab60dfcc9a
1391
1787
  };
1392
1788
 
1393
1789
 
@@ -1422,7 +1818,13 @@ const $c3de8257a8baa3b0$var$RENDER_STYLES = [
1422
1818
  "dashed",
1423
1819
  "watercolor",
1424
1820
  "hatched",
1425
- "incomplete"
1821
+ "incomplete",
1822
+ "stipple",
1823
+ "stencil",
1824
+ "noise-grain",
1825
+ "wood-grain",
1826
+ "marble-vein",
1827
+ "fabric-weave"
1426
1828
  ];
1427
1829
  function $c3de8257a8baa3b0$export$9fd4e64b2acd410e(rng) {
1428
1830
  return $c3de8257a8baa3b0$var$RENDER_STYLES[Math.floor(rng() * $c3de8257a8baa3b0$var$RENDER_STYLES.length)];
@@ -1480,23 +1882,50 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
1480
1882
  break;
1481
1883
  case "watercolor":
1482
1884
  {
1483
- // Draw 3-4 slightly offset passes at low opacity for a bleed effect
1484
- const passes = 3 + (rng ? Math.floor(rng() * 2) : 0);
1885
+ // Improved watercolor: edge darkening + radial bleed + layered washes
1886
+ const passes = 4 + (rng ? Math.floor(rng() * 2) : 0);
1485
1887
  const savedAlpha = ctx.globalAlpha;
1486
- ctx.globalAlpha = savedAlpha * (0.3 / passes * 2);
1888
+ // Pass 1: Base wash large, soft fill at low opacity
1889
+ ctx.globalAlpha = savedAlpha * 0.15;
1890
+ ctx.save();
1891
+ const baseScale = 1.08 + (rng ? rng() * 0.04 : 0);
1892
+ ctx.scale(baseScale, baseScale);
1893
+ ctx.fill();
1894
+ ctx.restore();
1895
+ // Pass 2: Multiple offset washes with radial displacement
1896
+ ctx.globalAlpha = savedAlpha * (0.25 / passes * 2);
1487
1897
  for(let p = 0; p < passes; p++){
1488
- const jx = rng ? (rng() - 0.5) * size * 0.06 : 0;
1489
- const jy = rng ? (rng() - 0.5) * size * 0.06 : 0;
1898
+ // Radial outward displacement (not uniform) for organic bleed
1899
+ const angle = rng ? rng() * Math.PI * 2 : p * Math.PI / 2;
1900
+ const dist = rng ? rng() * size * 0.05 : size * 0.02;
1901
+ const jx = Math.cos(angle) * dist;
1902
+ const jy = Math.sin(angle) * dist;
1490
1903
  ctx.save();
1491
1904
  ctx.translate(jx, jy);
1492
1905
  ctx.fill();
1493
1906
  ctx.restore();
1494
1907
  }
1908
+ // Pass 3: Edge darkening — draw a slightly smaller shape with lighter fill
1909
+ // to simulate pigment pooling at boundaries
1910
+ ctx.globalAlpha = savedAlpha * 0.35;
1911
+ ctx.save();
1912
+ const innerScale = 0.85 + (rng ? rng() * 0.08 : 0);
1913
+ ctx.scale(innerScale, innerScale);
1914
+ // Lighten the fill for the inner area
1915
+ const origFill = ctx.fillStyle;
1916
+ if (typeof fillColor === "string") ctx.fillStyle = fillColor.replace(/[\d.]+\)$/, (m)=>{
1917
+ const v = parseFloat(m);
1918
+ return Math.min(1, v * 1.4).toFixed(2) + ")";
1919
+ });
1920
+ ctx.fill();
1921
+ ctx.fillStyle = origFill;
1922
+ ctx.restore();
1495
1923
  ctx.globalAlpha = savedAlpha;
1496
- // Light stroke on top
1497
- ctx.globalAlpha *= 0.4;
1924
+ // Soft stroke on top — thinner than normal for delicacy
1925
+ ctx.globalAlpha *= 0.25;
1926
+ ctx.lineWidth = strokeWidth * 0.6;
1498
1927
  ctx.stroke();
1499
- ctx.globalAlpha /= 0.4;
1928
+ ctx.globalAlpha /= 0.25;
1500
1929
  break;
1501
1930
  }
1502
1931
  case "hatched":
@@ -1569,6 +1998,213 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
1569
1998
  ctx.lineDashOffset = 0;
1570
1999
  break;
1571
2000
  }
2001
+ case "stipple":
2002
+ {
2003
+ // Dot-fill texture — clip to shape, then scatter dots
2004
+ const savedAlphaS = ctx.globalAlpha;
2005
+ ctx.globalAlpha = savedAlphaS * 0.15;
2006
+ ctx.fill(); // ghost fill
2007
+ ctx.globalAlpha = savedAlphaS;
2008
+ ctx.save();
2009
+ ctx.clip();
2010
+ const dotSpacing = Math.max(2, size * 0.03);
2011
+ const extent = size * 0.55;
2012
+ ctx.globalAlpha = savedAlphaS * 0.7;
2013
+ for(let dx = -extent; dx <= extent; dx += dotSpacing)for(let dy = -extent; dy <= extent; dy += dotSpacing){
2014
+ // Jitter each dot position for organic feel
2015
+ const jx = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
2016
+ const jy = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
2017
+ const dotR = rng ? dotSpacing * (0.15 + rng() * 0.2) : dotSpacing * 0.2;
2018
+ ctx.beginPath();
2019
+ ctx.arc(dx + jx, dy + jy, dotR, 0, Math.PI * 2);
2020
+ ctx.fill();
2021
+ }
2022
+ ctx.restore();
2023
+ ctx.globalAlpha = savedAlphaS;
2024
+ // Outline
2025
+ ctx.globalAlpha *= 0.4;
2026
+ ctx.stroke();
2027
+ ctx.globalAlpha /= 0.4;
2028
+ break;
2029
+ }
2030
+ case "stencil":
2031
+ {
2032
+ // Negative-space cutout — fill a rectangle, then erase the shape
2033
+ const savedAlphaSt = ctx.globalAlpha;
2034
+ // Fill a bounding area with the stroke color
2035
+ ctx.globalAlpha = savedAlphaSt * 0.5;
2036
+ ctx.fillStyle = strokeColor;
2037
+ ctx.fillRect(-size * 0.6, -size * 0.6, size * 1.2, size * 1.2);
2038
+ // Cut out the shape using destination-out
2039
+ ctx.globalCompositeOperation = "destination-out";
2040
+ ctx.globalAlpha = 1;
2041
+ ctx.fill();
2042
+ ctx.globalCompositeOperation = "source-over";
2043
+ ctx.globalAlpha = savedAlphaSt;
2044
+ // Subtle outline of the cutout
2045
+ ctx.globalAlpha *= 0.3;
2046
+ ctx.stroke();
2047
+ ctx.globalAlpha /= 0.3;
2048
+ break;
2049
+ }
2050
+ case "noise-grain":
2051
+ {
2052
+ // Procedural noise grain texture clipped to shape boundary
2053
+ const savedAlphaN = ctx.globalAlpha;
2054
+ ctx.globalAlpha = savedAlphaN * 0.25;
2055
+ ctx.fill(); // base tint
2056
+ ctx.globalAlpha = savedAlphaN;
2057
+ ctx.save();
2058
+ ctx.clip();
2059
+ const grainSpacing = Math.max(1.5, size * 0.015);
2060
+ const extentN = size * 0.55;
2061
+ ctx.globalAlpha = savedAlphaN * 0.6;
2062
+ for(let gx = -extentN; gx <= extentN; gx += grainSpacing)for(let gy = -extentN; gy <= extentN; gy += grainSpacing){
2063
+ if (!rng) break;
2064
+ const jx = (rng() - 0.5) * grainSpacing * 1.2;
2065
+ const jy = (rng() - 0.5) * grainSpacing * 1.2;
2066
+ const brightness = rng() > 0.5 ? 255 : 0;
2067
+ const dotAlpha = 0.15 + rng() * 0.35;
2068
+ ctx.globalAlpha = savedAlphaN * dotAlpha;
2069
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
2070
+ const dotSize = grainSpacing * (0.3 + rng() * 0.5);
2071
+ ctx.fillRect(gx + jx, gy + jy, dotSize, dotSize);
2072
+ }
2073
+ ctx.restore();
2074
+ ctx.fillStyle = fillColor;
2075
+ ctx.globalAlpha = savedAlphaN;
2076
+ ctx.globalAlpha *= 0.4;
2077
+ ctx.stroke();
2078
+ ctx.globalAlpha /= 0.4;
2079
+ break;
2080
+ }
2081
+ case "wood-grain":
2082
+ {
2083
+ // Parallel wavy lines simulating wood grain, clipped to shape
2084
+ const savedAlphaW = ctx.globalAlpha;
2085
+ ctx.globalAlpha = savedAlphaW * 0.2;
2086
+ ctx.fill(); // base tint
2087
+ ctx.globalAlpha = savedAlphaW;
2088
+ ctx.save();
2089
+ ctx.clip();
2090
+ const grainLineSpacing = Math.max(2, size * 0.035);
2091
+ const extentW = size * 0.55;
2092
+ const waveFreq = rng ? 3 + rng() * 5 : 5;
2093
+ const waveAmp = rng ? size * (0.01 + rng() * 0.03) : size * 0.02;
2094
+ const grainAngle = rng ? rng() * Math.PI : Math.PI * 0.25;
2095
+ ctx.lineWidth = Math.max(0.5, strokeWidth * 0.3);
2096
+ ctx.globalAlpha = savedAlphaW * 0.5;
2097
+ const cosG = Math.cos(grainAngle);
2098
+ const sinG = Math.sin(grainAngle);
2099
+ for(let d = -extentW; d <= extentW; d += grainLineSpacing){
2100
+ ctx.beginPath();
2101
+ for(let t = -extentW; t <= extentW; t += 2){
2102
+ const wave = Math.sin(t / extentW * waveFreq * Math.PI) * waveAmp;
2103
+ const px = t * cosG - (d + wave) * sinG;
2104
+ const py = t * sinG + (d + wave) * cosG;
2105
+ if (t === -extentW) ctx.moveTo(px, py);
2106
+ else ctx.lineTo(px, py);
2107
+ }
2108
+ ctx.stroke();
2109
+ }
2110
+ ctx.restore();
2111
+ ctx.globalAlpha = savedAlphaW;
2112
+ ctx.globalAlpha *= 0.35;
2113
+ ctx.stroke();
2114
+ ctx.globalAlpha /= 0.35;
2115
+ break;
2116
+ }
2117
+ case "marble-vein":
2118
+ {
2119
+ // Branching vein lines on a soft fill, clipped to shape
2120
+ const savedAlphaM = ctx.globalAlpha;
2121
+ ctx.globalAlpha = savedAlphaM * 0.35;
2122
+ ctx.fill(); // soft base
2123
+ ctx.globalAlpha = savedAlphaM;
2124
+ ctx.save();
2125
+ ctx.clip();
2126
+ const veinCount = rng ? 2 + Math.floor(rng() * 3) : 3;
2127
+ const extentM = size * 0.45;
2128
+ ctx.lineWidth = Math.max(0.5, strokeWidth * 0.5);
2129
+ ctx.globalAlpha = savedAlphaM * 0.4;
2130
+ for(let v = 0; v < veinCount; v++){
2131
+ const startX = rng ? (rng() - 0.5) * extentM * 2 : 0;
2132
+ const startY = rng ? -extentM + rng() * extentM * 0.5 : -extentM;
2133
+ let vx = startX;
2134
+ let vy = startY;
2135
+ const steps = 15 + (rng ? Math.floor(rng() * 15) : 10);
2136
+ const stepLen = size * 0.04;
2137
+ ctx.beginPath();
2138
+ ctx.moveTo(vx, vy);
2139
+ for(let s = 0; s < steps; s++){
2140
+ const drift = rng ? (rng() - 0.5) * stepLen * 1.5 : 0;
2141
+ vx += drift;
2142
+ vy += stepLen;
2143
+ ctx.lineTo(vx, vy);
2144
+ // Branch ~20% of the time
2145
+ if (rng && rng() < 0.2 && s > 2 && s < steps - 3) {
2146
+ const branchDir = rng() < 0.5 ? -1 : 1;
2147
+ let bx = vx;
2148
+ let by = vy;
2149
+ const bSteps = 3 + Math.floor(rng() * 5);
2150
+ ctx.moveTo(bx, by);
2151
+ for(let bs = 0; bs < bSteps; bs++){
2152
+ bx += branchDir * stepLen * (0.5 + rng() * 0.5);
2153
+ by += stepLen * 0.6;
2154
+ ctx.lineTo(bx, by);
2155
+ }
2156
+ ctx.moveTo(vx, vy); // return to main vein
2157
+ }
2158
+ }
2159
+ ctx.stroke();
2160
+ }
2161
+ ctx.restore();
2162
+ ctx.globalAlpha = savedAlphaM;
2163
+ ctx.globalAlpha *= 0.3;
2164
+ ctx.stroke();
2165
+ ctx.globalAlpha /= 0.3;
2166
+ break;
2167
+ }
2168
+ case "fabric-weave":
2169
+ {
2170
+ // Interlocking horizontal/vertical threads clipped to shape
2171
+ const savedAlphaF = ctx.globalAlpha;
2172
+ ctx.globalAlpha = savedAlphaF * 0.15;
2173
+ ctx.fill(); // ghost base
2174
+ ctx.globalAlpha = savedAlphaF;
2175
+ ctx.save();
2176
+ ctx.clip();
2177
+ const threadSpacing = Math.max(2, size * 0.04);
2178
+ const extentF = size * 0.55;
2179
+ ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
2180
+ ctx.globalAlpha = savedAlphaF * 0.55;
2181
+ // Horizontal threads
2182
+ for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2183
+ ctx.beginPath();
2184
+ ctx.moveTo(-extentF, y);
2185
+ ctx.lineTo(extentF, y);
2186
+ ctx.stroke();
2187
+ }
2188
+ // Vertical threads (offset by half spacing for weave effect)
2189
+ ctx.globalAlpha = savedAlphaF * 0.45;
2190
+ ctx.strokeStyle = fillColor;
2191
+ for(let x = -extentF; x <= extentF; x += threadSpacing * 2){
2192
+ ctx.beginPath();
2193
+ for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2194
+ // Over-under: draw segment, skip segment
2195
+ ctx.moveTo(x, y);
2196
+ ctx.lineTo(x, y + threadSpacing);
2197
+ }
2198
+ ctx.stroke();
2199
+ }
2200
+ ctx.strokeStyle = strokeColor;
2201
+ ctx.restore();
2202
+ ctx.globalAlpha = savedAlphaF;
2203
+ ctx.globalAlpha *= 0.3;
2204
+ ctx.stroke();
2205
+ ctx.globalAlpha /= 0.3;
2206
+ break;
2207
+ }
1572
2208
  case "fill-and-stroke":
1573
2209
  default:
1574
2210
  ctx.fill();
@@ -1615,77 +2251,1076 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1615
2251
  });
1616
2252
  ctx.restore();
1617
2253
  }
2254
+ function $c3de8257a8baa3b0$export$8bd8bbd1a8e53689(ctx, shape, x, y, config) {
2255
+ const { mirrorAxis: mirrorAxis = "horizontal", mirrorGap: mirrorGap = 0 } = config;
2256
+ // Draw the primary shape
2257
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config);
2258
+ // Draw the mirrored copy
2259
+ ctx.save();
2260
+ const savedAlpha = ctx.globalAlpha;
2261
+ ctx.globalAlpha = savedAlpha * 0.7; // mirror is slightly softer
2262
+ switch(mirrorAxis){
2263
+ case "horizontal":
2264
+ // Reflect across vertical axis at shape position
2265
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y + mirrorGap, {
2266
+ ...config,
2267
+ rotation: -(config.rotation || 0),
2268
+ size: config.size * 0.95
2269
+ });
2270
+ break;
2271
+ case "vertical":
2272
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x + mirrorGap, y, {
2273
+ ...config,
2274
+ rotation: 180 - (config.rotation || 0),
2275
+ size: config.size * 0.95
2276
+ });
2277
+ break;
2278
+ case "diagonal":
2279
+ // Reflect across 45° axis
2280
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x + mirrorGap * 0.7, y + mirrorGap * 0.7, {
2281
+ ...config,
2282
+ rotation: 90 - (config.rotation || 0),
2283
+ size: config.size * 0.9
2284
+ });
2285
+ break;
2286
+ case "radial-4":
2287
+ // Four-way radial mirror
2288
+ for(let i = 1; i < 4; i++){
2289
+ const angle = i / 4 * Math.PI * 2;
2290
+ const mx = x + Math.cos(angle) * mirrorGap;
2291
+ const my = y + Math.sin(angle) * mirrorGap;
2292
+ ctx.globalAlpha = savedAlpha * (0.7 - i * 0.1);
2293
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, mx, my, {
2294
+ ...config,
2295
+ rotation: (config.rotation || 0) + i * 90,
2296
+ size: config.size * (0.95 - i * 0.05)
2297
+ });
2298
+ }
2299
+ break;
2300
+ }
2301
+ ctx.globalAlpha = savedAlpha;
2302
+ ctx.restore();
2303
+ }
2304
+ function $c3de8257a8baa3b0$export$879206e23912d1a9(rng) {
2305
+ const roll = rng();
2306
+ if (roll < 0.60) return null;
2307
+ if (roll < 0.75) return "horizontal";
2308
+ if (roll < 0.87) return "vertical";
2309
+ if (roll < 0.95) return "diagonal";
2310
+ return "radial-4";
2311
+ }
1618
2312
 
1619
2313
 
1620
2314
 
1621
-
1622
- /**
1623
- * Configuration options for image generation.
1624
- */ const $93cf69256c93baa9$export$c2f8e0cc249a8d8f = {
1625
- width: 2048,
1626
- height: 2048,
1627
- gridSize: 5,
1628
- layers: 4,
1629
- minShapeSize: 30,
1630
- maxShapeSize: 400,
1631
- baseOpacity: 0.7,
1632
- opacityReduction: 0.12,
1633
- shapesPerLayer: 0
1634
- };
1635
-
1636
-
1637
2315
  /**
1638
- * Visual archetypesfundamentally different rendering personalities
1639
- * selected deterministically from the hash.
2316
+ * Shape affinity system controls which shapes look good together,
2317
+ * quality tiers for different rendering contexts, and size preferences.
1640
2318
  *
1641
- * Each archetype overrides key rendering parameters to produce images
1642
- * that look like they came from different generators entirely.
1643
- */ // ── Archetype definitions ───────────────────────────────────────────
1644
- const $f89bc858f7202849$var$ARCHETYPES = [
1645
- {
1646
- name: "dense-chaotic",
1647
- gridSize: 9,
1648
- layers: 5,
1649
- baseOpacity: 0.5,
1650
- opacityReduction: 0.06,
1651
- minShapeSize: 10,
1652
- maxShapeSize: 200,
1653
- backgroundStyle: "radial-dark",
1654
- paletteMode: "harmonious",
1655
- preferredStyles: [
1656
- "fill-and-stroke",
1657
- "watercolor",
1658
- "fill-only"
2319
+ * This replaces the naive "pick any shape" approach with intentional
2320
+ * curation that produces more cohesive compositions.
2321
+ */ // ── Quality tiers ───────────────────────────────────────────────────
2322
+ // Not all shapes render equally well at all sizes or in all contexts.
2323
+ // Tier 1 shapes are visually strong at any size; Tier 3 shapes need
2324
+ // specific conditions to look good.
2325
+ const $e73976f898150d4d$export$4343b39fe47bd82c = {
2326
+ // ── Basic shapes ──────────────────────────────────────────────
2327
+ circle: {
2328
+ tier: 1,
2329
+ minSizeFraction: 0.05,
2330
+ maxSizeFraction: 1.0,
2331
+ affinities: [
2332
+ "circle",
2333
+ "blob",
2334
+ "hexagon",
2335
+ "flowerOfLife",
2336
+ "seedOfLife"
1659
2337
  ],
1660
- flowLineMultiplier: 2.5,
1661
- heroShape: false,
1662
- glowMultiplier: 0.5,
1663
- sizePower: 1.2,
1664
- invertForeground: false
2338
+ category: "basic",
2339
+ heroCandidate: false,
2340
+ bestStyles: [
2341
+ "fill-only",
2342
+ "watercolor",
2343
+ "fill-and-stroke"
2344
+ ]
1665
2345
  },
1666
- {
1667
- name: "minimal-spacious",
1668
- gridSize: 2,
1669
- layers: 2,
1670
- baseOpacity: 0.85,
1671
- opacityReduction: 0.05,
1672
- minShapeSize: 150,
1673
- maxShapeSize: 600,
1674
- backgroundStyle: "solid-light",
1675
- paletteMode: "duotone",
1676
- preferredStyles: [
2346
+ square: {
2347
+ tier: 2,
2348
+ minSizeFraction: 0.08,
2349
+ maxSizeFraction: 0.7,
2350
+ affinities: [
2351
+ "square",
2352
+ "diamond",
2353
+ "superellipse",
2354
+ "islamicPattern"
2355
+ ],
2356
+ category: "basic",
2357
+ heroCandidate: false,
2358
+ bestStyles: [
1677
2359
  "fill-and-stroke",
1678
2360
  "stroke-only",
1679
- "incomplete"
2361
+ "hatched"
2362
+ ]
2363
+ },
2364
+ triangle: {
2365
+ tier: 1,
2366
+ minSizeFraction: 0.06,
2367
+ maxSizeFraction: 0.9,
2368
+ affinities: [
2369
+ "triangle",
2370
+ "diamond",
2371
+ "hexagon",
2372
+ "merkaba",
2373
+ "sriYantra"
1680
2374
  ],
1681
- flowLineMultiplier: 0.3,
1682
- heroShape: true,
1683
- glowMultiplier: 0,
1684
- sizePower: 0.8,
1685
- invertForeground: false
2375
+ category: "basic",
2376
+ heroCandidate: false,
2377
+ bestStyles: [
2378
+ "fill-and-stroke",
2379
+ "fill-only",
2380
+ "watercolor"
2381
+ ]
1686
2382
  },
1687
- {
1688
- name: "organic-flow",
2383
+ hexagon: {
2384
+ tier: 1,
2385
+ minSizeFraction: 0.05,
2386
+ maxSizeFraction: 1.0,
2387
+ affinities: [
2388
+ "hexagon",
2389
+ "circle",
2390
+ "flowerOfLife",
2391
+ "metatronsCube",
2392
+ "triangle"
2393
+ ],
2394
+ category: "basic",
2395
+ heroCandidate: false,
2396
+ bestStyles: [
2397
+ "fill-only",
2398
+ "fill-and-stroke",
2399
+ "watercolor"
2400
+ ]
2401
+ },
2402
+ star: {
2403
+ tier: 2,
2404
+ minSizeFraction: 0.08,
2405
+ maxSizeFraction: 0.6,
2406
+ affinities: [
2407
+ "star",
2408
+ "circle",
2409
+ "mandala",
2410
+ "spirograph"
2411
+ ],
2412
+ category: "basic",
2413
+ heroCandidate: false,
2414
+ bestStyles: [
2415
+ "fill-and-stroke",
2416
+ "stroke-only",
2417
+ "dashed"
2418
+ ]
2419
+ },
2420
+ "jacked-star": {
2421
+ tier: 3,
2422
+ minSizeFraction: 0.1,
2423
+ maxSizeFraction: 0.4,
2424
+ affinities: [
2425
+ "star",
2426
+ "circle"
2427
+ ],
2428
+ category: "basic",
2429
+ heroCandidate: false,
2430
+ bestStyles: [
2431
+ "stroke-only",
2432
+ "dashed"
2433
+ ]
2434
+ },
2435
+ heart: {
2436
+ tier: 3,
2437
+ minSizeFraction: 0.1,
2438
+ maxSizeFraction: 0.5,
2439
+ affinities: [
2440
+ "circle",
2441
+ "blob"
2442
+ ],
2443
+ category: "basic",
2444
+ heroCandidate: false,
2445
+ bestStyles: [
2446
+ "fill-only",
2447
+ "watercolor"
2448
+ ]
2449
+ },
2450
+ diamond: {
2451
+ tier: 2,
2452
+ minSizeFraction: 0.06,
2453
+ maxSizeFraction: 0.8,
2454
+ affinities: [
2455
+ "diamond",
2456
+ "triangle",
2457
+ "square",
2458
+ "merkaba"
2459
+ ],
2460
+ category: "basic",
2461
+ heroCandidate: false,
2462
+ bestStyles: [
2463
+ "fill-and-stroke",
2464
+ "fill-only",
2465
+ "double-stroke"
2466
+ ]
2467
+ },
2468
+ cube: {
2469
+ tier: 3,
2470
+ minSizeFraction: 0.08,
2471
+ maxSizeFraction: 0.5,
2472
+ affinities: [
2473
+ "square",
2474
+ "diamond"
2475
+ ],
2476
+ category: "basic",
2477
+ heroCandidate: false,
2478
+ bestStyles: [
2479
+ "stroke-only",
2480
+ "fill-and-stroke"
2481
+ ]
2482
+ },
2483
+ // ── Complex shapes ────────────────────────────────────────────
2484
+ platonicSolid: {
2485
+ tier: 2,
2486
+ minSizeFraction: 0.15,
2487
+ maxSizeFraction: 0.8,
2488
+ affinities: [
2489
+ "metatronsCube",
2490
+ "merkaba",
2491
+ "hexagon",
2492
+ "triangle"
2493
+ ],
2494
+ category: "complex",
2495
+ heroCandidate: true,
2496
+ bestStyles: [
2497
+ "stroke-only",
2498
+ "double-stroke",
2499
+ "dashed"
2500
+ ]
2501
+ },
2502
+ fibonacciSpiral: {
2503
+ tier: 1,
2504
+ minSizeFraction: 0.2,
2505
+ maxSizeFraction: 1.0,
2506
+ affinities: [
2507
+ "circle",
2508
+ "rose",
2509
+ "spirograph",
2510
+ "flowerOfLife"
2511
+ ],
2512
+ category: "complex",
2513
+ heroCandidate: true,
2514
+ bestStyles: [
2515
+ "stroke-only",
2516
+ "incomplete",
2517
+ "watercolor"
2518
+ ]
2519
+ },
2520
+ islamicPattern: {
2521
+ tier: 2,
2522
+ minSizeFraction: 0.25,
2523
+ maxSizeFraction: 0.9,
2524
+ affinities: [
2525
+ "square",
2526
+ "hexagon",
2527
+ "star",
2528
+ "mandala"
2529
+ ],
2530
+ category: "complex",
2531
+ heroCandidate: true,
2532
+ bestStyles: [
2533
+ "stroke-only",
2534
+ "dashed",
2535
+ "hatched"
2536
+ ]
2537
+ },
2538
+ celticKnot: {
2539
+ tier: 2,
2540
+ minSizeFraction: 0.2,
2541
+ maxSizeFraction: 0.7,
2542
+ affinities: [
2543
+ "circle",
2544
+ "lissajous",
2545
+ "spirograph"
2546
+ ],
2547
+ category: "complex",
2548
+ heroCandidate: true,
2549
+ bestStyles: [
2550
+ "stroke-only",
2551
+ "double-stroke"
2552
+ ]
2553
+ },
2554
+ merkaba: {
2555
+ tier: 1,
2556
+ minSizeFraction: 0.15,
2557
+ maxSizeFraction: 1.0,
2558
+ affinities: [
2559
+ "triangle",
2560
+ "diamond",
2561
+ "sriYantra",
2562
+ "metatronsCube"
2563
+ ],
2564
+ category: "complex",
2565
+ heroCandidate: true,
2566
+ bestStyles: [
2567
+ "stroke-only",
2568
+ "fill-and-stroke",
2569
+ "double-stroke"
2570
+ ]
2571
+ },
2572
+ mandala: {
2573
+ tier: 1,
2574
+ minSizeFraction: 0.2,
2575
+ maxSizeFraction: 1.0,
2576
+ affinities: [
2577
+ "circle",
2578
+ "flowerOfLife",
2579
+ "spirograph",
2580
+ "rose"
2581
+ ],
2582
+ category: "complex",
2583
+ heroCandidate: true,
2584
+ bestStyles: [
2585
+ "stroke-only",
2586
+ "dashed",
2587
+ "incomplete"
2588
+ ]
2589
+ },
2590
+ fractal: {
2591
+ tier: 2,
2592
+ minSizeFraction: 0.2,
2593
+ maxSizeFraction: 0.8,
2594
+ affinities: [
2595
+ "blob",
2596
+ "lissajous",
2597
+ "circle"
2598
+ ],
2599
+ category: "complex",
2600
+ heroCandidate: true,
2601
+ bestStyles: [
2602
+ "stroke-only",
2603
+ "incomplete"
2604
+ ]
2605
+ },
2606
+ // ── Sacred shapes ─────────────────────────────────────────────
2607
+ flowerOfLife: {
2608
+ tier: 1,
2609
+ minSizeFraction: 0.2,
2610
+ maxSizeFraction: 1.0,
2611
+ affinities: [
2612
+ "circle",
2613
+ "hexagon",
2614
+ "seedOfLife",
2615
+ "eggOfLife",
2616
+ "metatronsCube"
2617
+ ],
2618
+ category: "sacred",
2619
+ heroCandidate: true,
2620
+ bestStyles: [
2621
+ "stroke-only",
2622
+ "watercolor",
2623
+ "incomplete"
2624
+ ]
2625
+ },
2626
+ treeOfLife: {
2627
+ tier: 2,
2628
+ minSizeFraction: 0.25,
2629
+ maxSizeFraction: 0.9,
2630
+ affinities: [
2631
+ "circle",
2632
+ "flowerOfLife",
2633
+ "metatronsCube"
2634
+ ],
2635
+ category: "sacred",
2636
+ heroCandidate: true,
2637
+ bestStyles: [
2638
+ "stroke-only",
2639
+ "double-stroke"
2640
+ ]
2641
+ },
2642
+ metatronsCube: {
2643
+ tier: 1,
2644
+ minSizeFraction: 0.2,
2645
+ maxSizeFraction: 1.0,
2646
+ affinities: [
2647
+ "hexagon",
2648
+ "flowerOfLife",
2649
+ "platonicSolid",
2650
+ "merkaba"
2651
+ ],
2652
+ category: "sacred",
2653
+ heroCandidate: true,
2654
+ bestStyles: [
2655
+ "stroke-only",
2656
+ "dashed",
2657
+ "incomplete"
2658
+ ]
2659
+ },
2660
+ sriYantra: {
2661
+ tier: 1,
2662
+ minSizeFraction: 0.2,
2663
+ maxSizeFraction: 1.0,
2664
+ affinities: [
2665
+ "triangle",
2666
+ "merkaba",
2667
+ "mandala",
2668
+ "diamond"
2669
+ ],
2670
+ category: "sacred",
2671
+ heroCandidate: true,
2672
+ bestStyles: [
2673
+ "stroke-only",
2674
+ "fill-and-stroke",
2675
+ "double-stroke"
2676
+ ]
2677
+ },
2678
+ seedOfLife: {
2679
+ tier: 1,
2680
+ minSizeFraction: 0.15,
2681
+ maxSizeFraction: 0.9,
2682
+ affinities: [
2683
+ "circle",
2684
+ "flowerOfLife",
2685
+ "eggOfLife",
2686
+ "hexagon"
2687
+ ],
2688
+ category: "sacred",
2689
+ heroCandidate: true,
2690
+ bestStyles: [
2691
+ "stroke-only",
2692
+ "watercolor",
2693
+ "fill-only"
2694
+ ]
2695
+ },
2696
+ vesicaPiscis: {
2697
+ tier: 2,
2698
+ minSizeFraction: 0.15,
2699
+ maxSizeFraction: 0.7,
2700
+ affinities: [
2701
+ "circle",
2702
+ "seedOfLife",
2703
+ "flowerOfLife"
2704
+ ],
2705
+ category: "sacred",
2706
+ heroCandidate: false,
2707
+ bestStyles: [
2708
+ "stroke-only",
2709
+ "watercolor"
2710
+ ]
2711
+ },
2712
+ torus: {
2713
+ tier: 3,
2714
+ minSizeFraction: 0.2,
2715
+ maxSizeFraction: 0.6,
2716
+ affinities: [
2717
+ "circle",
2718
+ "spirograph",
2719
+ "waveRing"
2720
+ ],
2721
+ category: "sacred",
2722
+ heroCandidate: false,
2723
+ bestStyles: [
2724
+ "stroke-only",
2725
+ "dashed"
2726
+ ]
2727
+ },
2728
+ eggOfLife: {
2729
+ tier: 2,
2730
+ minSizeFraction: 0.15,
2731
+ maxSizeFraction: 0.8,
2732
+ affinities: [
2733
+ "circle",
2734
+ "seedOfLife",
2735
+ "flowerOfLife"
2736
+ ],
2737
+ category: "sacred",
2738
+ heroCandidate: true,
2739
+ bestStyles: [
2740
+ "stroke-only",
2741
+ "watercolor"
2742
+ ]
2743
+ },
2744
+ // ── Procedural shapes ─────────────────────────────────────────
2745
+ blob: {
2746
+ tier: 1,
2747
+ minSizeFraction: 0.05,
2748
+ maxSizeFraction: 1.0,
2749
+ affinities: [
2750
+ "blob",
2751
+ "circle",
2752
+ "superellipse",
2753
+ "waveRing"
2754
+ ],
2755
+ category: "procedural",
2756
+ heroCandidate: false,
2757
+ bestStyles: [
2758
+ "fill-only",
2759
+ "watercolor",
2760
+ "fill-and-stroke"
2761
+ ]
2762
+ },
2763
+ ngon: {
2764
+ tier: 2,
2765
+ minSizeFraction: 0.06,
2766
+ maxSizeFraction: 0.8,
2767
+ affinities: [
2768
+ "hexagon",
2769
+ "triangle",
2770
+ "diamond",
2771
+ "superellipse"
2772
+ ],
2773
+ category: "procedural",
2774
+ heroCandidate: false,
2775
+ bestStyles: [
2776
+ "fill-and-stroke",
2777
+ "fill-only",
2778
+ "hatched"
2779
+ ]
2780
+ },
2781
+ lissajous: {
2782
+ tier: 2,
2783
+ minSizeFraction: 0.15,
2784
+ maxSizeFraction: 0.8,
2785
+ affinities: [
2786
+ "spirograph",
2787
+ "rose",
2788
+ "celticKnot",
2789
+ "fibonacciSpiral"
2790
+ ],
2791
+ category: "procedural",
2792
+ heroCandidate: false,
2793
+ bestStyles: [
2794
+ "stroke-only",
2795
+ "incomplete",
2796
+ "dashed"
2797
+ ]
2798
+ },
2799
+ superellipse: {
2800
+ tier: 1,
2801
+ minSizeFraction: 0.05,
2802
+ maxSizeFraction: 1.0,
2803
+ affinities: [
2804
+ "circle",
2805
+ "square",
2806
+ "blob",
2807
+ "hexagon"
2808
+ ],
2809
+ category: "procedural",
2810
+ heroCandidate: false,
2811
+ bestStyles: [
2812
+ "fill-only",
2813
+ "watercolor",
2814
+ "fill-and-stroke",
2815
+ "wood-grain"
2816
+ ]
2817
+ },
2818
+ spirograph: {
2819
+ tier: 1,
2820
+ minSizeFraction: 0.15,
2821
+ maxSizeFraction: 0.9,
2822
+ affinities: [
2823
+ "rose",
2824
+ "lissajous",
2825
+ "mandala",
2826
+ "flowerOfLife"
2827
+ ],
2828
+ category: "procedural",
2829
+ heroCandidate: true,
2830
+ bestStyles: [
2831
+ "stroke-only",
2832
+ "incomplete",
2833
+ "dashed"
2834
+ ]
2835
+ },
2836
+ waveRing: {
2837
+ tier: 2,
2838
+ minSizeFraction: 0.1,
2839
+ maxSizeFraction: 0.8,
2840
+ affinities: [
2841
+ "circle",
2842
+ "blob",
2843
+ "torus",
2844
+ "spirograph"
2845
+ ],
2846
+ category: "procedural",
2847
+ heroCandidate: false,
2848
+ bestStyles: [
2849
+ "stroke-only",
2850
+ "dashed",
2851
+ "incomplete"
2852
+ ]
2853
+ },
2854
+ rose: {
2855
+ tier: 1,
2856
+ minSizeFraction: 0.1,
2857
+ maxSizeFraction: 0.9,
2858
+ affinities: [
2859
+ "spirograph",
2860
+ "mandala",
2861
+ "flowerOfLife",
2862
+ "circle"
2863
+ ],
2864
+ category: "procedural",
2865
+ heroCandidate: true,
2866
+ bestStyles: [
2867
+ "stroke-only",
2868
+ "fill-only",
2869
+ "watercolor"
2870
+ ]
2871
+ },
2872
+ // ── New procedural shapes ─────────────────────────────────────
2873
+ shardField: {
2874
+ tier: 2,
2875
+ minSizeFraction: 0.1,
2876
+ maxSizeFraction: 0.7,
2877
+ affinities: [
2878
+ "voronoiCell",
2879
+ "diamond",
2880
+ "triangle",
2881
+ "penroseTile"
2882
+ ],
2883
+ category: "procedural",
2884
+ heroCandidate: false,
2885
+ bestStyles: [
2886
+ "fill-and-stroke",
2887
+ "stroke-only",
2888
+ "fill-only"
2889
+ ]
2890
+ },
2891
+ voronoiCell: {
2892
+ tier: 1,
2893
+ minSizeFraction: 0.08,
2894
+ maxSizeFraction: 0.9,
2895
+ affinities: [
2896
+ "shardField",
2897
+ "ngon",
2898
+ "superellipse",
2899
+ "blob"
2900
+ ],
2901
+ category: "procedural",
2902
+ heroCandidate: false,
2903
+ bestStyles: [
2904
+ "fill-and-stroke",
2905
+ "fill-only",
2906
+ "watercolor",
2907
+ "marble-vein"
2908
+ ]
2909
+ },
2910
+ crescent: {
2911
+ tier: 1,
2912
+ minSizeFraction: 0.1,
2913
+ maxSizeFraction: 1.0,
2914
+ affinities: [
2915
+ "circle",
2916
+ "blob",
2917
+ "cloudForm",
2918
+ "vesicaPiscis"
2919
+ ],
2920
+ category: "procedural",
2921
+ heroCandidate: true,
2922
+ bestStyles: [
2923
+ "fill-only",
2924
+ "watercolor",
2925
+ "fill-and-stroke"
2926
+ ]
2927
+ },
2928
+ tendril: {
2929
+ tier: 2,
2930
+ minSizeFraction: 0.1,
2931
+ maxSizeFraction: 0.8,
2932
+ affinities: [
2933
+ "blob",
2934
+ "inkSplat",
2935
+ "lissajous",
2936
+ "fibonacciSpiral"
2937
+ ],
2938
+ category: "procedural",
2939
+ heroCandidate: false,
2940
+ bestStyles: [
2941
+ "fill-only",
2942
+ "watercolor",
2943
+ "fill-and-stroke"
2944
+ ]
2945
+ },
2946
+ cloudForm: {
2947
+ tier: 1,
2948
+ minSizeFraction: 0.15,
2949
+ maxSizeFraction: 1.0,
2950
+ affinities: [
2951
+ "blob",
2952
+ "circle",
2953
+ "crescent",
2954
+ "superellipse"
2955
+ ],
2956
+ category: "procedural",
2957
+ heroCandidate: false,
2958
+ bestStyles: [
2959
+ "fill-only",
2960
+ "watercolor"
2961
+ ]
2962
+ },
2963
+ inkSplat: {
2964
+ tier: 2,
2965
+ minSizeFraction: 0.1,
2966
+ maxSizeFraction: 0.8,
2967
+ affinities: [
2968
+ "blob",
2969
+ "tendril",
2970
+ "shardField",
2971
+ "star"
2972
+ ],
2973
+ category: "procedural",
2974
+ heroCandidate: false,
2975
+ bestStyles: [
2976
+ "fill-only",
2977
+ "watercolor",
2978
+ "fill-and-stroke"
2979
+ ]
2980
+ },
2981
+ geodesicDome: {
2982
+ tier: 2,
2983
+ minSizeFraction: 0.2,
2984
+ maxSizeFraction: 0.9,
2985
+ affinities: [
2986
+ "metatronsCube",
2987
+ "platonicSolid",
2988
+ "hexagon",
2989
+ "triangle"
2990
+ ],
2991
+ category: "procedural",
2992
+ heroCandidate: true,
2993
+ bestStyles: [
2994
+ "stroke-only",
2995
+ "dashed",
2996
+ "double-stroke"
2997
+ ]
2998
+ },
2999
+ penroseTile: {
3000
+ tier: 2,
3001
+ minSizeFraction: 0.06,
3002
+ maxSizeFraction: 0.6,
3003
+ affinities: [
3004
+ "diamond",
3005
+ "triangle",
3006
+ "shardField",
3007
+ "voronoiCell"
3008
+ ],
3009
+ category: "procedural",
3010
+ heroCandidate: false,
3011
+ bestStyles: [
3012
+ "fill-and-stroke",
3013
+ "fill-only",
3014
+ "double-stroke"
3015
+ ]
3016
+ },
3017
+ reuleauxTriangle: {
3018
+ tier: 1,
3019
+ minSizeFraction: 0.08,
3020
+ maxSizeFraction: 1.0,
3021
+ affinities: [
3022
+ "triangle",
3023
+ "circle",
3024
+ "superellipse",
3025
+ "vesicaPiscis"
3026
+ ],
3027
+ category: "procedural",
3028
+ heroCandidate: true,
3029
+ bestStyles: [
3030
+ "fill-and-stroke",
3031
+ "fill-only",
3032
+ "watercolor"
3033
+ ]
3034
+ },
3035
+ dotCluster: {
3036
+ tier: 3,
3037
+ minSizeFraction: 0.05,
3038
+ maxSizeFraction: 0.5,
3039
+ affinities: [
3040
+ "cloudForm",
3041
+ "inkSplat",
3042
+ "blob"
3043
+ ],
3044
+ category: "procedural",
3045
+ heroCandidate: false,
3046
+ bestStyles: [
3047
+ "fill-only",
3048
+ "stipple"
3049
+ ]
3050
+ },
3051
+ crosshatchPatch: {
3052
+ tier: 3,
3053
+ minSizeFraction: 0.1,
3054
+ maxSizeFraction: 0.6,
3055
+ affinities: [
3056
+ "voronoiCell",
3057
+ "ngon",
3058
+ "superellipse"
3059
+ ],
3060
+ category: "procedural",
3061
+ heroCandidate: false,
3062
+ bestStyles: [
3063
+ "stroke-only",
3064
+ "hatched",
3065
+ "fabric-weave"
3066
+ ]
3067
+ }
3068
+ };
3069
+ function $e73976f898150d4d$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
3070
+ const available = shapeNames.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]);
3071
+ // Pick a seed shape — tier 1 shapes that are hero candidates
3072
+ const heroPool = available.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s].tier === 1 && $e73976f898150d4d$export$4343b39fe47bd82c[s].heroCandidate);
3073
+ const seedShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : available[Math.floor(rng() * available.length)];
3074
+ const seedProfile = $e73976f898150d4d$export$4343b39fe47bd82c[seedShape];
3075
+ // Primary: seed shape + its direct affinities (tier 1-2 only)
3076
+ const primaryCandidates = [
3077
+ seedShape,
3078
+ ...seedProfile.affinities
3079
+ ].filter((s)=>available.includes(s)).filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s].tier <= 2);
3080
+ const primary = [
3081
+ ...new Set(primaryCandidates)
3082
+ ].slice(0, 5);
3083
+ // Supporting: affinities of affinities, plus same-category shapes
3084
+ const supportingSet = new Set();
3085
+ for (const p of primary){
3086
+ const profile = $e73976f898150d4d$export$4343b39fe47bd82c[p];
3087
+ if (!profile) continue;
3088
+ for (const aff of profile.affinities)if (available.includes(aff) && !primary.includes(aff)) supportingSet.add(aff);
3089
+ }
3090
+ // Add same-category tier 1-2 shapes
3091
+ for (const s of available){
3092
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
3093
+ if (p.category === seedProfile.category && p.tier <= 2 && !primary.includes(s)) supportingSet.add(s);
3094
+ }
3095
+ const supporting = [
3096
+ ...supportingSet
3097
+ ].slice(0, 6);
3098
+ // Accents: tier 1 shapes from other categories for contrast
3099
+ const usedCategories = new Set([
3100
+ ...primary,
3101
+ ...supporting
3102
+ ].map((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]?.category));
3103
+ const accentCandidates = available.filter((s)=>!primary.includes(s) && !supporting.includes(s) && $e73976f898150d4d$export$4343b39fe47bd82c[s].tier <= 2 && !usedCategories.has($e73976f898150d4d$export$4343b39fe47bd82c[s].category));
3104
+ // Shuffle and take a few
3105
+ const accents = [];
3106
+ const shuffled = [
3107
+ ...accentCandidates
3108
+ ];
3109
+ for(let i = shuffled.length - 1; i > 0; i--){
3110
+ const j = Math.floor(rng() * (i + 1));
3111
+ [shuffled[i], shuffled[j]] = [
3112
+ shuffled[j],
3113
+ shuffled[i]
3114
+ ];
3115
+ }
3116
+ accents.push(...shuffled.slice(0, 3));
3117
+ // For certain archetypes, bias the palette
3118
+ if (archetypeName === "geometric-precision") // Remove blobs and organic shapes from primary
3119
+ return {
3120
+ primary: primary.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]?.category !== "procedural" || s === "ngon"),
3121
+ supporting: supporting.filter((s)=>s !== "blob"),
3122
+ accents: accents
3123
+ };
3124
+ if (archetypeName === "organic-flow") {
3125
+ // Boost procedural/organic shapes
3126
+ const organicBoost = available.filter((s)=>[
3127
+ "blob",
3128
+ "superellipse",
3129
+ "waveRing",
3130
+ "rose"
3131
+ ].includes(s) && !primary.includes(s));
3132
+ return {
3133
+ primary: [
3134
+ ...primary,
3135
+ ...organicBoost.slice(0, 2)
3136
+ ],
3137
+ supporting: supporting,
3138
+ accents: accents
3139
+ };
3140
+ }
3141
+ if (archetypeName === "shattered-glass") {
3142
+ // Favor angular, fragmented shapes
3143
+ const shardBoost = available.filter((s)=>[
3144
+ "shardField",
3145
+ "voronoiCell",
3146
+ "penroseTile",
3147
+ "diamond",
3148
+ "triangle",
3149
+ "ngon"
3150
+ ].includes(s) && !primary.includes(s));
3151
+ return {
3152
+ primary: [
3153
+ ...primary.filter((s)=>s !== "blob" && s !== "cloudForm"),
3154
+ ...shardBoost.slice(0, 3)
3155
+ ],
3156
+ supporting: supporting.filter((s)=>s !== "blob" && s !== "cloudForm"),
3157
+ accents: accents
3158
+ };
3159
+ }
3160
+ if (archetypeName === "botanical") {
3161
+ // Favor organic, flowing shapes
3162
+ const botanicalBoost = available.filter((s)=>[
3163
+ "tendril",
3164
+ "cloudForm",
3165
+ "blob",
3166
+ "crescent",
3167
+ "rose",
3168
+ "inkSplat"
3169
+ ].includes(s) && !primary.includes(s));
3170
+ return {
3171
+ primary: [
3172
+ ...primary,
3173
+ ...botanicalBoost.slice(0, 3)
3174
+ ],
3175
+ supporting: supporting,
3176
+ accents: accents
3177
+ };
3178
+ }
3179
+ if (archetypeName === "stipple-portrait") {
3180
+ // Favor small, dot-friendly shapes
3181
+ const stippleBoost = available.filter((s)=>[
3182
+ "dotCluster",
3183
+ "circle",
3184
+ "crosshatchPatch",
3185
+ "voronoiCell",
3186
+ "blob"
3187
+ ].includes(s) && !primary.includes(s));
3188
+ return {
3189
+ primary: [
3190
+ ...primary,
3191
+ ...stippleBoost.slice(0, 3)
3192
+ ],
3193
+ supporting: supporting,
3194
+ accents: accents
3195
+ };
3196
+ }
3197
+ if (archetypeName === "celestial") {
3198
+ // Favor sacred geometry and cosmic shapes
3199
+ const celestialBoost = available.filter((s)=>[
3200
+ "crescent",
3201
+ "geodesicDome",
3202
+ "mandala",
3203
+ "flowerOfLife",
3204
+ "spirograph",
3205
+ "fibonacciSpiral"
3206
+ ].includes(s) && !primary.includes(s));
3207
+ return {
3208
+ primary: [
3209
+ ...primary,
3210
+ ...celestialBoost.slice(0, 3)
3211
+ ],
3212
+ supporting: supporting,
3213
+ accents: accents
3214
+ };
3215
+ }
3216
+ return {
3217
+ primary: primary,
3218
+ supporting: supporting,
3219
+ accents: accents
3220
+ };
3221
+ }
3222
+ function $e73976f898150d4d$export$3c37d9a045754d0e(palette, rng, sizeFraction) {
3223
+ // Filter each tier by size constraints
3224
+ const validPrimary = palette.primary.filter((s)=>{
3225
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
3226
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
3227
+ });
3228
+ const validSupporting = palette.supporting.filter((s)=>{
3229
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
3230
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
3231
+ });
3232
+ const validAccents = palette.accents.filter((s)=>{
3233
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
3234
+ return p && sizeFraction >= p.minSizeFraction && sizeFraction <= p.maxSizeFraction;
3235
+ });
3236
+ const roll = rng();
3237
+ if (roll < 0.60 && validPrimary.length > 0) return validPrimary[Math.floor(rng() * validPrimary.length)];
3238
+ if (roll < 0.90 && validSupporting.length > 0) return validSupporting[Math.floor(rng() * validSupporting.length)];
3239
+ if (validAccents.length > 0) return validAccents[Math.floor(rng() * validAccents.length)];
3240
+ // Fallback: any valid primary or supporting
3241
+ const fallback = [
3242
+ ...validPrimary,
3243
+ ...validSupporting
3244
+ ];
3245
+ if (fallback.length > 0) return fallback[Math.floor(rng() * fallback.length)];
3246
+ // Ultimate fallback
3247
+ return palette.primary[0] || "circle";
3248
+ }
3249
+ function $e73976f898150d4d$export$ab873bb6fb56c1a8(shapeName, layerStyle, rng) {
3250
+ const profile = $e73976f898150d4d$export$4343b39fe47bd82c[shapeName];
3251
+ if (!profile || rng() > 0.7) return layerStyle;
3252
+ return profile.bestStyles[Math.floor(rng() * profile.bestStyles.length)];
3253
+ }
3254
+
3255
+
3256
+
3257
+ /**
3258
+ * Configuration options for image generation.
3259
+ */ const $93cf69256c93baa9$export$c2f8e0cc249a8d8f = {
3260
+ width: 2048,
3261
+ height: 2048,
3262
+ gridSize: 5,
3263
+ layers: 4,
3264
+ minShapeSize: 30,
3265
+ maxShapeSize: 400,
3266
+ baseOpacity: 0.7,
3267
+ opacityReduction: 0.12,
3268
+ shapesPerLayer: 0
3269
+ };
3270
+
3271
+
3272
+ /**
3273
+ * Visual archetypes — fundamentally different rendering personalities
3274
+ * selected deterministically from the hash.
3275
+ *
3276
+ * Each archetype overrides key rendering parameters to produce images
3277
+ * that look like they came from different generators entirely.
3278
+ */ // ── Archetype definitions ───────────────────────────────────────────
3279
+ const $f89bc858f7202849$var$ARCHETYPES = [
3280
+ {
3281
+ name: "dense-chaotic",
3282
+ gridSize: 9,
3283
+ layers: 5,
3284
+ baseOpacity: 0.5,
3285
+ opacityReduction: 0.06,
3286
+ minShapeSize: 10,
3287
+ maxShapeSize: 200,
3288
+ backgroundStyle: "radial-dark",
3289
+ paletteMode: "harmonious",
3290
+ preferredStyles: [
3291
+ "fill-and-stroke",
3292
+ "watercolor",
3293
+ "fill-only"
3294
+ ],
3295
+ flowLineMultiplier: 2.5,
3296
+ heroShape: false,
3297
+ glowMultiplier: 0.5,
3298
+ sizePower: 1.2,
3299
+ invertForeground: false
3300
+ },
3301
+ {
3302
+ name: "minimal-spacious",
3303
+ gridSize: 2,
3304
+ layers: 2,
3305
+ baseOpacity: 0.85,
3306
+ opacityReduction: 0.05,
3307
+ minShapeSize: 150,
3308
+ maxShapeSize: 600,
3309
+ backgroundStyle: "solid-light",
3310
+ paletteMode: "duotone",
3311
+ preferredStyles: [
3312
+ "fill-and-stroke",
3313
+ "stroke-only",
3314
+ "incomplete"
3315
+ ],
3316
+ flowLineMultiplier: 0.3,
3317
+ heroShape: true,
3318
+ glowMultiplier: 0,
3319
+ sizePower: 0.8,
3320
+ invertForeground: false
3321
+ },
3322
+ {
3323
+ name: "organic-flow",
1689
3324
  gridSize: 4,
1690
3325
  layers: 3,
1691
3326
  baseOpacity: 0.4,
@@ -1832,6 +3467,69 @@ const $f89bc858f7202849$var$ARCHETYPES = [
1832
3467
  sizePower: 2.5,
1833
3468
  invertForeground: false
1834
3469
  },
3470
+ {
3471
+ name: "watercolor-wash",
3472
+ gridSize: 3,
3473
+ layers: 3,
3474
+ baseOpacity: 0.25,
3475
+ opacityReduction: 0.03,
3476
+ minShapeSize: 200,
3477
+ maxShapeSize: 700,
3478
+ backgroundStyle: "radial-light",
3479
+ paletteMode: "harmonious",
3480
+ preferredStyles: [
3481
+ "watercolor",
3482
+ "fill-only",
3483
+ "incomplete"
3484
+ ],
3485
+ flowLineMultiplier: 0.5,
3486
+ heroShape: false,
3487
+ glowMultiplier: 0.3,
3488
+ sizePower: 0.6,
3489
+ invertForeground: false
3490
+ },
3491
+ {
3492
+ name: "op-art",
3493
+ gridSize: 8,
3494
+ layers: 2,
3495
+ baseOpacity: 0.95,
3496
+ opacityReduction: 0.05,
3497
+ minShapeSize: 20,
3498
+ maxShapeSize: 200,
3499
+ backgroundStyle: "solid-light",
3500
+ paletteMode: "high-contrast",
3501
+ preferredStyles: [
3502
+ "fill-and-stroke",
3503
+ "stroke-only",
3504
+ "dashed"
3505
+ ],
3506
+ flowLineMultiplier: 0,
3507
+ heroShape: false,
3508
+ glowMultiplier: 0,
3509
+ sizePower: 0.4,
3510
+ invertForeground: false
3511
+ },
3512
+ {
3513
+ name: "collage",
3514
+ gridSize: 4,
3515
+ layers: 3,
3516
+ baseOpacity: 0.9,
3517
+ opacityReduction: 0.08,
3518
+ minShapeSize: 80,
3519
+ maxShapeSize: 500,
3520
+ backgroundStyle: "solid-light",
3521
+ paletteMode: "duotone",
3522
+ preferredStyles: [
3523
+ "fill-and-stroke",
3524
+ "fill-only",
3525
+ "double-stroke"
3526
+ ],
3527
+ flowLineMultiplier: 0,
3528
+ heroShape: true,
3529
+ glowMultiplier: 0,
3530
+ sizePower: 0.7,
3531
+ invertForeground: false
3532
+ },
1835
3533
  {
1836
3534
  name: "classic",
1837
3535
  gridSize: 5,
@@ -1852,6 +3550,91 @@ const $f89bc858f7202849$var$ARCHETYPES = [
1852
3550
  glowMultiplier: 1,
1853
3551
  sizePower: 1.8,
1854
3552
  invertForeground: false
3553
+ },
3554
+ {
3555
+ name: "shattered-glass",
3556
+ gridSize: 8,
3557
+ layers: 3,
3558
+ baseOpacity: 0.85,
3559
+ opacityReduction: 0.1,
3560
+ minShapeSize: 15,
3561
+ maxShapeSize: 250,
3562
+ backgroundStyle: "solid-dark",
3563
+ paletteMode: "high-contrast",
3564
+ preferredStyles: [
3565
+ "fill-and-stroke",
3566
+ "stroke-only",
3567
+ "fill-only"
3568
+ ],
3569
+ flowLineMultiplier: 0,
3570
+ heroShape: false,
3571
+ glowMultiplier: 0.3,
3572
+ sizePower: 1.0,
3573
+ invertForeground: false
3574
+ },
3575
+ {
3576
+ name: "botanical",
3577
+ gridSize: 4,
3578
+ layers: 4,
3579
+ baseOpacity: 0.5,
3580
+ opacityReduction: 0.06,
3581
+ minShapeSize: 30,
3582
+ maxShapeSize: 400,
3583
+ backgroundStyle: "radial-light",
3584
+ paletteMode: "earth",
3585
+ preferredStyles: [
3586
+ "watercolor",
3587
+ "fill-only",
3588
+ "incomplete"
3589
+ ],
3590
+ flowLineMultiplier: 3,
3591
+ heroShape: true,
3592
+ glowMultiplier: 0.2,
3593
+ sizePower: 1.6,
3594
+ invertForeground: false
3595
+ },
3596
+ {
3597
+ name: "stipple-portrait",
3598
+ gridSize: 9,
3599
+ layers: 2,
3600
+ baseOpacity: 0.8,
3601
+ opacityReduction: 0.05,
3602
+ minShapeSize: 5,
3603
+ maxShapeSize: 120,
3604
+ backgroundStyle: "solid-light",
3605
+ paletteMode: "monochrome",
3606
+ preferredStyles: [
3607
+ "stipple",
3608
+ "fill-only",
3609
+ "hatched"
3610
+ ],
3611
+ flowLineMultiplier: 0,
3612
+ heroShape: false,
3613
+ glowMultiplier: 0,
3614
+ sizePower: 2.8,
3615
+ invertForeground: false
3616
+ },
3617
+ {
3618
+ name: "celestial",
3619
+ gridSize: 7,
3620
+ layers: 5,
3621
+ baseOpacity: 0.45,
3622
+ opacityReduction: 0.04,
3623
+ minShapeSize: 8,
3624
+ maxShapeSize: 450,
3625
+ backgroundStyle: "radial-dark",
3626
+ paletteMode: "neon",
3627
+ preferredStyles: [
3628
+ "fill-only",
3629
+ "watercolor",
3630
+ "stroke-only",
3631
+ "incomplete"
3632
+ ],
3633
+ flowLineMultiplier: 2,
3634
+ heroShape: true,
3635
+ glowMultiplier: 2.5,
3636
+ sizePower: 2.2,
3637
+ invertForeground: false
1855
3638
  }
1856
3639
  ];
1857
3640
  function $f89bc858f7202849$export$f1142fd7da4d6590(rng) {
@@ -1859,26 +3642,7 @@ function $f89bc858f7202849$export$f1142fd7da4d6590(rng) {
1859
3642
  }
1860
3643
 
1861
3644
 
1862
- // ── Shape categories for weighted selection ─────────────────────────
1863
- const $4f72c5a314eddf25$var$BASIC_SHAPES = [
1864
- "circle",
1865
- "square",
1866
- "triangle",
1867
- "hexagon",
1868
- "diamond",
1869
- "cube"
1870
- ];
1871
- const $4f72c5a314eddf25$var$COMPLEX_SHAPES = [
1872
- "star",
1873
- "jacked-star",
1874
- "heart",
1875
- "platonicSolid",
1876
- "fibonacciSpiral",
1877
- "islamicPattern",
1878
- "celticKnot",
1879
- "merkaba",
1880
- "fractal"
1881
- ];
3645
+ // ── Shape categories for weighted selection (legacy fallback) ───────
1882
3646
  const $4f72c5a314eddf25$var$SACRED_SHAPES = [
1883
3647
  "mandala",
1884
3648
  "flowerOfLife",
@@ -1890,15 +3654,6 @@ const $4f72c5a314eddf25$var$SACRED_SHAPES = [
1890
3654
  "torus",
1891
3655
  "eggOfLife"
1892
3656
  ];
1893
- const $4f72c5a314eddf25$var$PROCEDURAL_SHAPES = [
1894
- "blob",
1895
- "ngon",
1896
- "lissajous",
1897
- "superellipse",
1898
- "spirograph",
1899
- "waveRing",
1900
- "rose"
1901
- ];
1902
3657
  const $4f72c5a314eddf25$var$COMPOSITION_MODES = [
1903
3658
  "radial",
1904
3659
  "flow-field",
@@ -1906,23 +3661,6 @@ const $4f72c5a314eddf25$var$COMPOSITION_MODES = [
1906
3661
  "grid-subdivision",
1907
3662
  "clustered"
1908
3663
  ];
1909
- // ── Helper: pick shape with layer-aware weighting ───────────────────
1910
- function $4f72c5a314eddf25$var$pickShape(rng, layerRatio, shapeNames) {
1911
- const basicW = 1 - layerRatio * 0.6;
1912
- const complexW = 0.3 + layerRatio * 0.3;
1913
- const sacredW = 0.1 + layerRatio * 0.4;
1914
- const proceduralW = 0.25 + layerRatio * 0.2; // always present, grows with depth
1915
- const total = basicW + complexW + sacredW + proceduralW;
1916
- const roll = rng() * total;
1917
- let pool;
1918
- if (roll < basicW) pool = $4f72c5a314eddf25$var$BASIC_SHAPES;
1919
- else if (roll < basicW + complexW) pool = $4f72c5a314eddf25$var$COMPLEX_SHAPES;
1920
- else if (roll < basicW + complexW + sacredW) pool = $4f72c5a314eddf25$var$SACRED_SHAPES;
1921
- else pool = $4f72c5a314eddf25$var$PROCEDURAL_SHAPES;
1922
- const available = pool.filter((s)=>shapeNames.includes(s));
1923
- if (available.length === 0) return shapeNames[Math.floor(rng() * shapeNames.length)];
1924
- return available[Math.floor(rng() * available.length)];
1925
- }
1926
3664
  // ── Helper: get position based on composition mode ──────────────────
1927
3665
  function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
1928
3666
  switch(mode){
@@ -1981,22 +3719,28 @@ function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height,
1981
3719
  };
1982
3720
  }
1983
3721
  }
1984
- // ── Helper: positional color blending ───────────────────────────────
1985
- function $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, colors, rng) {
1986
- const nx = x / width;
1987
- const ny = y / height;
1988
- const posIndex = (nx * 0.6 + ny * 0.4) * (colors.length - 1);
1989
- const baseIdx = Math.floor(posIndex) % colors.length;
1990
- return (0, $d016ad53434219a1$export$59539d800dbe6858)(colors[baseIdx], rng, 0.08);
3722
+ // ── Helper: positional color from hierarchy ─────────────────────────
3723
+ function $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, hierarchy, rng) {
3724
+ // Blend position into color selection — shapes near center lean dominant
3725
+ const distFromCenter = Math.hypot(x - width / 2, y - height / 2) / Math.hypot(width / 2, height / 2);
3726
+ // Center = more dominant, edges = more accent
3727
+ if (distFromCenter < 0.35) return (0, $d016ad53434219a1$export$18a34c25ea7e724b)(hierarchy.dominant, rng, 10, 0.08);
3728
+ else if (distFromCenter < 0.7) return (0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(hierarchy, rng), rng, 8, 0.06);
3729
+ else {
3730
+ // Edges: bias toward secondary/accent
3731
+ const roll = rng();
3732
+ const color = roll < 0.4 ? hierarchy.secondary : roll < 0.75 ? hierarchy.accent : hierarchy.dominant;
3733
+ return (0, $d016ad53434219a1$export$18a34c25ea7e724b)(color, rng, 12, 0.08);
3734
+ }
1991
3735
  }
1992
- // ── Helper: check if a position is inside a void zone (Feature E) ───
3736
+ // ── Helper: check if a position is inside a void zone ───────────────
1993
3737
  function $4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones) {
1994
3738
  for (const zone of voidZones){
1995
3739
  if (Math.hypot(x - zone.x, y - zone.y) < zone.radius) return true;
1996
3740
  }
1997
3741
  return false;
1998
3742
  }
1999
- // ── Helper: density check for negative space (Feature E) ────────────
3743
+ // ── Helper: density check ───────────────────────────────────────────
2000
3744
  function $4f72c5a314eddf25$var$localDensity(x, y, positions, radius) {
2001
3745
  let count = 0;
2002
3746
  for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
@@ -2070,6 +3814,135 @@ function $4f72c5a314eddf25$var$drawBackground(ctx, style, bgStart, bgEnd, width,
2070
3814
  }
2071
3815
  }
2072
3816
  }
3817
+ const $4f72c5a314eddf25$var$CONSTELLATIONS = [
3818
+ {
3819
+ name: "flanked-triangle",
3820
+ build: (rng, baseSize)=>{
3821
+ const gap = baseSize * (0.6 + rng() * 0.3);
3822
+ return [
3823
+ {
3824
+ dx: 0,
3825
+ dy: 0,
3826
+ shape: "triangle",
3827
+ size: baseSize,
3828
+ rotation: rng() * 360
3829
+ },
3830
+ {
3831
+ dx: -gap,
3832
+ dy: gap * 0.3,
3833
+ shape: "circle",
3834
+ size: baseSize * 0.35,
3835
+ rotation: 0
3836
+ },
3837
+ {
3838
+ dx: gap,
3839
+ dy: gap * 0.3,
3840
+ shape: "circle",
3841
+ size: baseSize * 0.35,
3842
+ rotation: 0
3843
+ }
3844
+ ];
3845
+ }
3846
+ },
3847
+ {
3848
+ name: "hexagon-ring",
3849
+ build: (rng, baseSize)=>{
3850
+ const members = [];
3851
+ const count = 5 + Math.floor(rng() * 2);
3852
+ const ringR = baseSize * 0.6;
3853
+ for(let i = 0; i < count; i++){
3854
+ const angle = i / count * Math.PI * 2;
3855
+ members.push({
3856
+ dx: Math.cos(angle) * ringR,
3857
+ dy: Math.sin(angle) * ringR,
3858
+ shape: "hexagon",
3859
+ size: baseSize * (0.25 + rng() * 0.1),
3860
+ rotation: angle * 180 / Math.PI
3861
+ });
3862
+ }
3863
+ return members;
3864
+ }
3865
+ },
3866
+ {
3867
+ name: "spiral-dots",
3868
+ build: (rng, baseSize)=>{
3869
+ const members = [];
3870
+ const count = 7 + Math.floor(rng() * 5);
3871
+ const turns = 1.5 + rng();
3872
+ for(let i = 0; i < count; i++){
3873
+ const t = i / count;
3874
+ const angle = t * Math.PI * 2 * turns;
3875
+ const r = t * baseSize * 0.7;
3876
+ members.push({
3877
+ dx: Math.cos(angle) * r,
3878
+ dy: Math.sin(angle) * r,
3879
+ shape: "circle",
3880
+ size: baseSize * (0.08 + (1 - t) * 0.12),
3881
+ rotation: 0
3882
+ });
3883
+ }
3884
+ return members;
3885
+ }
3886
+ },
3887
+ {
3888
+ name: "diamond-cluster",
3889
+ build: (rng, baseSize)=>{
3890
+ const gap = baseSize * 0.45;
3891
+ return [
3892
+ {
3893
+ dx: 0,
3894
+ dy: -gap,
3895
+ shape: "diamond",
3896
+ size: baseSize * 0.4,
3897
+ rotation: 0
3898
+ },
3899
+ {
3900
+ dx: gap,
3901
+ dy: 0,
3902
+ shape: "diamond",
3903
+ size: baseSize * 0.35,
3904
+ rotation: 15
3905
+ },
3906
+ {
3907
+ dx: 0,
3908
+ dy: gap,
3909
+ shape: "diamond",
3910
+ size: baseSize * 0.3,
3911
+ rotation: 30
3912
+ },
3913
+ {
3914
+ dx: -gap,
3915
+ dy: 0,
3916
+ shape: "diamond",
3917
+ size: baseSize * 0.35,
3918
+ rotation: -15
3919
+ }
3920
+ ];
3921
+ }
3922
+ },
3923
+ {
3924
+ name: "crescent-pair",
3925
+ build: (rng, baseSize)=>{
3926
+ const gap = baseSize * 0.5;
3927
+ return [
3928
+ {
3929
+ dx: -gap * 0.4,
3930
+ dy: 0,
3931
+ shape: "crescent",
3932
+ size: baseSize * 0.5,
3933
+ rotation: rng() * 30
3934
+ },
3935
+ {
3936
+ dx: gap * 0.4,
3937
+ dy: 0,
3938
+ shape: "crescent",
3939
+ size: baseSize * 0.45,
3940
+ rotation: 180 + rng() * 30
3941
+ }
3942
+ ];
3943
+ }
3944
+ }
3945
+ ];
2073
3946
  function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2074
3947
  const finalConfig = {
2075
3948
  ...(0, $93cf69256c93baa9$export$c2f8e0cc249a8d8f),
@@ -2092,7 +3965,15 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2092
3965
  const [bgStart, bgEnd] = colorScheme.getBackgroundColorsByMode(archetype.paletteMode);
2093
3966
  const tempMode = colorScheme.getTemperatureMode();
2094
3967
  const fgTempTarget = tempMode === "warm-bg" ? "cool" : tempMode === "cool-bg" ? "warm" : null;
3968
+ // ── 0b. Color hierarchy — dominant/secondary/accent weighting ──
3969
+ const colorHierarchy = (0, $d016ad53434219a1$export$fabac4600b87056)(colors, rng);
3970
+ // ── 0c. Shape palette — curated shapes that work well together ──
2095
3971
  const shapeNames = Object.keys((0, $9c828bde2acaae64$export$4ff7fc6f1af248b5));
3972
+ const shapePalette = (0, $e73976f898150d4d$export$4a95df8944b5033b)(rng, shapeNames, archetype.name);
3973
+ // ── 0d. Color grading — unified tone for the whole image ───────
3974
+ const colorGrade = (0, $d016ad53434219a1$export$6d1620b367f86f7a)(rng);
3975
+ // ── 0e. Light direction — consistent shadow angle ──────────────
3976
+ const lightAngle = rng() * Math.PI * 2;
2096
3977
  const scaleFactor = Math.min(width, height) / 1024;
2097
3978
  const adjustedMinSize = minShapeSize * scaleFactor;
2098
3979
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -2101,27 +3982,45 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2101
3982
  // ── 1. Background ──────────────────────────────────────────────
2102
3983
  const bgRadius = Math.hypot(cx, cy);
2103
3984
  $4f72c5a314eddf25$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
3985
+ // Gradient mesh overlay — 3-4 color control points for richer backgrounds
3986
+ const meshPoints = 3 + Math.floor(rng() * 2);
3987
+ ctx.globalCompositeOperation = "soft-light";
3988
+ for(let i = 0; i < meshPoints; i++){
3989
+ const mx = rng() * width;
3990
+ const my = rng() * height;
3991
+ const mRadius = Math.min(width, height) * (0.3 + rng() * 0.4);
3992
+ const mColor = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
3993
+ const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
3994
+ grad.addColorStop(0, (0, $d016ad53434219a1$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
3995
+ grad.addColorStop(1, "rgba(0,0,0,0)");
3996
+ ctx.globalAlpha = 1;
3997
+ ctx.fillStyle = grad;
3998
+ ctx.fillRect(0, 0, width, height);
3999
+ }
4000
+ ctx.globalCompositeOperation = "source-over";
2104
4001
  // Compute average background luminance for contrast enforcement
2105
4002
  const bgLum = ((0, $d016ad53434219a1$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $d016ad53434219a1$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
2106
- // ── 1b. Layered background (Feature G) ─────────────────────────
2107
- // Draw large, very faint shapes to give the background texture
4003
+ // ── 1b. Layered background archetype-coherent shapes ─────────
2108
4004
  const bgShapeCount = 3 + Math.floor(rng() * 4);
2109
4005
  ctx.globalCompositeOperation = "soft-light";
2110
4006
  for(let i = 0; i < bgShapeCount; i++){
2111
4007
  const bx = rng() * width;
2112
4008
  const by = rng() * height;
2113
4009
  const bSize = width * 0.3 + rng() * width * 0.5;
2114
- const bColor = colors[Math.floor(rng() * colors.length)];
4010
+ const bColor = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
2115
4011
  ctx.globalAlpha = 0.03 + rng() * 0.05;
2116
4012
  ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(bColor, 0.15);
2117
4013
  ctx.beginPath();
2118
- ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
4014
+ // Use archetype-appropriate background shapes
4015
+ if (archetype.name === "geometric-precision" || archetype.name === "op-art") // Rectangular shapes for geometric archetypes
4016
+ ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
4017
+ else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
2119
4018
  ctx.fill();
2120
4019
  }
2121
4020
  // Subtle concentric rings from center
2122
4021
  const ringCount = 2 + Math.floor(rng() * 3);
2123
4022
  ctx.globalAlpha = 0.02 + rng() * 0.03;
2124
- ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colors[0], 0.1);
4023
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
2125
4024
  ctx.lineWidth = 1 * scaleFactor;
2126
4025
  for(let i = 1; i <= ringCount; i++){
2127
4026
  const r = Math.min(width, height) * 0.15 * i;
@@ -2130,12 +4029,70 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2130
4029
  ctx.stroke();
2131
4030
  }
2132
4031
  ctx.globalCompositeOperation = "source-over";
4032
+ // ── 1c. Background pattern layer — subtle textured paper ───────
4033
+ const bgPatternRoll = rng();
4034
+ if (bgPatternRoll < 0.6) {
4035
+ ctx.save();
4036
+ ctx.globalCompositeOperation = "soft-light";
4037
+ const patternOpacity = 0.02 + rng() * 0.04;
4038
+ const patternColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4039
+ if (bgPatternRoll < 0.2) {
4040
+ // Dot grid
4041
+ const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4042
+ const dotR = dotSpacing * 0.08;
4043
+ ctx.globalAlpha = patternOpacity;
4044
+ ctx.fillStyle = patternColor;
4045
+ for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4046
+ ctx.beginPath();
4047
+ ctx.arc(px, py, dotR, 0, Math.PI * 2);
4048
+ ctx.fill();
4049
+ }
4050
+ } else if (bgPatternRoll < 0.4) {
4051
+ // Diagonal lines
4052
+ const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4053
+ ctx.globalAlpha = patternOpacity;
4054
+ ctx.strokeStyle = patternColor;
4055
+ ctx.lineWidth = 0.5 * scaleFactor;
4056
+ const diag = Math.hypot(width, height);
4057
+ for(let d = -diag; d < diag; d += lineSpacing){
4058
+ ctx.beginPath();
4059
+ ctx.moveTo(d, 0);
4060
+ ctx.lineTo(d + height, height);
4061
+ ctx.stroke();
4062
+ }
4063
+ } else {
4064
+ // Tessellation — hexagonal grid of tiny shapes
4065
+ const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4066
+ const tessH = tessSize * Math.sqrt(3);
4067
+ ctx.globalAlpha = patternOpacity * 0.7;
4068
+ ctx.strokeStyle = patternColor;
4069
+ ctx.lineWidth = 0.4 * scaleFactor;
4070
+ for(let row = 0; row * tessH < height + tessH; row++){
4071
+ const offsetX = row % 2 * tessSize * 0.75;
4072
+ for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4073
+ const hx = col * tessSize * 1.5 + offsetX;
4074
+ const hy = row * tessH;
4075
+ ctx.beginPath();
4076
+ for(let s = 0; s < 6; s++){
4077
+ const angle = Math.PI / 3 * s - Math.PI / 6;
4078
+ const vx = hx + Math.cos(angle) * tessSize * 0.5;
4079
+ const vy = hy + Math.sin(angle) * tessSize * 0.5;
4080
+ if (s === 0) ctx.moveTo(vx, vy);
4081
+ else ctx.lineTo(vx, vy);
4082
+ }
4083
+ ctx.closePath();
4084
+ ctx.stroke();
4085
+ }
4086
+ }
4087
+ }
4088
+ ctx.restore();
4089
+ }
4090
+ ctx.globalCompositeOperation = "source-over";
2133
4091
  // ── 2. Composition mode ────────────────────────────────────────
2134
4092
  const compositionMode = $4f72c5a314eddf25$var$COMPOSITION_MODES[Math.floor(rng() * $4f72c5a314eddf25$var$COMPOSITION_MODES.length)];
2135
4093
  const symRoll = rng();
2136
4094
  const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
2137
4095
  // ── 3. Focal points + void zones ───────────────────────────────
2138
- // Rule-of-thirds intersection points for intentional composition
2139
4096
  const THIRDS_POINTS = [
2140
4097
  {
2141
4098
  x: 1 / 3,
@@ -2156,10 +4113,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2156
4113
  ];
2157
4114
  const numFocal = 1 + Math.floor(rng() * 2);
2158
4115
  const focalPoints = [];
2159
- for(let f = 0; f < numFocal; f++)// 70% chance to snap to a rule-of-thirds point, 30% free placement
2160
- if (rng() < 0.7) {
4116
+ for(let f = 0; f < numFocal; f++)if (rng() < 0.7) {
2161
4117
  const tp = THIRDS_POINTS[Math.floor(rng() * THIRDS_POINTS.length)];
2162
- // Small jitter around the thirds point so it's not robotic
2163
4118
  focalPoints.push({
2164
4119
  x: width * (tp.x + (rng() - 0.5) * 0.08),
2165
4120
  y: height * (tp.y + (rng() - 0.5) * 0.08),
@@ -2170,7 +4125,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2170
4125
  y: height * (0.2 + rng() * 0.6),
2171
4126
  strength: 0.3 + rng() * 0.4
2172
4127
  });
2173
- // Feature E: 1-2 void zones where shapes are sparse (negative space)
2174
4128
  const numVoids = Math.floor(rng() * 2) + 1;
2175
4129
  const voidZones = [];
2176
4130
  for(let v = 0; v < numVoids; v++)voidZones.push({
@@ -2202,20 +4156,24 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2202
4156
  }
2203
4157
  // Track all placed shapes for density checks and connecting curves
2204
4158
  const shapePositions = [];
4159
+ // Hero avoidance radius — shapes near the hero orient toward it
4160
+ let heroCenter = null;
2205
4161
  // ── 4b. Hero shape — a dominant focal element ───────────────────
2206
4162
  if (archetype.heroShape && rng() < 0.6) {
2207
4163
  const heroFocal = focalPoints[0];
4164
+ // Use shape palette hero candidates
2208
4165
  const heroPool = [
2209
- ...$4f72c5a314eddf25$var$SACRED_SHAPES,
2210
- "fibonacciSpiral",
2211
- "merkaba",
2212
- "fractal"
2213
- ];
2214
- const heroShape = heroPool.filter((s)=>shapeNames.includes(s))[Math.floor(rng() * heroPool.filter((s)=>shapeNames.includes(s)).length)] || shapeNames[Math.floor(rng() * shapeNames.length)];
4166
+ ...shapePalette.primary,
4167
+ ...shapePalette.supporting
4168
+ ].filter((s)=>(0, $e73976f898150d4d$export$4343b39fe47bd82c)[s]?.heroCandidate && shapeNames.includes(s));
4169
+ const heroShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : shapeNames[Math.floor(rng() * shapeNames.length)];
2215
4170
  const heroSize = adjustedMaxSize * (0.8 + rng() * 0.5);
2216
4171
  const heroRotation = rng() * 360;
2217
- const heroFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05), bgLum), 0.15 + rng() * 0.2);
2218
- const heroStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05), bgLum);
4172
+ const heroFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.dominant, rng, 6, 0.05), bgLum), 0.15 + rng() * 0.2);
4173
+ const heroStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.accent, rng, 6, 0.05), bgLum);
4174
+ // Get best style for this hero shape
4175
+ const heroProfile = (0, $e73976f898150d4d$export$4343b39fe47bd82c)[heroShape];
4176
+ const heroStyle = heroProfile ? heroProfile.bestStyles[Math.floor(rng() * heroProfile.bestStyles.length)] : rng() < 0.4 ? "watercolor" : "fill-and-stroke";
2219
4177
  ctx.globalAlpha = 0.5 + rng() * 0.2;
2220
4178
  (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, heroShape, heroFocal.x, heroFocal.y, {
2221
4179
  fillColor: heroFill,
@@ -2226,14 +4184,20 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2226
4184
  proportionType: "GOLDEN_RATIO",
2227
4185
  glowRadius: (12 + rng() * 20) * scaleFactor,
2228
4186
  glowColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(heroStroke, 0.4),
2229
- gradientFillEnd: (0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1),
2230
- renderStyle: rng() < 0.4 ? "watercolor" : "fill-and-stroke",
4187
+ gradientFillEnd: (0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
4188
+ renderStyle: heroStyle,
2231
4189
  rng: rng
2232
4190
  });
2233
- shapePositions.push({
4191
+ heroCenter = {
2234
4192
  x: heroFocal.x,
2235
4193
  y: heroFocal.y,
2236
4194
  size: heroSize
4195
+ };
4196
+ shapePositions.push({
4197
+ x: heroFocal.x,
4198
+ y: heroFocal.y,
4199
+ size: heroSize,
4200
+ shape: heroShape
2237
4201
  });
2238
4202
  }
2239
4203
  // ── 5. Shape layers ────────────────────────────────────────────
@@ -2244,46 +4208,64 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2244
4208
  const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
2245
4209
  const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
2246
4210
  const layerSizeScale = 1 - layer * 0.15;
2247
- // Feature B: per-layer blend mode
4211
+ // Per-layer blend mode
2248
4212
  const layerBlend = (0, $c3de8257a8baa3b0$export$7bb7bff4e26fa06b)(rng);
2249
4213
  ctx.globalCompositeOperation = layerBlend;
2250
- // Feature C: per-layer render style bias — prefer archetype styles
4214
+ // Per-layer render style bias — prefer archetype styles
2251
4215
  const layerRenderStyle = rng() < 0.6 ? archetype.preferredStyles[Math.floor(rng() * archetype.preferredStyles.length)] : (0, $c3de8257a8baa3b0$export$9fd4e64b2acd410e)(rng);
2252
- // Feature D: atmospheric desaturation for later layers
2253
- const atmosphericDesat = layerRatio * 0.3; // 0 for first layer, up to 0.3 for last
4216
+ // Atmospheric desaturation for later layers
4217
+ const atmosphericDesat = layerRatio * 0.3;
4218
+ // Depth-of-field simulation — later layers are "further away"
4219
+ // Reduce stroke widths and shift colors toward the background
4220
+ const dofFactor = 1 - layerRatio * 0.5; // 1.0 for front layer, 0.5 for back
4221
+ const dofStrokeScale = 0.4 + dofFactor * 0.6; // strokes thin out with depth
4222
+ const dofContrastReduction = layerRatio * 0.2; // colors fade toward bg
2254
4223
  for(let i = 0; i < numShapes; i++){
2255
4224
  // Position from composition mode, then focal bias
2256
4225
  const rawPos = $4f72c5a314eddf25$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
2257
4226
  const [x, y] = applyFocalBias(rawPos.x, rawPos.y);
2258
- // Feature E: skip shapes in void zones, reduce in dense areas
4227
+ // Skip shapes in void zones, reduce in dense areas
2259
4228
  if ($4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones)) {
2260
- // 85% chance to skip — allows a few shapes to bleed in
2261
4229
  if (rng() < 0.85) continue;
2262
4230
  }
2263
4231
  if ($4f72c5a314eddf25$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
2264
- if (rng() < 0.6) continue; // thin out dense areas
4232
+ if (rng() < 0.6) continue;
2265
4233
  }
2266
- // Weighted shape selection
2267
- const shape = $4f72c5a314eddf25$var$pickShape(rng, layerRatio, shapeNames);
2268
4234
  // Power distribution for size — archetype controls the curve
2269
4235
  const sizeT = Math.pow(rng(), archetype.sizePower);
2270
4236
  const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
4237
+ // Size fraction for affinity-aware shape selection
4238
+ const sizeFraction = size / adjustedMaxSize;
4239
+ // Palette-driven shape selection (replaces naive pickShape)
4240
+ const shape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, sizeFraction);
2271
4241
  // Flow-field rotation in flow-field mode, random otherwise
2272
- const rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
2273
- // Positional color blending + jitter
2274
- let fillBase = $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, colors, rng);
2275
- const strokeBase = colors[Math.floor(rng() * colors.length)];
2276
- // Feature D: desaturate colors on later layers for depth
4242
+ let rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
4243
+ // Hero avoidance: shapes near the hero orient toward it
4244
+ if (heroCenter) {
4245
+ const distToHero = Math.hypot(x - heroCenter.x, y - heroCenter.y);
4246
+ const heroInfluence = heroCenter.size * 1.5;
4247
+ if (distToHero < heroInfluence && distToHero > 0) {
4248
+ const angleToHero = Math.atan2(heroCenter.y - y, heroCenter.x - x) * 180 / Math.PI;
4249
+ const blendFactor = 1 - distToHero / heroInfluence;
4250
+ rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
4251
+ }
4252
+ }
4253
+ // Positional color from hierarchy + jitter
4254
+ let fillBase = $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
4255
+ const strokeBase = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4256
+ // Desaturate colors on later layers for depth
2277
4257
  if (atmosphericDesat > 0) fillBase = (0, $d016ad53434219a1$export$fb75607d98509d9)(fillBase, atmosphericDesat);
2278
4258
  // Temperature contrast: shift foreground shapes opposite to background
2279
4259
  if (fgTempTarget) fillBase = (0, $d016ad53434219a1$export$51ea55f869b7e0d3)(fillBase, fgTempTarget, 0.15 + layerRatio * 0.1);
2280
- const fillColor = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$59539d800dbe6858)(fillBase, rng, 0.06), bgLum);
2281
- const strokeColor = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$59539d800dbe6858)(strokeBase, rng, 0.05), bgLum);
4260
+ const fillColor = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(fillBase, rng, 6, 0.05), bgLum);
4261
+ const strokeColor = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
2282
4262
  // Semi-transparent fill
2283
4263
  const fillAlpha = 0.2 + rng() * 0.5;
2284
4264
  const transparentFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, fillAlpha);
2285
- const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor;
2286
- ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5);
4265
+ const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor * dofStrokeScale;
4266
+ // Depth-of-field: reduce opacity slightly for distant layers
4267
+ const dofOpacityScale = 1 - dofContrastReduction;
4268
+ ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5) * dofOpacityScale;
2287
4269
  // Glow on sacred shapes more often — scaled by archetype
2288
4270
  const isSacred = $4f72c5a314eddf25$var$SACRED_SHAPES.includes(shape);
2289
4271
  const baseGlowChance = isSacred ? 0.45 : 0.2;
@@ -2292,58 +4274,177 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2292
4274
  const glowRadius = hasGlow ? (8 + rng() * 20) * scaleFactor : 0;
2293
4275
  // Gradient fill on ~30%
2294
4276
  const hasGradient = rng() < 0.3;
2295
- const gradientEnd = hasGradient ? (0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1) : undefined;
2296
- // Feature C: per-shape render style (70% use layer style, 30% pick their own)
2297
- const shapeRenderStyle = rng() < 0.7 ? layerRenderStyle : (0, $c3de8257a8baa3b0$export$9fd4e64b2acd410e)(rng);
2298
- // Feature F: organic edge jitter — applied via watercolor style on ~15% of shapes
4277
+ const gradientEnd = hasGradient ? (0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1) : undefined;
4278
+ // Affinity-aware render style selection
4279
+ const shapeRenderStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
4280
+ // Organic edge jitter — applied via watercolor style on ~15% of shapes
2299
4281
  const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
2300
4282
  const finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
2301
- (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
4283
+ // Consistent light direction subtle shadow offset
4284
+ const shadowDist = hasGlow ? 0 : size * 0.02;
4285
+ const shadowOffX = shadowDist * Math.cos(lightAngle);
4286
+ const shadowOffY = shadowDist * Math.sin(lightAngle);
4287
+ // ── 5a. Tangent placement — nudge toward nearest shape edge ──
4288
+ let finalX = x;
4289
+ let finalY = y;
4290
+ if (shapePositions.length > 0 && rng() < 0.25) {
4291
+ // Find nearest placed shape
4292
+ let nearestDist = Infinity;
4293
+ let nearestPos = null;
4294
+ for (const sp of shapePositions){
4295
+ const d = Math.hypot(x - sp.x, y - sp.y);
4296
+ if (d < nearestDist && d > 0) {
4297
+ nearestDist = d;
4298
+ nearestPos = sp;
4299
+ }
4300
+ }
4301
+ if (nearestPos) {
4302
+ // Target distance: edges kissing (sum of half-sizes)
4303
+ const targetDist = (size + nearestPos.size) * 0.5;
4304
+ if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
4305
+ const angle = Math.atan2(y - nearestPos.y, x - nearestPos.x);
4306
+ finalX = nearestPos.x + Math.cos(angle) * targetDist;
4307
+ finalY = nearestPos.y + Math.sin(angle) * targetDist;
4308
+ // Keep in bounds
4309
+ finalX = Math.max(0, Math.min(width, finalX));
4310
+ finalY = Math.max(0, Math.min(height, finalY));
4311
+ }
4312
+ }
4313
+ }
4314
+ // ── 5b. Shape mirroring — basic shapes get reflected copies ──
4315
+ const mirrorAxis = (0, $c3de8257a8baa3b0$export$879206e23912d1a9)(rng);
4316
+ const isBasicShape = [
4317
+ "circle",
4318
+ "triangle",
4319
+ "square",
4320
+ "hexagon",
4321
+ "star",
4322
+ "diamond",
4323
+ "crescent",
4324
+ "penroseTile",
4325
+ "reuleauxTriangle"
4326
+ ].includes(shape);
4327
+ const shouldMirror = mirrorAxis !== null && isBasicShape && size > adjustedMaxSize * 0.2;
4328
+ const shapeConfig = {
2302
4329
  fillColor: transparentFill,
2303
4330
  strokeColor: strokeColor,
2304
4331
  strokeWidth: strokeWidth,
2305
4332
  size: size,
2306
4333
  rotation: rotation,
2307
4334
  proportionType: "GOLDEN_RATIO",
2308
- glowRadius: glowRadius,
2309
- glowColor: hasGlow ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.6) : undefined,
4335
+ glowRadius: glowRadius || (shadowDist > 0 ? shadowDist * 2 : 0),
4336
+ glowColor: hasGlow ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
2310
4337
  gradientFillEnd: gradientEnd,
2311
4338
  renderStyle: finalRenderStyle,
2312
4339
  rng: rng
4340
+ };
4341
+ if (shouldMirror) (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4342
+ ...shapeConfig,
4343
+ mirrorAxis: mirrorAxis,
4344
+ mirrorGap: size * (0.1 + rng() * 0.3)
2313
4345
  });
4346
+ else (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
2314
4347
  shapePositions.push({
2315
- x: x,
2316
- y: y,
2317
- size: size
4348
+ x: finalX,
4349
+ y: finalY,
4350
+ size: size,
4351
+ shape: shape
2318
4352
  });
2319
- // ── 5b. Recursive nesting ──────────────────────────────────
4353
+ // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
4354
+ if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
4355
+ const echoCount = 2 + Math.floor(rng() * 2);
4356
+ const echoAngle = rng() * Math.PI * 2;
4357
+ for(let e = 0; e < echoCount; e++){
4358
+ const echoScale = 0.3 - e * 0.08;
4359
+ const echoDist = size * (0.6 + e * 0.4);
4360
+ const echoX = finalX + Math.cos(echoAngle) * echoDist;
4361
+ const echoY = finalY + Math.sin(echoAngle) * echoDist;
4362
+ const echoSize = size * Math.max(0.1, echoScale);
4363
+ if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
4364
+ ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
4365
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
4366
+ fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
4367
+ strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.4),
4368
+ strokeWidth: strokeWidth * 0.6,
4369
+ size: echoSize,
4370
+ rotation: rotation + (e + 1) * 15,
4371
+ proportionType: "GOLDEN_RATIO",
4372
+ renderStyle: finalRenderStyle,
4373
+ rng: rng
4374
+ });
4375
+ shapePositions.push({
4376
+ x: echoX,
4377
+ y: echoY,
4378
+ size: echoSize,
4379
+ shape: shape
4380
+ });
4381
+ }
4382
+ }
4383
+ // ── 5d. Recursive nesting ──────────────────────────────────
2320
4384
  if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
2321
4385
  const innerCount = 1 + Math.floor(rng() * 3);
2322
4386
  for(let n = 0; n < innerCount; n++){
2323
- const innerShape = $4f72c5a314eddf25$var$pickShape(rng, Math.min(1, layerRatio + 0.3), shapeNames);
4387
+ // Pick inner shape from palette affinities
4388
+ const innerSizeFraction = size * 0.25 / adjustedMaxSize;
4389
+ const innerShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
2324
4390
  const innerSize = size * (0.15 + rng() * 0.25);
2325
4391
  const innerOffX = (rng() - 0.5) * size * 0.4;
2326
4392
  const innerOffY = (rng() - 0.5) * size * 0.4;
2327
4393
  const innerRot = rng() * 360;
2328
- const innerFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1), 0.3 + rng() * 0.4);
4394
+ const innerFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
2329
4395
  ctx.globalAlpha = layerOpacity * 0.7;
2330
- (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, innerShape, x + innerOffX, y + innerOffY, {
4396
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
2331
4397
  fillColor: innerFill,
2332
4398
  strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.5),
2333
4399
  strokeWidth: strokeWidth * 0.6,
2334
4400
  size: innerSize,
2335
4401
  rotation: innerRot,
2336
4402
  proportionType: "GOLDEN_RATIO",
2337
- renderStyle: shapeRenderStyle,
4403
+ renderStyle: (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng),
4404
+ rng: rng
4405
+ });
4406
+ }
4407
+ }
4408
+ // ── 5e. Shape constellations — pre-composed groups ─────────
4409
+ if (size > adjustedMaxSize * 0.35 && rng() < 0.12) {
4410
+ const constellation = $4f72c5a314eddf25$var$CONSTELLATIONS[Math.floor(rng() * $4f72c5a314eddf25$var$CONSTELLATIONS.length)];
4411
+ const members = constellation.build(rng, size);
4412
+ const groupRotation = rng() * Math.PI * 2;
4413
+ const cosR = Math.cos(groupRotation);
4414
+ const sinR = Math.sin(groupRotation);
4415
+ for (const member of members){
4416
+ // Rotate the group offset by the group rotation
4417
+ const mx = finalX + member.dx * cosR - member.dy * sinR;
4418
+ const my = finalY + member.dx * sinR + member.dy * cosR;
4419
+ if (mx < 0 || mx > width || my < 0 || my > height) continue;
4420
+ const memberFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
4421
+ const memberStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
4422
+ ctx.globalAlpha = layerOpacity * 0.6;
4423
+ // Use the member's shape if available, otherwise fall back to palette
4424
+ const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
4425
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
4426
+ fillColor: memberFill,
4427
+ strokeColor: memberStroke,
4428
+ strokeWidth: strokeWidth * 0.7,
4429
+ size: member.size,
4430
+ rotation: member.rotation + groupRotation * 180 / Math.PI,
4431
+ proportionType: "GOLDEN_RATIO",
4432
+ renderStyle: (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng),
2338
4433
  rng: rng
2339
4434
  });
4435
+ shapePositions.push({
4436
+ x: mx,
4437
+ y: my,
4438
+ size: member.size,
4439
+ shape: memberShape
4440
+ });
2340
4441
  }
2341
4442
  }
2342
4443
  }
2343
4444
  }
2344
4445
  // Reset blend mode for post-processing passes
2345
4446
  ctx.globalCompositeOperation = "source-over";
2346
- // ── 6. Flow-line pass (Feature H: tapered brush strokes) ───────
4447
+ // ── 6. Flow-line pass variable color, branching, pressure ────
2347
4448
  const baseFlowLines = 6 + Math.floor(rng() * 10);
2348
4449
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
2349
4450
  for(let i = 0; i < numFlowLines; i++){
@@ -2352,9 +4453,13 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2352
4453
  const steps = 30 + Math.floor(rng() * 40);
2353
4454
  const stepLen = (3 + rng() * 5) * scaleFactor;
2354
4455
  const startWidth = (1 + rng() * 3) * scaleFactor;
2355
- const lineColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)(colors[Math.floor(rng() * colors.length)], bgLum), 0.4);
4456
+ // Variable color: interpolate between two hierarchy colors along the stroke
4457
+ const lineColorStart = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
4458
+ const lineColorEnd = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
2356
4459
  const lineAlpha = 0.06 + rng() * 0.1;
2357
- // Draw as individual segments with tapering width
4460
+ // Pressure simulation: sinusoidal width variation
4461
+ const pressureFreq = 2 + rng() * 4;
4462
+ const pressurePhase = rng() * Math.PI * 2;
2358
4463
  let prevX = fx;
2359
4464
  let prevY = fy;
2360
4465
  for(let s = 0; s < steps; s++){
@@ -2362,37 +4467,61 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2362
4467
  fx += Math.cos(angle) * stepLen;
2363
4468
  fy += Math.sin(angle) * stepLen;
2364
4469
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
2365
- // Taper: thick at start, thin at end
2366
- const taper = 1 - s / steps * 0.8;
4470
+ const t = s / steps;
4471
+ // Taper + pressure
4472
+ const taper = 1 - t * 0.8;
4473
+ const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
2367
4474
  ctx.globalAlpha = lineAlpha * taper;
4475
+ // Interpolate color along stroke
4476
+ const lineColor = t < 0.5 ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $d016ad53434219a1$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
2368
4477
  ctx.strokeStyle = lineColor;
2369
- ctx.lineWidth = startWidth * taper;
4478
+ ctx.lineWidth = startWidth * taper * pressure;
2370
4479
  ctx.lineCap = "round";
2371
4480
  ctx.beginPath();
2372
4481
  ctx.moveTo(prevX, prevY);
2373
4482
  ctx.lineTo(fx, fy);
2374
4483
  ctx.stroke();
4484
+ // Branching: ~12% chance per step to spawn a thinner child stroke
4485
+ if (rng() < 0.12 && s > 5 && s < steps - 10) {
4486
+ const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
4487
+ let bx = fx;
4488
+ let by = fy;
4489
+ let bPrevX = fx;
4490
+ let bPrevY = fy;
4491
+ const branchSteps = 5 + Math.floor(rng() * 10);
4492
+ const branchWidth = startWidth * taper * 0.4;
4493
+ for(let bs = 0; bs < branchSteps; bs++){
4494
+ const bAngle = branchAngle + (rng() - 0.5) * 0.2;
4495
+ bx += Math.cos(bAngle) * stepLen * 0.8;
4496
+ by += Math.sin(bAngle) * stepLen * 0.8;
4497
+ if (bx < 0 || bx > width || by < 0 || by > height) break;
4498
+ const bTaper = 1 - bs / branchSteps * 0.9;
4499
+ ctx.globalAlpha = lineAlpha * taper * bTaper * 0.6;
4500
+ ctx.lineWidth = branchWidth * bTaper;
4501
+ ctx.beginPath();
4502
+ ctx.moveTo(bPrevX, bPrevY);
4503
+ ctx.lineTo(bx, by);
4504
+ ctx.stroke();
4505
+ bPrevX = bx;
4506
+ bPrevY = by;
4507
+ }
4508
+ }
2375
4509
  prevX = fx;
2376
4510
  prevY = fy;
2377
4511
  }
2378
4512
  }
2379
4513
  // ── 6b. Apply symmetry mirroring ─────────────────────────────────
2380
- // Mirror the rendered content (shapes + flow lines) before post-processing.
2381
- // Uses ctx.canvas which is available in both Node (@napi-rs/canvas) and browsers.
2382
4514
  if (symmetryMode !== "none") {
2383
4515
  const canvas = ctx.canvas;
2384
4516
  ctx.save();
2385
4517
  if (symmetryMode === "bilateral-x" || symmetryMode === "quad") {
2386
- // Mirror left half onto right half
2387
4518
  ctx.save();
2388
4519
  ctx.translate(width, 0);
2389
4520
  ctx.scale(-1, 1);
2390
- // Draw the left half (0 to cx) onto the mirrored right side
2391
4521
  ctx.drawImage(canvas, 0, 0, Math.ceil(cx), height, 0, 0, Math.ceil(cx), height);
2392
4522
  ctx.restore();
2393
4523
  }
2394
4524
  if (symmetryMode === "bilateral-y" || symmetryMode === "quad") {
2395
- // Mirror top half onto bottom half
2396
4525
  ctx.save();
2397
4526
  ctx.translate(0, height);
2398
4527
  ctx.scale(1, -1);
@@ -2415,7 +4544,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2415
4544
  }
2416
4545
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
2417
4546
  ctx.globalAlpha = 1;
2418
- const vignetteStrength = 0.25 + rng() * 0.2; // 25-45% edge darkening
4547
+ const vignetteStrength = 0.25 + rng() * 0.2;
2419
4548
  const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
2420
4549
  vigGrad.addColorStop(0, "rgba(0,0,0,0)");
2421
4550
  vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
@@ -2441,13 +4570,57 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2441
4570
  const cpx = mx + -dy / (dist || 1) * bulge;
2442
4571
  const cpy = my + dx / (dist || 1) * bulge;
2443
4572
  ctx.globalAlpha = 0.06 + rng() * 0.1;
2444
- ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)(colors[Math.floor(rng() * colors.length)], bgLum), 0.3);
4573
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
2445
4574
  ctx.beginPath();
2446
4575
  ctx.moveTo(a.x, a.y);
2447
4576
  ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);
2448
4577
  ctx.stroke();
2449
4578
  }
2450
4579
  }
4580
+ // ── 10. Post-processing ────────────────────────────────────────
4581
+ // 10a. Color grading — unified tone across the whole image
4582
+ // Apply as a semi-transparent overlay in the grade hue
4583
+ ctx.globalAlpha = colorGrade.intensity * 0.25;
4584
+ ctx.globalCompositeOperation = "soft-light";
4585
+ const gradeHsl = `hsl(${Math.round(colorGrade.hue)}, 40%, 50%)`;
4586
+ ctx.fillStyle = gradeHsl;
4587
+ ctx.fillRect(0, 0, width, height);
4588
+ ctx.globalCompositeOperation = "source-over";
4589
+ // 10b. Chromatic aberration — subtle RGB channel offset at edges
4590
+ // Only apply for neon/cosmic/ethereal archetypes where it fits
4591
+ const chromaArchetypes = [
4592
+ "neon-glow",
4593
+ "cosmic",
4594
+ "ethereal"
4595
+ ];
4596
+ if (chromaArchetypes.includes(archetype.name)) {
4597
+ const chromaOffset = Math.ceil(2 * scaleFactor);
4598
+ const canvas = ctx.canvas;
4599
+ // Shift red channel slightly
4600
+ ctx.globalAlpha = 0.03;
4601
+ ctx.globalCompositeOperation = "screen";
4602
+ ctx.drawImage(canvas, chromaOffset, 0, width, height, 0, 0, width, height);
4603
+ // Shift blue channel opposite
4604
+ ctx.drawImage(canvas, -chromaOffset, 0, width, height, 0, 0, width, height);
4605
+ ctx.globalCompositeOperation = "source-over";
4606
+ }
4607
+ // 10c. Bloom — soft glow on bright areas for neon/cosmic archetypes
4608
+ const bloomArchetypes = [
4609
+ "neon-glow",
4610
+ "cosmic"
4611
+ ];
4612
+ if (bloomArchetypes.includes(archetype.name)) {
4613
+ const canvas = ctx.canvas;
4614
+ ctx.globalAlpha = 0.08;
4615
+ ctx.globalCompositeOperation = "screen";
4616
+ // Draw the image slightly scaled up and blurred via shadow
4617
+ ctx.save();
4618
+ ctx.shadowBlur = 30 * scaleFactor;
4619
+ ctx.shadowColor = "rgba(255,255,255,0.3)";
4620
+ ctx.drawImage(canvas, 0, 0, width, height);
4621
+ ctx.restore();
4622
+ ctx.globalCompositeOperation = "source-over";
4623
+ }
2451
4624
  ctx.globalAlpha = 1;
2452
4625
  }
2453
4626