git-hash-art 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.js CHANGED
@@ -518,6 +518,18 @@ function $b5a262d09b87e373$export$6d1620b367f86f7a(rng) {
518
518
  intensity: intensity
519
519
  };
520
520
  }
521
+ function $b5a262d09b87e373$export$1793a1bfbe4f6ff5(hex, degrees) {
522
+ const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
523
+ return $b5a262d09b87e373$var$hslToHex((h + degrees + 360) % 360, s, l);
524
+ }
525
+ function $b5a262d09b87e373$export$703ba40a4347f77a(base, layerRatio, hueShiftPerLayer) {
526
+ const shift = layerRatio * hueShiftPerLayer;
527
+ return {
528
+ dominant: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.dominant, shift),
529
+ secondary: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
530
+ accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
531
+ };
532
+ }
521
533
 
522
534
 
523
535
 
@@ -1308,35 +1320,32 @@ const $77711f013715e6da$export$c2fc138f94dd4b2a = {
1308
1320
  */ const $2899cc29bfcdb86b$export$580f80cfb9de73bc = (ctx, size, config)=>{
1309
1321
  const rng = config?.rng ?? Math.random;
1310
1322
  const r = size / 2;
1311
- const numPoints = 5 + Math.floor(rng() * 5); // 5-9 lobes
1323
+ const numPoints = 5 + Math.floor(rng() * 5);
1312
1324
  const points = [];
1313
1325
  for(let i = 0; i < numPoints; i++){
1314
1326
  const angle = i / numPoints * Math.PI * 2;
1315
- const jitter = 0.5 + rng() * 0.5; // radius varies 50-100%
1327
+ const jitter = 0.5 + rng() * 0.5;
1316
1328
  points.push({
1317
1329
  x: Math.cos(angle) * r * jitter,
1318
1330
  y: Math.sin(angle) * r * jitter
1319
1331
  });
1320
1332
  }
1321
1333
  ctx.beginPath();
1322
- // Start at midpoint between last and first point
1323
1334
  const last = points[points.length - 1];
1324
1335
  const first = points[0];
1325
1336
  ctx.moveTo((last.x + first.x) / 2, (last.y + first.y) / 2);
1326
1337
  for(let i = 0; i < numPoints; i++){
1327
1338
  const curr = points[i];
1328
1339
  const next = points[(i + 1) % numPoints];
1329
- const midX = (curr.x + next.x) / 2;
1330
- const midY = (curr.y + next.y) / 2;
1331
- ctx.quadraticCurveTo(curr.x, curr.y, midX, midY);
1340
+ ctx.quadraticCurveTo(curr.x, curr.y, (curr.x + next.x) / 2, (curr.y + next.y) / 2);
1332
1341
  }
1333
1342
  ctx.closePath();
1334
1343
  };
1335
1344
  const $2899cc29bfcdb86b$export$7a6094023f0902a6 = (ctx, size, config)=>{
1336
1345
  const rng = config?.rng ?? Math.random;
1337
1346
  const r = size / 2;
1338
- const sides = 3 + Math.floor(rng() * 10); // 3-12 sides
1339
- const jitterAmount = 0.1 + rng() * 0.4; // 10-50% vertex displacement
1347
+ const sides = 3 + Math.floor(rng() * 10);
1348
+ const jitterAmount = 0.1 + rng() * 0.4;
1340
1349
  ctx.beginPath();
1341
1350
  for(let i = 0; i < sides; i++){
1342
1351
  const angle = i / sides * Math.PI * 2 - Math.PI / 2;
@@ -1351,10 +1360,9 @@ const $2899cc29bfcdb86b$export$7a6094023f0902a6 = (ctx, size, config)=>{
1351
1360
  const $2899cc29bfcdb86b$export$ef56b4a8316e47d5 = (ctx, size, config)=>{
1352
1361
  const rng = config?.rng ?? Math.random;
1353
1362
  const r = size / 2;
1354
- // Frequency ratios small integers produce recognizable patterns
1355
- const freqA = 1 + Math.floor(rng() * 5); // 1-5
1356
- const freqB = 1 + Math.floor(rng() * 5); // 1-5
1357
- const phase = rng() * Math.PI; // phase offset
1363
+ const freqA = 1 + Math.floor(rng() * 5);
1364
+ const freqB = 1 + Math.floor(rng() * 5);
1365
+ const phase = rng() * Math.PI;
1358
1366
  const steps = 120;
1359
1367
  ctx.beginPath();
1360
1368
  for(let i = 0; i <= steps; i++){
@@ -1369,7 +1377,6 @@ const $2899cc29bfcdb86b$export$ef56b4a8316e47d5 = (ctx, size, config)=>{
1369
1377
  const $2899cc29bfcdb86b$export$1db9219b4f34658c = (ctx, size, config)=>{
1370
1378
  const rng = config?.rng ?? Math.random;
1371
1379
  const r = size / 2;
1372
- // Exponent range: 0.3 (spiky astroid) to 5 (rounded rectangle)
1373
1380
  const n = 0.3 + rng() * 4.7;
1374
1381
  const steps = 120;
1375
1382
  ctx.beginPath();
@@ -1377,7 +1384,6 @@ const $2899cc29bfcdb86b$export$1db9219b4f34658c = (ctx, size, config)=>{
1377
1384
  const t = i / steps * Math.PI * 2;
1378
1385
  const cosT = Math.cos(t);
1379
1386
  const sinT = Math.sin(t);
1380
- // Superellipse parametric form
1381
1387
  const x = Math.sign(cosT) * Math.pow(Math.abs(cosT), 2 / n) * r;
1382
1388
  const y = Math.sign(sinT) * Math.pow(Math.abs(sinT), 2 / n) * r;
1383
1389
  if (i === 0) ctx.moveTo(x, y);
@@ -1388,11 +1394,9 @@ const $2899cc29bfcdb86b$export$1db9219b4f34658c = (ctx, size, config)=>{
1388
1394
  const $2899cc29bfcdb86b$export$b027c64d22b01985 = (ctx, size, config)=>{
1389
1395
  const rng = config?.rng ?? Math.random;
1390
1396
  const scale = size / 2;
1391
- // R = outer radius, r = inner radius, d = pen distance from inner center
1392
1397
  const R = 1;
1393
- const r = 0.2 + rng() * 0.6; // 0.2-0.8
1394
- const d = 0.3 + rng() * 0.7; // 0.3-1.0
1395
- // Number of full rotations needed to close the curve
1398
+ const r = 0.2 + rng() * 0.6;
1399
+ const d = 0.3 + rng() * 0.7;
1396
1400
  const gcd = (a, b)=>{
1397
1401
  const ai = Math.round(a * 1000);
1398
1402
  const bi = Math.round(b * 1000);
@@ -1400,7 +1404,7 @@ const $2899cc29bfcdb86b$export$b027c64d22b01985 = (ctx, size, config)=>{
1400
1404
  return g(ai, bi) / 1000;
1401
1405
  };
1402
1406
  const period = r / gcd(R, r);
1403
- const maxT = Math.min(period, 10) * Math.PI * 2; // cap at 10 rotations
1407
+ const maxT = Math.min(period, 10) * Math.PI * 2;
1404
1408
  const steps = Math.min(600, Math.floor(maxT * 20));
1405
1409
  ctx.beginPath();
1406
1410
  for(let i = 0; i <= steps; i++){
@@ -1415,9 +1419,9 @@ const $2899cc29bfcdb86b$export$b027c64d22b01985 = (ctx, size, config)=>{
1415
1419
  const $2899cc29bfcdb86b$export$7608ccd03bfb705d = (ctx, size, config)=>{
1416
1420
  const rng = config?.rng ?? Math.random;
1417
1421
  const r = size / 2;
1418
- const rings = 2 + Math.floor(rng() * 4); // 2-5 rings
1419
- const freq = 3 + Math.floor(rng() * 12); // 3-14 waves per ring
1420
- const amp = 0.05 + rng() * 0.15; // 5-20% of radius
1422
+ const rings = 2 + Math.floor(rng() * 4);
1423
+ const freq = 3 + Math.floor(rng() * 12);
1424
+ const amp = 0.05 + rng() * 0.15;
1421
1425
  ctx.beginPath();
1422
1426
  for(let ring = 0; ring < rings; ring++){
1423
1427
  const baseR = r * (0.3 + ring / rings * 0.7);
@@ -1435,7 +1439,7 @@ const $2899cc29bfcdb86b$export$7608ccd03bfb705d = (ctx, size, config)=>{
1435
1439
  const $2899cc29bfcdb86b$export$11a377e7498bb523 = (ctx, size, config)=>{
1436
1440
  const rng = config?.rng ?? Math.random;
1437
1441
  const r = size / 2;
1438
- const k = 2 + Math.floor(rng() * 6); // 2-7 petal parameter
1442
+ const k = 2 + Math.floor(rng() * 6);
1439
1443
  const steps = 200;
1440
1444
  ctx.beginPath();
1441
1445
  for(let i = 0; i <= steps; i++){
@@ -1448,6 +1452,308 @@ const $2899cc29bfcdb86b$export$11a377e7498bb523 = (ctx, size, config)=>{
1448
1452
  }
1449
1453
  ctx.closePath();
1450
1454
  };
1455
+ const $2899cc29bfcdb86b$export$76b6526575ea179b = (ctx, size, config)=>{
1456
+ const rng = config?.rng ?? Math.random;
1457
+ const r = size / 2;
1458
+ const shardCount = 4 + Math.floor(rng() * 5); // 4-8 shards
1459
+ ctx.beginPath();
1460
+ for(let s = 0; s < shardCount; s++){
1461
+ const baseAngle = s / shardCount * Math.PI * 2 + (rng() - 0.5) * 0.3;
1462
+ const dist = r * (0.15 + rng() * 0.35);
1463
+ const cx = Math.cos(baseAngle) * dist;
1464
+ const cy = Math.sin(baseAngle) * dist;
1465
+ const shardSize = r * (0.2 + rng() * 0.4);
1466
+ const verts = 3 + Math.floor(rng() * 3); // 3-5 vertices per shard
1467
+ const shardAngleOffset = rng() * Math.PI * 2;
1468
+ for(let v = 0; v < verts; v++){
1469
+ const angle = shardAngleOffset + v / verts * Math.PI * 2;
1470
+ // Elongate shards along their radial direction
1471
+ const stretch = v % 2 === 0 ? 1.0 : 0.3 + rng() * 0.4;
1472
+ const px = cx + Math.cos(angle) * shardSize * stretch;
1473
+ const py = cy + Math.sin(angle) * shardSize * stretch;
1474
+ if (v === 0) ctx.moveTo(px, py);
1475
+ else ctx.lineTo(px, py);
1476
+ }
1477
+ ctx.closePath();
1478
+ }
1479
+ };
1480
+ const $2899cc29bfcdb86b$export$ed9ff98da5b05073 = (ctx, size, config)=>{
1481
+ const rng = config?.rng ?? Math.random;
1482
+ const r = size / 2;
1483
+ const edgeCount = 5 + Math.floor(rng() * 4); // 5-8 edges
1484
+ const points = [];
1485
+ // Generate edge midpoints at varying distances
1486
+ for(let i = 0; i < edgeCount; i++){
1487
+ const angle = i / edgeCount * Math.PI * 2 + (rng() - 0.5) * 0.4;
1488
+ const dist = r * (0.6 + rng() * 0.4);
1489
+ points.push({
1490
+ angle: angle,
1491
+ x: Math.cos(angle) * dist,
1492
+ y: Math.sin(angle) * dist
1493
+ });
1494
+ }
1495
+ // Sort by angle for proper winding
1496
+ points.sort((a, b)=>a.angle - b.angle);
1497
+ ctx.beginPath();
1498
+ ctx.moveTo(points[0].x, points[0].y);
1499
+ for(let i = 1; i < points.length; i++)ctx.lineTo(points[i].x, points[i].y);
1500
+ ctx.closePath();
1501
+ };
1502
+ const $2899cc29bfcdb86b$export$e0452d9a794fe7e5 = (ctx, size, config)=>{
1503
+ const rng = config?.rng ?? Math.random;
1504
+ const r = size / 2;
1505
+ const biteSize = 0.6 + rng() * 0.3; // 60-90% of radius
1506
+ const biteOffset = r * (0.3 + rng() * 0.4);
1507
+ const biteAngle = rng() * Math.PI * 2;
1508
+ // Outer circle
1509
+ ctx.beginPath();
1510
+ ctx.arc(0, 0, r, 0, Math.PI * 2);
1511
+ // Subtract inner circle using even-odd rule
1512
+ const bx = Math.cos(biteAngle) * biteOffset;
1513
+ const by = Math.sin(biteAngle) * biteOffset;
1514
+ // Draw inner circle counter-clockwise for subtraction
1515
+ ctx.moveTo(bx + r * biteSize, by);
1516
+ ctx.arc(bx, by, r * biteSize, 0, Math.PI * 2, true);
1517
+ };
1518
+ const $2899cc29bfcdb86b$export$38bfe5eb52137e01 = (ctx, size, config)=>{
1519
+ const rng = config?.rng ?? Math.random;
1520
+ const r = size / 2;
1521
+ const segments = 12 + Math.floor(rng() * 8);
1522
+ const startAngle = rng() * Math.PI * 2;
1523
+ const curvature = (rng() - 0.5) * 0.4;
1524
+ // Build spine points
1525
+ const spine = [];
1526
+ let angle = startAngle;
1527
+ let px = 0, py = 0;
1528
+ for(let i = 0; i <= segments; i++){
1529
+ spine.push({
1530
+ x: px,
1531
+ y: py
1532
+ });
1533
+ const stepLen = r / segments * (1.5 + rng() * 0.5);
1534
+ angle += curvature + (rng() - 0.5) * 0.6;
1535
+ px += Math.cos(angle) * stepLen;
1536
+ py += Math.sin(angle) * stepLen;
1537
+ }
1538
+ // Build tapered outline by offsetting perpendicular to spine
1539
+ ctx.beginPath();
1540
+ const leftSide = [];
1541
+ const rightSide = [];
1542
+ for(let i = 0; i < spine.length; i++){
1543
+ const t = i / (spine.length - 1);
1544
+ const width = r * 0.12 * (1 - t * 0.9); // taper from thick to thin
1545
+ const next = spine[Math.min(i + 1, spine.length - 1)];
1546
+ const dx = next.x - spine[i].x;
1547
+ const dy = next.y - spine[i].y;
1548
+ const len = Math.hypot(dx, dy) || 1;
1549
+ const nx = -dy / len;
1550
+ const ny = dx / len;
1551
+ leftSide.push({
1552
+ x: spine[i].x + nx * width,
1553
+ y: spine[i].y + ny * width
1554
+ });
1555
+ rightSide.push({
1556
+ x: spine[i].x - nx * width,
1557
+ y: spine[i].y - ny * width
1558
+ });
1559
+ }
1560
+ ctx.moveTo(leftSide[0].x, leftSide[0].y);
1561
+ for(let i = 1; i < leftSide.length; i++)ctx.lineTo(leftSide[i].x, leftSide[i].y);
1562
+ for(let i = rightSide.length - 1; i >= 0; i--)ctx.lineTo(rightSide[i].x, rightSide[i].y);
1563
+ ctx.closePath();
1564
+ };
1565
+ const $2899cc29bfcdb86b$export$105caa8cfd63c422 = (ctx, size, config)=>{
1566
+ const rng = config?.rng ?? Math.random;
1567
+ const r = size / 2;
1568
+ const lobeCount = 4 + Math.floor(rng() * 4); // 4-7 lobes
1569
+ const spineAngle = rng() * Math.PI * 2;
1570
+ const spineLen = r * 0.6;
1571
+ ctx.beginPath();
1572
+ for(let i = 0; i < lobeCount; i++){
1573
+ const t = i / (lobeCount - 1) - 0.5; // -0.5 to 0.5
1574
+ const sx = Math.cos(spineAngle) * spineLen * t;
1575
+ const sy = Math.sin(spineAngle) * spineLen * t;
1576
+ // Offset perpendicular for cloud shape
1577
+ const perpAngle = spineAngle + Math.PI / 2;
1578
+ const perpOff = (rng() - 0.3) * r * 0.3;
1579
+ const cx = sx + Math.cos(perpAngle) * perpOff;
1580
+ const cy = sy + Math.sin(perpAngle) * perpOff;
1581
+ const lobeR = r * (0.25 + rng() * 0.2);
1582
+ ctx.moveTo(cx + lobeR, cy);
1583
+ ctx.arc(cx, cy, lobeR, 0, Math.PI * 2);
1584
+ }
1585
+ };
1586
+ const $2899cc29bfcdb86b$export$e181e5bd3c539569 = (ctx, size, config)=>{
1587
+ const rng = config?.rng ?? Math.random;
1588
+ const r = size / 2;
1589
+ const spikeCount = 8 + Math.floor(rng() * 8); // 8-15 spikes
1590
+ const points = [];
1591
+ for(let i = 0; i < spikeCount; i++){
1592
+ const angle = i / spikeCount * Math.PI * 2;
1593
+ const isSpike = i % 2 === 0;
1594
+ const dist = isSpike ? r * (0.5 + rng() * 0.5 // spikes reach 50-100% of radius
1595
+ ) : r * (0.15 + rng() * 0.2); // valleys at 15-35%
1596
+ points.push({
1597
+ x: Math.cos(angle) * dist,
1598
+ y: Math.sin(angle) * dist
1599
+ });
1600
+ }
1601
+ ctx.beginPath();
1602
+ ctx.moveTo(points[0].x, points[0].y);
1603
+ for(let i = 0; i < points.length; i++){
1604
+ const curr = points[i];
1605
+ const next = points[(i + 1) % points.length];
1606
+ const cpx = (curr.x + next.x) / 2 + (rng() - 0.5) * r * 0.15;
1607
+ const cpy = (curr.y + next.y) / 2 + (rng() - 0.5) * r * 0.15;
1608
+ ctx.quadraticCurveTo(cpx, cpy, next.x, next.y);
1609
+ }
1610
+ ctx.closePath();
1611
+ };
1612
+ const $2899cc29bfcdb86b$export$155b4780b4c6bb7b = (ctx, size, config)=>{
1613
+ const rng = config?.rng ?? Math.random;
1614
+ const r = size / 2;
1615
+ const subdivisions = 1 + Math.floor(rng() * 3); // 1-3
1616
+ // Start with icosahedron vertices projected to 2D
1617
+ const baseVerts = 6 + subdivisions * 4;
1618
+ const points = [];
1619
+ for(let i = 0; i < baseVerts; i++){
1620
+ const angle = i / baseVerts * Math.PI * 2;
1621
+ const ring = i % 2 === 0 ? 1.0 : 0.5 + rng() * 0.3;
1622
+ points.push({
1623
+ x: Math.cos(angle) * r * ring,
1624
+ y: Math.sin(angle) * r * ring
1625
+ });
1626
+ }
1627
+ ctx.beginPath();
1628
+ // Draw triangulated mesh — connect each point to neighbors and center
1629
+ for(let i = 0; i < points.length; i++){
1630
+ const next = points[(i + 1) % points.length];
1631
+ ctx.moveTo(points[i].x, points[i].y);
1632
+ ctx.lineTo(next.x, next.y);
1633
+ // Connect to center
1634
+ ctx.moveTo(points[i].x, points[i].y);
1635
+ ctx.lineTo(0, 0);
1636
+ // Cross-connect to create triangulation
1637
+ if (i % 2 === 0 && i + 2 < points.length) {
1638
+ ctx.moveTo(points[i].x, points[i].y);
1639
+ ctx.lineTo(points[i + 2].x, points[i + 2].y);
1640
+ }
1641
+ }
1642
+ // Outer ring
1643
+ ctx.moveTo(points[0].x, points[0].y);
1644
+ for(let i = 1; i < points.length; i++)ctx.lineTo(points[i].x, points[i].y);
1645
+ ctx.closePath();
1646
+ };
1647
+ const $2899cc29bfcdb86b$export$9a7e648f11155172 = (ctx, size, config)=>{
1648
+ const rng = config?.rng ?? Math.random;
1649
+ const r = size / 2;
1650
+ const phi = (1 + Math.sqrt(5)) / 2; // golden ratio
1651
+ const isKite = rng() < 0.5;
1652
+ ctx.beginPath();
1653
+ if (isKite) {
1654
+ // Kite: two golden triangles joined at base
1655
+ const topY = -r;
1656
+ const bottomY = r * (1 / phi);
1657
+ const midY = r * (1 / phi - 1) * 0.3;
1658
+ const wingX = r * 0.6;
1659
+ ctx.moveTo(0, topY);
1660
+ ctx.lineTo(wingX, midY);
1661
+ ctx.lineTo(0, bottomY);
1662
+ ctx.lineTo(-wingX, midY);
1663
+ } else {
1664
+ // Dart: concave quadrilateral
1665
+ const topY = -r;
1666
+ const bottomY = r * 0.3;
1667
+ const midY = -r * 0.1;
1668
+ const wingX = r * 0.5;
1669
+ ctx.moveTo(0, topY);
1670
+ ctx.lineTo(wingX, midY);
1671
+ ctx.lineTo(0, bottomY);
1672
+ ctx.lineTo(-wingX, midY);
1673
+ }
1674
+ ctx.closePath();
1675
+ };
1676
+ const $2899cc29bfcdb86b$export$1fc0aedbabd73399 = (ctx, size, config)=>{
1677
+ const r = size / 2;
1678
+ // Vertices of equilateral triangle
1679
+ const verts = [];
1680
+ for(let i = 0; i < 3; i++){
1681
+ const angle = i / 3 * Math.PI * 2 - Math.PI / 2;
1682
+ verts.push({
1683
+ x: Math.cos(angle) * r * 0.7,
1684
+ y: Math.sin(angle) * r * 0.7
1685
+ });
1686
+ }
1687
+ // Side length = distance between vertices
1688
+ const sideLen = Math.hypot(verts[1].x - verts[0].x, verts[1].y - verts[0].y);
1689
+ ctx.beginPath();
1690
+ for(let i = 0; i < 3; i++){
1691
+ const from = verts[(i + 1) % 3];
1692
+ const to = verts[(i + 2) % 3];
1693
+ const center = verts[i];
1694
+ const startAngle = Math.atan2(from.y - center.y, from.x - center.x);
1695
+ const endAngle = Math.atan2(to.y - center.y, to.x - center.x);
1696
+ if (i === 0) ctx.moveTo(from.x, from.y);
1697
+ ctx.arc(center.x, center.y, sideLen, startAngle, endAngle);
1698
+ }
1699
+ ctx.closePath();
1700
+ };
1701
+ const $2899cc29bfcdb86b$export$ef7b5e0c19a21fd1 = (ctx, size, config)=>{
1702
+ const rng = config?.rng ?? Math.random;
1703
+ const r = size / 2;
1704
+ const dotCount = 15 + Math.floor(rng() * 25); // 15-39 dots
1705
+ const clusterTightness = 0.3 + rng() * 0.5;
1706
+ ctx.beginPath();
1707
+ for(let i = 0; i < dotCount; i++){
1708
+ // Gaussian-ish distribution via Box-Muller approximation
1709
+ const u1 = Math.max(0.001, rng());
1710
+ const u2 = rng();
1711
+ const mag = Math.sqrt(-2 * Math.log(u1)) * clusterTightness;
1712
+ const angle = u2 * Math.PI * 2;
1713
+ const dx = Math.cos(angle) * mag * r;
1714
+ const dy = Math.sin(angle) * mag * r;
1715
+ const dotR = r * (0.02 + rng() * 0.04);
1716
+ ctx.moveTo(dx + dotR, dy);
1717
+ ctx.arc(dx, dy, dotR, 0, Math.PI * 2);
1718
+ }
1719
+ };
1720
+ const $2899cc29bfcdb86b$export$f15df8ab60dfcc9a = (ctx, size, config)=>{
1721
+ const rng = config?.rng ?? Math.random;
1722
+ const r = size / 2;
1723
+ const angle1 = rng() * Math.PI;
1724
+ const angle2 = angle1 + Math.PI / 2 + (rng() - 0.5) * 0.3;
1725
+ const spacing = r * (0.08 + rng() * 0.08);
1726
+ const hasCross = rng() < 0.6;
1727
+ // Draw bounding shape (ellipse)
1728
+ const rx = r * (0.7 + rng() * 0.3);
1729
+ const ry = r * (0.5 + rng() * 0.3);
1730
+ // Outer boundary
1731
+ ctx.beginPath();
1732
+ ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
1733
+ // Hatch lines clipped to the ellipse
1734
+ const cos1 = Math.cos(angle1);
1735
+ const sin1 = Math.sin(angle1);
1736
+ for(let d = -r; d <= r; d += spacing){
1737
+ const lx1 = d * cos1 - r * sin1;
1738
+ const ly1 = d * sin1 + r * cos1;
1739
+ const lx2 = d * cos1 + r * sin1;
1740
+ const ly2 = d * sin1 - r * cos1;
1741
+ ctx.moveTo(lx1, ly1);
1742
+ ctx.lineTo(lx2, ly2);
1743
+ }
1744
+ if (hasCross) {
1745
+ const cos2 = Math.cos(angle2);
1746
+ const sin2 = Math.sin(angle2);
1747
+ for(let d = -r; d <= r; d += spacing * 1.3){
1748
+ const lx1 = d * cos2 - r * sin2;
1749
+ const ly1 = d * sin2 + r * cos2;
1750
+ const lx2 = d * cos2 + r * sin2;
1751
+ const ly2 = d * sin2 - r * cos2;
1752
+ ctx.moveTo(lx1, ly1);
1753
+ ctx.lineTo(lx2, ly2);
1754
+ }
1755
+ }
1756
+ };
1451
1757
  const $2899cc29bfcdb86b$export$40cfb4c637f2fbb5 = {
1452
1758
  blob: $2899cc29bfcdb86b$export$580f80cfb9de73bc,
1453
1759
  ngon: $2899cc29bfcdb86b$export$7a6094023f0902a6,
@@ -1455,7 +1761,18 @@ const $2899cc29bfcdb86b$export$40cfb4c637f2fbb5 = {
1455
1761
  superellipse: $2899cc29bfcdb86b$export$1db9219b4f34658c,
1456
1762
  spirograph: $2899cc29bfcdb86b$export$b027c64d22b01985,
1457
1763
  waveRing: $2899cc29bfcdb86b$export$7608ccd03bfb705d,
1458
- rose: $2899cc29bfcdb86b$export$11a377e7498bb523
1764
+ rose: $2899cc29bfcdb86b$export$11a377e7498bb523,
1765
+ shardField: $2899cc29bfcdb86b$export$76b6526575ea179b,
1766
+ voronoiCell: $2899cc29bfcdb86b$export$ed9ff98da5b05073,
1767
+ crescent: $2899cc29bfcdb86b$export$e0452d9a794fe7e5,
1768
+ tendril: $2899cc29bfcdb86b$export$38bfe5eb52137e01,
1769
+ cloudForm: $2899cc29bfcdb86b$export$105caa8cfd63c422,
1770
+ inkSplat: $2899cc29bfcdb86b$export$e181e5bd3c539569,
1771
+ geodesicDome: $2899cc29bfcdb86b$export$155b4780b4c6bb7b,
1772
+ penroseTile: $2899cc29bfcdb86b$export$9a7e648f11155172,
1773
+ reuleauxTriangle: $2899cc29bfcdb86b$export$1fc0aedbabd73399,
1774
+ dotCluster: $2899cc29bfcdb86b$export$ef7b5e0c19a21fd1,
1775
+ crosshatchPatch: $2899cc29bfcdb86b$export$f15df8ab60dfcc9a
1459
1776
  };
1460
1777
 
1461
1778
 
@@ -1490,7 +1807,14 @@ const $e0f99502ff383dd8$var$RENDER_STYLES = [
1490
1807
  "dashed",
1491
1808
  "watercolor",
1492
1809
  "hatched",
1493
- "incomplete"
1810
+ "incomplete",
1811
+ "stipple",
1812
+ "stencil",
1813
+ "noise-grain",
1814
+ "wood-grain",
1815
+ "marble-vein",
1816
+ "fabric-weave",
1817
+ "hand-drawn"
1494
1818
  ];
1495
1819
  function $e0f99502ff383dd8$export$9fd4e64b2acd410e(rng) {
1496
1820
  return $e0f99502ff383dd8$var$RENDER_STYLES[Math.floor(rng() * $e0f99502ff383dd8$var$RENDER_STYLES.length)];
@@ -1664,6 +1988,240 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
1664
1988
  ctx.lineDashOffset = 0;
1665
1989
  break;
1666
1990
  }
1991
+ case "stipple":
1992
+ {
1993
+ // Dot-fill texture — clip to shape, then scatter dots
1994
+ const savedAlphaS = ctx.globalAlpha;
1995
+ ctx.globalAlpha = savedAlphaS * 0.15;
1996
+ ctx.fill(); // ghost fill
1997
+ ctx.globalAlpha = savedAlphaS;
1998
+ ctx.save();
1999
+ ctx.clip();
2000
+ const dotSpacing = Math.max(2, size * 0.03);
2001
+ const extent = size * 0.55;
2002
+ ctx.globalAlpha = savedAlphaS * 0.7;
2003
+ for(let dx = -extent; dx <= extent; dx += dotSpacing)for(let dy = -extent; dy <= extent; dy += dotSpacing){
2004
+ // Jitter each dot position for organic feel
2005
+ const jx = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
2006
+ const jy = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
2007
+ const dotR = rng ? dotSpacing * (0.15 + rng() * 0.2) : dotSpacing * 0.2;
2008
+ ctx.beginPath();
2009
+ ctx.arc(dx + jx, dy + jy, dotR, 0, Math.PI * 2);
2010
+ ctx.fill();
2011
+ }
2012
+ ctx.restore();
2013
+ ctx.globalAlpha = savedAlphaS;
2014
+ // Outline
2015
+ ctx.globalAlpha *= 0.4;
2016
+ ctx.stroke();
2017
+ ctx.globalAlpha /= 0.4;
2018
+ break;
2019
+ }
2020
+ case "stencil":
2021
+ {
2022
+ // Negative-space cutout — fill a rectangle, then erase the shape
2023
+ const savedAlphaSt = ctx.globalAlpha;
2024
+ // Fill a bounding area with the stroke color
2025
+ ctx.globalAlpha = savedAlphaSt * 0.5;
2026
+ ctx.fillStyle = strokeColor;
2027
+ ctx.fillRect(-size * 0.6, -size * 0.6, size * 1.2, size * 1.2);
2028
+ // Cut out the shape using destination-out
2029
+ ctx.globalCompositeOperation = "destination-out";
2030
+ ctx.globalAlpha = 1;
2031
+ ctx.fill();
2032
+ ctx.globalCompositeOperation = "source-over";
2033
+ ctx.globalAlpha = savedAlphaSt;
2034
+ // Subtle outline of the cutout
2035
+ ctx.globalAlpha *= 0.3;
2036
+ ctx.stroke();
2037
+ ctx.globalAlpha /= 0.3;
2038
+ break;
2039
+ }
2040
+ case "noise-grain":
2041
+ {
2042
+ // Procedural noise grain texture clipped to shape boundary
2043
+ const savedAlphaN = ctx.globalAlpha;
2044
+ ctx.globalAlpha = savedAlphaN * 0.25;
2045
+ ctx.fill(); // base tint
2046
+ ctx.globalAlpha = savedAlphaN;
2047
+ ctx.save();
2048
+ ctx.clip();
2049
+ const grainSpacing = Math.max(1.5, size * 0.015);
2050
+ const extentN = size * 0.55;
2051
+ ctx.globalAlpha = savedAlphaN * 0.6;
2052
+ for(let gx = -extentN; gx <= extentN; gx += grainSpacing)for(let gy = -extentN; gy <= extentN; gy += grainSpacing){
2053
+ if (!rng) break;
2054
+ const jx = (rng() - 0.5) * grainSpacing * 1.2;
2055
+ const jy = (rng() - 0.5) * grainSpacing * 1.2;
2056
+ const brightness = rng() > 0.5 ? 255 : 0;
2057
+ const dotAlpha = 0.15 + rng() * 0.35;
2058
+ ctx.globalAlpha = savedAlphaN * dotAlpha;
2059
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
2060
+ const dotSize = grainSpacing * (0.3 + rng() * 0.5);
2061
+ ctx.fillRect(gx + jx, gy + jy, dotSize, dotSize);
2062
+ }
2063
+ ctx.restore();
2064
+ ctx.fillStyle = fillColor;
2065
+ ctx.globalAlpha = savedAlphaN;
2066
+ ctx.globalAlpha *= 0.4;
2067
+ ctx.stroke();
2068
+ ctx.globalAlpha /= 0.4;
2069
+ break;
2070
+ }
2071
+ case "wood-grain":
2072
+ {
2073
+ // Parallel wavy lines simulating wood grain, clipped to shape
2074
+ const savedAlphaW = ctx.globalAlpha;
2075
+ ctx.globalAlpha = savedAlphaW * 0.2;
2076
+ ctx.fill(); // base tint
2077
+ ctx.globalAlpha = savedAlphaW;
2078
+ ctx.save();
2079
+ ctx.clip();
2080
+ const grainLineSpacing = Math.max(2, size * 0.035);
2081
+ const extentW = size * 0.55;
2082
+ const waveFreq = rng ? 3 + rng() * 5 : 5;
2083
+ const waveAmp = rng ? size * (0.01 + rng() * 0.03) : size * 0.02;
2084
+ const grainAngle = rng ? rng() * Math.PI : Math.PI * 0.25;
2085
+ ctx.lineWidth = Math.max(0.5, strokeWidth * 0.3);
2086
+ ctx.globalAlpha = savedAlphaW * 0.5;
2087
+ const cosG = Math.cos(grainAngle);
2088
+ const sinG = Math.sin(grainAngle);
2089
+ for(let d = -extentW; d <= extentW; d += grainLineSpacing){
2090
+ ctx.beginPath();
2091
+ for(let t = -extentW; t <= extentW; t += 2){
2092
+ const wave = Math.sin(t / extentW * waveFreq * Math.PI) * waveAmp;
2093
+ const px = t * cosG - (d + wave) * sinG;
2094
+ const py = t * sinG + (d + wave) * cosG;
2095
+ if (t === -extentW) ctx.moveTo(px, py);
2096
+ else ctx.lineTo(px, py);
2097
+ }
2098
+ ctx.stroke();
2099
+ }
2100
+ ctx.restore();
2101
+ ctx.globalAlpha = savedAlphaW;
2102
+ ctx.globalAlpha *= 0.35;
2103
+ ctx.stroke();
2104
+ ctx.globalAlpha /= 0.35;
2105
+ break;
2106
+ }
2107
+ case "marble-vein":
2108
+ {
2109
+ // Branching vein lines on a soft fill, clipped to shape
2110
+ const savedAlphaM = ctx.globalAlpha;
2111
+ ctx.globalAlpha = savedAlphaM * 0.35;
2112
+ ctx.fill(); // soft base
2113
+ ctx.globalAlpha = savedAlphaM;
2114
+ ctx.save();
2115
+ ctx.clip();
2116
+ const veinCount = rng ? 2 + Math.floor(rng() * 3) : 3;
2117
+ const extentM = size * 0.45;
2118
+ ctx.lineWidth = Math.max(0.5, strokeWidth * 0.5);
2119
+ ctx.globalAlpha = savedAlphaM * 0.4;
2120
+ for(let v = 0; v < veinCount; v++){
2121
+ const startX = rng ? (rng() - 0.5) * extentM * 2 : 0;
2122
+ const startY = rng ? -extentM + rng() * extentM * 0.5 : -extentM;
2123
+ let vx = startX;
2124
+ let vy = startY;
2125
+ const steps = 15 + (rng ? Math.floor(rng() * 15) : 10);
2126
+ const stepLen = size * 0.04;
2127
+ ctx.beginPath();
2128
+ ctx.moveTo(vx, vy);
2129
+ for(let s = 0; s < steps; s++){
2130
+ const drift = rng ? (rng() - 0.5) * stepLen * 1.5 : 0;
2131
+ vx += drift;
2132
+ vy += stepLen;
2133
+ ctx.lineTo(vx, vy);
2134
+ // Branch ~20% of the time
2135
+ if (rng && rng() < 0.2 && s > 2 && s < steps - 3) {
2136
+ const branchDir = rng() < 0.5 ? -1 : 1;
2137
+ let bx = vx;
2138
+ let by = vy;
2139
+ const bSteps = 3 + Math.floor(rng() * 5);
2140
+ ctx.moveTo(bx, by);
2141
+ for(let bs = 0; bs < bSteps; bs++){
2142
+ bx += branchDir * stepLen * (0.5 + rng() * 0.5);
2143
+ by += stepLen * 0.6;
2144
+ ctx.lineTo(bx, by);
2145
+ }
2146
+ ctx.moveTo(vx, vy); // return to main vein
2147
+ }
2148
+ }
2149
+ ctx.stroke();
2150
+ }
2151
+ ctx.restore();
2152
+ ctx.globalAlpha = savedAlphaM;
2153
+ ctx.globalAlpha *= 0.3;
2154
+ ctx.stroke();
2155
+ ctx.globalAlpha /= 0.3;
2156
+ break;
2157
+ }
2158
+ case "fabric-weave":
2159
+ {
2160
+ // Interlocking horizontal/vertical threads clipped to shape
2161
+ const savedAlphaF = ctx.globalAlpha;
2162
+ ctx.globalAlpha = savedAlphaF * 0.15;
2163
+ ctx.fill(); // ghost base
2164
+ ctx.globalAlpha = savedAlphaF;
2165
+ ctx.save();
2166
+ ctx.clip();
2167
+ const threadSpacing = Math.max(2, size * 0.04);
2168
+ const extentF = size * 0.55;
2169
+ ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
2170
+ ctx.globalAlpha = savedAlphaF * 0.55;
2171
+ // Horizontal threads
2172
+ for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2173
+ ctx.beginPath();
2174
+ ctx.moveTo(-extentF, y);
2175
+ ctx.lineTo(extentF, y);
2176
+ ctx.stroke();
2177
+ }
2178
+ // Vertical threads (offset by half spacing for weave effect)
2179
+ ctx.globalAlpha = savedAlphaF * 0.45;
2180
+ ctx.strokeStyle = fillColor;
2181
+ for(let x = -extentF; x <= extentF; x += threadSpacing * 2){
2182
+ ctx.beginPath();
2183
+ for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2184
+ // Over-under: draw segment, skip segment
2185
+ ctx.moveTo(x, y);
2186
+ ctx.lineTo(x, y + threadSpacing);
2187
+ }
2188
+ ctx.stroke();
2189
+ }
2190
+ ctx.strokeStyle = strokeColor;
2191
+ ctx.restore();
2192
+ ctx.globalAlpha = savedAlphaF;
2193
+ ctx.globalAlpha *= 0.3;
2194
+ ctx.stroke();
2195
+ ctx.globalAlpha /= 0.3;
2196
+ break;
2197
+ }
2198
+ case "hand-drawn":
2199
+ {
2200
+ // Wobbly hand-drawn edge treatment — fill normally, then redraw
2201
+ // the outline with perturbed control points for a sketchy feel
2202
+ const savedAlphaHD = ctx.globalAlpha;
2203
+ ctx.globalAlpha = savedAlphaHD * 0.85;
2204
+ ctx.fill();
2205
+ ctx.globalAlpha = savedAlphaHD;
2206
+ // Draw 2-3 slightly offset wobbly strokes for a sketchy look
2207
+ const wobblePasses = 2 + (rng ? Math.floor(rng() * 2) : 0);
2208
+ ctx.lineWidth = strokeWidth * 0.8;
2209
+ for(let wp = 0; wp < wobblePasses; wp++){
2210
+ ctx.globalAlpha = savedAlphaHD * (0.4 - wp * 0.1);
2211
+ ctx.save();
2212
+ // Slight random offset per pass
2213
+ const wobbleX = rng ? (rng() - 0.5) * size * 0.02 : 0;
2214
+ const wobbleY = rng ? (rng() - 0.5) * size * 0.02 : 0;
2215
+ ctx.translate(wobbleX, wobbleY);
2216
+ // Slightly different scale per pass for edge variation
2217
+ const wobbleScale = 1 + (rng ? (rng() - 0.5) * 0.03 : 0);
2218
+ ctx.scale(wobbleScale, wobbleScale);
2219
+ ctx.stroke();
2220
+ ctx.restore();
2221
+ }
2222
+ ctx.globalAlpha = savedAlphaHD;
2223
+ break;
2224
+ }
1667
2225
  case "fill-and-stroke":
1668
2226
  default:
1669
2227
  ctx.fill();
@@ -1710,6 +2268,64 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1710
2268
  });
1711
2269
  ctx.restore();
1712
2270
  }
2271
+ function $e0f99502ff383dd8$export$8bd8bbd1a8e53689(ctx, shape, x, y, config) {
2272
+ const { mirrorAxis: mirrorAxis = "horizontal", mirrorGap: mirrorGap = 0 } = config;
2273
+ // Draw the primary shape
2274
+ $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config);
2275
+ // Draw the mirrored copy
2276
+ ctx.save();
2277
+ const savedAlpha = ctx.globalAlpha;
2278
+ ctx.globalAlpha = savedAlpha * 0.7; // mirror is slightly softer
2279
+ switch(mirrorAxis){
2280
+ case "horizontal":
2281
+ // Reflect across vertical axis at shape position
2282
+ $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y + mirrorGap, {
2283
+ ...config,
2284
+ rotation: -(config.rotation || 0),
2285
+ size: config.size * 0.95
2286
+ });
2287
+ break;
2288
+ case "vertical":
2289
+ $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x + mirrorGap, y, {
2290
+ ...config,
2291
+ rotation: 180 - (config.rotation || 0),
2292
+ size: config.size * 0.95
2293
+ });
2294
+ break;
2295
+ case "diagonal":
2296
+ // Reflect across 45° axis
2297
+ $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x + mirrorGap * 0.7, y + mirrorGap * 0.7, {
2298
+ ...config,
2299
+ rotation: 90 - (config.rotation || 0),
2300
+ size: config.size * 0.9
2301
+ });
2302
+ break;
2303
+ case "radial-4":
2304
+ // Four-way radial mirror
2305
+ for(let i = 1; i < 4; i++){
2306
+ const angle = i / 4 * Math.PI * 2;
2307
+ const mx = x + Math.cos(angle) * mirrorGap;
2308
+ const my = y + Math.sin(angle) * mirrorGap;
2309
+ ctx.globalAlpha = savedAlpha * (0.7 - i * 0.1);
2310
+ $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, mx, my, {
2311
+ ...config,
2312
+ rotation: (config.rotation || 0) + i * 90,
2313
+ size: config.size * (0.95 - i * 0.05)
2314
+ });
2315
+ }
2316
+ break;
2317
+ }
2318
+ ctx.globalAlpha = savedAlpha;
2319
+ ctx.restore();
2320
+ }
2321
+ function $e0f99502ff383dd8$export$879206e23912d1a9(rng) {
2322
+ const roll = rng();
2323
+ if (roll < 0.60) return null;
2324
+ if (roll < 0.75) return "horizontal";
2325
+ if (roll < 0.87) return "vertical";
2326
+ if (roll < 0.95) return "diagonal";
2327
+ return "radial-4";
2328
+ }
1713
2329
 
1714
2330
 
1715
2331
 
@@ -1741,7 +2357,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
1741
2357
  bestStyles: [
1742
2358
  "fill-only",
1743
2359
  "watercolor",
1744
- "fill-and-stroke"
2360
+ "fill-and-stroke",
2361
+ "hand-drawn"
1745
2362
  ]
1746
2363
  },
1747
2364
  square: {
@@ -1778,7 +2395,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
1778
2395
  bestStyles: [
1779
2396
  "fill-and-stroke",
1780
2397
  "fill-only",
1781
- "watercolor"
2398
+ "watercolor",
2399
+ "hand-drawn"
1782
2400
  ]
1783
2401
  },
1784
2402
  hexagon: {
@@ -2158,7 +2776,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2158
2776
  bestStyles: [
2159
2777
  "fill-only",
2160
2778
  "watercolor",
2161
- "fill-and-stroke"
2779
+ "fill-and-stroke",
2780
+ "hand-drawn"
2162
2781
  ]
2163
2782
  },
2164
2783
  ngon: {
@@ -2212,7 +2831,8 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2212
2831
  bestStyles: [
2213
2832
  "fill-only",
2214
2833
  "watercolor",
2215
- "fill-and-stroke"
2834
+ "fill-and-stroke",
2835
+ "wood-grain"
2216
2836
  ]
2217
2837
  },
2218
2838
  spirograph: {
@@ -2268,84 +2888,355 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
2268
2888
  "fill-only",
2269
2889
  "watercolor"
2270
2890
  ]
2271
- }
2272
- };
2273
- function $8286059160ee2e04$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
2274
- const available = shapeNames.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]);
2275
- // Pick a seed shape — tier 1 shapes that are hero candidates
2276
- const heroPool = available.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s].tier === 1 && $8286059160ee2e04$export$4343b39fe47bd82c[s].heroCandidate);
2277
- const seedShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : available[Math.floor(rng() * available.length)];
2278
- const seedProfile = $8286059160ee2e04$export$4343b39fe47bd82c[seedShape];
2279
- // Primary: seed shape + its direct affinities (tier 1-2 only)
2280
- const primaryCandidates = [
2281
- seedShape,
2282
- ...seedProfile.affinities
2283
- ].filter((s)=>available.includes(s)).filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s].tier <= 2);
2284
- const primary = [
2285
- ...new Set(primaryCandidates)
2286
- ].slice(0, 5);
2287
- // Supporting: affinities of affinities, plus same-category shapes
2288
- const supportingSet = new Set();
2289
- for (const p of primary){
2290
- const profile = $8286059160ee2e04$export$4343b39fe47bd82c[p];
2291
- if (!profile) continue;
2292
- for (const aff of profile.affinities)if (available.includes(aff) && !primary.includes(aff)) supportingSet.add(aff);
2293
- }
2294
- // Add same-category tier 1-2 shapes
2295
- for (const s of available){
2296
- const p = $8286059160ee2e04$export$4343b39fe47bd82c[s];
2297
- if (p.category === seedProfile.category && p.tier <= 2 && !primary.includes(s)) supportingSet.add(s);
2298
- }
2299
- const supporting = [
2300
- ...supportingSet
2301
- ].slice(0, 6);
2302
- // Accents: tier 1 shapes from other categories for contrast
2303
- const usedCategories = new Set([
2304
- ...primary,
2305
- ...supporting
2306
- ].map((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]?.category));
2307
- const accentCandidates = available.filter((s)=>!primary.includes(s) && !supporting.includes(s) && $8286059160ee2e04$export$4343b39fe47bd82c[s].tier <= 2 && !usedCategories.has($8286059160ee2e04$export$4343b39fe47bd82c[s].category));
2308
- // Shuffle and take a few
2309
- const accents = [];
2310
- const shuffled = [
2311
- ...accentCandidates
2312
- ];
2313
- for(let i = shuffled.length - 1; i > 0; i--){
2314
- const j = Math.floor(rng() * (i + 1));
2315
- [shuffled[i], shuffled[j]] = [
2316
- shuffled[j],
2317
- shuffled[i]
2318
- ];
2319
- }
2320
- accents.push(...shuffled.slice(0, 3));
2321
- // For certain archetypes, bias the palette
2322
- if (archetypeName === "geometric-precision") // Remove blobs and organic shapes from primary
2323
- return {
2324
- primary: primary.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]?.category !== "procedural" || s === "ngon"),
2325
- supporting: supporting.filter((s)=>s !== "blob"),
2326
- accents: accents
2327
- };
2328
- if (archetypeName === "organic-flow") {
2329
- // Boost procedural/organic shapes
2330
- const organicBoost = available.filter((s)=>[
2331
- "blob",
2332
- "superellipse",
2333
- "waveRing",
2334
- "rose"
2335
- ].includes(s) && !primary.includes(s));
2336
- return {
2337
- primary: [
2338
- ...primary,
2339
- ...organicBoost.slice(0, 2)
2340
- ],
2341
- supporting: supporting,
2342
- accents: accents
2343
- };
2344
- }
2345
- return {
2346
- primary: primary,
2347
- supporting: supporting,
2348
- accents: accents
2891
+ },
2892
+ // ── New procedural shapes ─────────────────────────────────────
2893
+ shardField: {
2894
+ tier: 2,
2895
+ minSizeFraction: 0.1,
2896
+ maxSizeFraction: 0.7,
2897
+ affinities: [
2898
+ "voronoiCell",
2899
+ "diamond",
2900
+ "triangle",
2901
+ "penroseTile"
2902
+ ],
2903
+ category: "procedural",
2904
+ heroCandidate: false,
2905
+ bestStyles: [
2906
+ "fill-and-stroke",
2907
+ "stroke-only",
2908
+ "fill-only"
2909
+ ]
2910
+ },
2911
+ voronoiCell: {
2912
+ tier: 1,
2913
+ minSizeFraction: 0.08,
2914
+ maxSizeFraction: 0.9,
2915
+ affinities: [
2916
+ "shardField",
2917
+ "ngon",
2918
+ "superellipse",
2919
+ "blob"
2920
+ ],
2921
+ category: "procedural",
2922
+ heroCandidate: false,
2923
+ bestStyles: [
2924
+ "fill-and-stroke",
2925
+ "fill-only",
2926
+ "watercolor",
2927
+ "marble-vein"
2928
+ ]
2929
+ },
2930
+ crescent: {
2931
+ tier: 1,
2932
+ minSizeFraction: 0.1,
2933
+ maxSizeFraction: 1.0,
2934
+ affinities: [
2935
+ "circle",
2936
+ "blob",
2937
+ "cloudForm",
2938
+ "vesicaPiscis"
2939
+ ],
2940
+ category: "procedural",
2941
+ heroCandidate: true,
2942
+ bestStyles: [
2943
+ "fill-only",
2944
+ "watercolor",
2945
+ "fill-and-stroke"
2946
+ ]
2947
+ },
2948
+ tendril: {
2949
+ tier: 2,
2950
+ minSizeFraction: 0.1,
2951
+ maxSizeFraction: 0.8,
2952
+ affinities: [
2953
+ "blob",
2954
+ "inkSplat",
2955
+ "lissajous",
2956
+ "fibonacciSpiral"
2957
+ ],
2958
+ category: "procedural",
2959
+ heroCandidate: false,
2960
+ bestStyles: [
2961
+ "fill-only",
2962
+ "watercolor",
2963
+ "fill-and-stroke"
2964
+ ]
2965
+ },
2966
+ cloudForm: {
2967
+ tier: 1,
2968
+ minSizeFraction: 0.15,
2969
+ maxSizeFraction: 1.0,
2970
+ affinities: [
2971
+ "blob",
2972
+ "circle",
2973
+ "crescent",
2974
+ "superellipse"
2975
+ ],
2976
+ category: "procedural",
2977
+ heroCandidate: false,
2978
+ bestStyles: [
2979
+ "fill-only",
2980
+ "watercolor"
2981
+ ]
2982
+ },
2983
+ inkSplat: {
2984
+ tier: 2,
2985
+ minSizeFraction: 0.1,
2986
+ maxSizeFraction: 0.8,
2987
+ affinities: [
2988
+ "blob",
2989
+ "tendril",
2990
+ "shardField",
2991
+ "star"
2992
+ ],
2993
+ category: "procedural",
2994
+ heroCandidate: false,
2995
+ bestStyles: [
2996
+ "fill-only",
2997
+ "watercolor",
2998
+ "fill-and-stroke"
2999
+ ]
3000
+ },
3001
+ geodesicDome: {
3002
+ tier: 2,
3003
+ minSizeFraction: 0.2,
3004
+ maxSizeFraction: 0.9,
3005
+ affinities: [
3006
+ "metatronsCube",
3007
+ "platonicSolid",
3008
+ "hexagon",
3009
+ "triangle"
3010
+ ],
3011
+ category: "procedural",
3012
+ heroCandidate: true,
3013
+ bestStyles: [
3014
+ "stroke-only",
3015
+ "dashed",
3016
+ "double-stroke"
3017
+ ]
3018
+ },
3019
+ penroseTile: {
3020
+ tier: 2,
3021
+ minSizeFraction: 0.06,
3022
+ maxSizeFraction: 0.6,
3023
+ affinities: [
3024
+ "diamond",
3025
+ "triangle",
3026
+ "shardField",
3027
+ "voronoiCell"
3028
+ ],
3029
+ category: "procedural",
3030
+ heroCandidate: false,
3031
+ bestStyles: [
3032
+ "fill-and-stroke",
3033
+ "fill-only",
3034
+ "double-stroke"
3035
+ ]
3036
+ },
3037
+ reuleauxTriangle: {
3038
+ tier: 1,
3039
+ minSizeFraction: 0.08,
3040
+ maxSizeFraction: 1.0,
3041
+ affinities: [
3042
+ "triangle",
3043
+ "circle",
3044
+ "superellipse",
3045
+ "vesicaPiscis"
3046
+ ],
3047
+ category: "procedural",
3048
+ heroCandidate: true,
3049
+ bestStyles: [
3050
+ "fill-and-stroke",
3051
+ "fill-only",
3052
+ "watercolor"
3053
+ ]
3054
+ },
3055
+ dotCluster: {
3056
+ tier: 3,
3057
+ minSizeFraction: 0.05,
3058
+ maxSizeFraction: 0.5,
3059
+ affinities: [
3060
+ "cloudForm",
3061
+ "inkSplat",
3062
+ "blob"
3063
+ ],
3064
+ category: "procedural",
3065
+ heroCandidate: false,
3066
+ bestStyles: [
3067
+ "fill-only",
3068
+ "stipple"
3069
+ ]
3070
+ },
3071
+ crosshatchPatch: {
3072
+ tier: 3,
3073
+ minSizeFraction: 0.1,
3074
+ maxSizeFraction: 0.6,
3075
+ affinities: [
3076
+ "voronoiCell",
3077
+ "ngon",
3078
+ "superellipse"
3079
+ ],
3080
+ category: "procedural",
3081
+ heroCandidate: false,
3082
+ bestStyles: [
3083
+ "stroke-only",
3084
+ "hatched",
3085
+ "fabric-weave"
3086
+ ]
3087
+ }
3088
+ };
3089
+ function $8286059160ee2e04$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
3090
+ const available = shapeNames.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]);
3091
+ // Pick a seed shape — tier 1 shapes that are hero candidates
3092
+ const heroPool = available.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s].tier === 1 && $8286059160ee2e04$export$4343b39fe47bd82c[s].heroCandidate);
3093
+ const seedShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : available[Math.floor(rng() * available.length)];
3094
+ const seedProfile = $8286059160ee2e04$export$4343b39fe47bd82c[seedShape];
3095
+ // Primary: seed shape + its direct affinities (tier 1-2 only)
3096
+ const primaryCandidates = [
3097
+ seedShape,
3098
+ ...seedProfile.affinities
3099
+ ].filter((s)=>available.includes(s)).filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s].tier <= 2);
3100
+ const primary = [
3101
+ ...new Set(primaryCandidates)
3102
+ ].slice(0, 5);
3103
+ // Supporting: affinities of affinities, plus same-category shapes
3104
+ const supportingSet = new Set();
3105
+ for (const p of primary){
3106
+ const profile = $8286059160ee2e04$export$4343b39fe47bd82c[p];
3107
+ if (!profile) continue;
3108
+ for (const aff of profile.affinities)if (available.includes(aff) && !primary.includes(aff)) supportingSet.add(aff);
3109
+ }
3110
+ // Add same-category tier 1-2 shapes
3111
+ for (const s of available){
3112
+ const p = $8286059160ee2e04$export$4343b39fe47bd82c[s];
3113
+ if (p.category === seedProfile.category && p.tier <= 2 && !primary.includes(s)) supportingSet.add(s);
3114
+ }
3115
+ const supporting = [
3116
+ ...supportingSet
3117
+ ].slice(0, 6);
3118
+ // Accents: tier 1 shapes from other categories for contrast
3119
+ const usedCategories = new Set([
3120
+ ...primary,
3121
+ ...supporting
3122
+ ].map((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]?.category));
3123
+ const accentCandidates = available.filter((s)=>!primary.includes(s) && !supporting.includes(s) && $8286059160ee2e04$export$4343b39fe47bd82c[s].tier <= 2 && !usedCategories.has($8286059160ee2e04$export$4343b39fe47bd82c[s].category));
3124
+ // Shuffle and take a few
3125
+ const accents = [];
3126
+ const shuffled = [
3127
+ ...accentCandidates
3128
+ ];
3129
+ for(let i = shuffled.length - 1; i > 0; i--){
3130
+ const j = Math.floor(rng() * (i + 1));
3131
+ [shuffled[i], shuffled[j]] = [
3132
+ shuffled[j],
3133
+ shuffled[i]
3134
+ ];
3135
+ }
3136
+ accents.push(...shuffled.slice(0, 3));
3137
+ // For certain archetypes, bias the palette
3138
+ if (archetypeName === "geometric-precision") // Remove blobs and organic shapes from primary
3139
+ return {
3140
+ primary: primary.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]?.category !== "procedural" || s === "ngon"),
3141
+ supporting: supporting.filter((s)=>s !== "blob"),
3142
+ accents: accents
3143
+ };
3144
+ if (archetypeName === "organic-flow") {
3145
+ // Boost procedural/organic shapes
3146
+ const organicBoost = available.filter((s)=>[
3147
+ "blob",
3148
+ "superellipse",
3149
+ "waveRing",
3150
+ "rose"
3151
+ ].includes(s) && !primary.includes(s));
3152
+ return {
3153
+ primary: [
3154
+ ...primary,
3155
+ ...organicBoost.slice(0, 2)
3156
+ ],
3157
+ supporting: supporting,
3158
+ accents: accents
3159
+ };
3160
+ }
3161
+ if (archetypeName === "shattered-glass") {
3162
+ // Favor angular, fragmented shapes
3163
+ const shardBoost = available.filter((s)=>[
3164
+ "shardField",
3165
+ "voronoiCell",
3166
+ "penroseTile",
3167
+ "diamond",
3168
+ "triangle",
3169
+ "ngon"
3170
+ ].includes(s) && !primary.includes(s));
3171
+ return {
3172
+ primary: [
3173
+ ...primary.filter((s)=>s !== "blob" && s !== "cloudForm"),
3174
+ ...shardBoost.slice(0, 3)
3175
+ ],
3176
+ supporting: supporting.filter((s)=>s !== "blob" && s !== "cloudForm"),
3177
+ accents: accents
3178
+ };
3179
+ }
3180
+ if (archetypeName === "botanical") {
3181
+ // Favor organic, flowing shapes
3182
+ const botanicalBoost = available.filter((s)=>[
3183
+ "tendril",
3184
+ "cloudForm",
3185
+ "blob",
3186
+ "crescent",
3187
+ "rose",
3188
+ "inkSplat"
3189
+ ].includes(s) && !primary.includes(s));
3190
+ return {
3191
+ primary: [
3192
+ ...primary,
3193
+ ...botanicalBoost.slice(0, 3)
3194
+ ],
3195
+ supporting: supporting,
3196
+ accents: accents
3197
+ };
3198
+ }
3199
+ if (archetypeName === "stipple-portrait") {
3200
+ // Favor small, dot-friendly shapes
3201
+ const stippleBoost = available.filter((s)=>[
3202
+ "dotCluster",
3203
+ "circle",
3204
+ "crosshatchPatch",
3205
+ "voronoiCell",
3206
+ "blob"
3207
+ ].includes(s) && !primary.includes(s));
3208
+ return {
3209
+ primary: [
3210
+ ...primary,
3211
+ ...stippleBoost.slice(0, 3)
3212
+ ],
3213
+ supporting: supporting,
3214
+ accents: accents
3215
+ };
3216
+ }
3217
+ if (archetypeName === "celestial") {
3218
+ // Favor sacred geometry and cosmic shapes
3219
+ const celestialBoost = available.filter((s)=>[
3220
+ "crescent",
3221
+ "geodesicDome",
3222
+ "mandala",
3223
+ "flowerOfLife",
3224
+ "spirograph",
3225
+ "fibonacciSpiral"
3226
+ ].includes(s) && !primary.includes(s));
3227
+ return {
3228
+ primary: [
3229
+ ...primary,
3230
+ ...celestialBoost.slice(0, 3)
3231
+ ],
3232
+ supporting: supporting,
3233
+ accents: accents
3234
+ };
3235
+ }
3236
+ return {
3237
+ primary: primary,
3238
+ supporting: supporting,
3239
+ accents: accents
2349
3240
  };
2350
3241
  }
2351
3242
  function $8286059160ee2e04$export$3c37d9a045754d0e(palette, rng, sizeFraction) {
@@ -2679,10 +3570,138 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
2679
3570
  glowMultiplier: 1,
2680
3571
  sizePower: 1.8,
2681
3572
  invertForeground: false
3573
+ },
3574
+ {
3575
+ name: "shattered-glass",
3576
+ gridSize: 8,
3577
+ layers: 3,
3578
+ baseOpacity: 0.85,
3579
+ opacityReduction: 0.1,
3580
+ minShapeSize: 15,
3581
+ maxShapeSize: 250,
3582
+ backgroundStyle: "solid-dark",
3583
+ paletteMode: "high-contrast",
3584
+ preferredStyles: [
3585
+ "fill-and-stroke",
3586
+ "stroke-only",
3587
+ "fill-only"
3588
+ ],
3589
+ flowLineMultiplier: 0,
3590
+ heroShape: false,
3591
+ glowMultiplier: 0.3,
3592
+ sizePower: 1.0,
3593
+ invertForeground: false
3594
+ },
3595
+ {
3596
+ name: "botanical",
3597
+ gridSize: 4,
3598
+ layers: 4,
3599
+ baseOpacity: 0.5,
3600
+ opacityReduction: 0.06,
3601
+ minShapeSize: 30,
3602
+ maxShapeSize: 400,
3603
+ backgroundStyle: "radial-light",
3604
+ paletteMode: "earth",
3605
+ preferredStyles: [
3606
+ "watercolor",
3607
+ "fill-only",
3608
+ "incomplete"
3609
+ ],
3610
+ flowLineMultiplier: 3,
3611
+ heroShape: true,
3612
+ glowMultiplier: 0.2,
3613
+ sizePower: 1.6,
3614
+ invertForeground: false
3615
+ },
3616
+ {
3617
+ name: "stipple-portrait",
3618
+ gridSize: 9,
3619
+ layers: 2,
3620
+ baseOpacity: 0.8,
3621
+ opacityReduction: 0.05,
3622
+ minShapeSize: 5,
3623
+ maxShapeSize: 120,
3624
+ backgroundStyle: "solid-light",
3625
+ paletteMode: "monochrome",
3626
+ preferredStyles: [
3627
+ "stipple",
3628
+ "fill-only",
3629
+ "hatched"
3630
+ ],
3631
+ flowLineMultiplier: 0,
3632
+ heroShape: false,
3633
+ glowMultiplier: 0,
3634
+ sizePower: 2.8,
3635
+ invertForeground: false
3636
+ },
3637
+ {
3638
+ name: "celestial",
3639
+ gridSize: 7,
3640
+ layers: 5,
3641
+ baseOpacity: 0.45,
3642
+ opacityReduction: 0.04,
3643
+ minShapeSize: 8,
3644
+ maxShapeSize: 450,
3645
+ backgroundStyle: "radial-dark",
3646
+ paletteMode: "neon",
3647
+ preferredStyles: [
3648
+ "fill-only",
3649
+ "watercolor",
3650
+ "stroke-only",
3651
+ "incomplete"
3652
+ ],
3653
+ flowLineMultiplier: 2,
3654
+ heroShape: true,
3655
+ glowMultiplier: 2.5,
3656
+ sizePower: 2.2,
3657
+ invertForeground: false
2682
3658
  }
2683
3659
  ];
3660
+ /**
3661
+ * Linearly interpolate between two archetype numeric parameters.
3662
+ */ function $68a238ccd77f2bcd$var$lerpNum(a, b, t) {
3663
+ return a + (b - a) * t;
3664
+ }
3665
+ /**
3666
+ * Blend two archetypes by interpolating their numeric parameters
3667
+ * and merging their style arrays.
3668
+ */ function $68a238ccd77f2bcd$var$blendArchetypes(a, b, t) {
3669
+ // Merge preferred styles — unique union, primary archetype first
3670
+ const mergedStyles = [
3671
+ ...new Set([
3672
+ ...a.preferredStyles,
3673
+ ...b.preferredStyles
3674
+ ])
3675
+ ];
3676
+ return {
3677
+ name: `${a.name}+${b.name}`,
3678
+ gridSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.gridSize, b.gridSize, t)),
3679
+ layers: Math.round($68a238ccd77f2bcd$var$lerpNum(a.layers, b.layers, t)),
3680
+ baseOpacity: $68a238ccd77f2bcd$var$lerpNum(a.baseOpacity, b.baseOpacity, t),
3681
+ opacityReduction: $68a238ccd77f2bcd$var$lerpNum(a.opacityReduction, b.opacityReduction, t),
3682
+ minShapeSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.minShapeSize, b.minShapeSize, t)),
3683
+ maxShapeSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.maxShapeSize, b.maxShapeSize, t)),
3684
+ backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3685
+ paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3686
+ preferredStyles: mergedStyles,
3687
+ flowLineMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3688
+ heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3689
+ glowMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
3690
+ sizePower: $68a238ccd77f2bcd$var$lerpNum(a.sizePower, b.sizePower, t),
3691
+ invertForeground: t < 0.5 ? a.invertForeground : b.invertForeground
3692
+ };
3693
+ }
2684
3694
  function $68a238ccd77f2bcd$export$f1142fd7da4d6590(rng) {
2685
- return $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3695
+ const primary = $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3696
+ // ~15% chance of blending with a second archetype
3697
+ if (rng() < 0.15) {
3698
+ const secondary = $68a238ccd77f2bcd$var$ARCHETYPES[Math.floor(rng() * $68a238ccd77f2bcd$var$ARCHETYPES.length)];
3699
+ if (secondary.name !== primary.name) {
3700
+ const blendT = 0.25 + rng() * 0.25; // 25-50% blend toward secondary
3701
+ return $68a238ccd77f2bcd$var$blendArchetypes(primary, secondary, blendT);
3702
+ }
3703
+ }
3704
+ return primary;
2686
3705
  }
2687
3706
 
2688
3707
 
@@ -2858,6 +3877,135 @@ function $1f63dc64b5593c73$var$drawBackground(ctx, style, bgStart, bgEnd, width,
2858
3877
  }
2859
3878
  }
2860
3879
  }
3880
+ const $1f63dc64b5593c73$var$CONSTELLATIONS = [
3881
+ {
3882
+ name: "flanked-triangle",
3883
+ build: (rng, baseSize)=>{
3884
+ const gap = baseSize * (0.6 + rng() * 0.3);
3885
+ return [
3886
+ {
3887
+ dx: 0,
3888
+ dy: 0,
3889
+ shape: "triangle",
3890
+ size: baseSize,
3891
+ rotation: rng() * 360
3892
+ },
3893
+ {
3894
+ dx: -gap,
3895
+ dy: gap * 0.3,
3896
+ shape: "circle",
3897
+ size: baseSize * 0.35,
3898
+ rotation: 0
3899
+ },
3900
+ {
3901
+ dx: gap,
3902
+ dy: gap * 0.3,
3903
+ shape: "circle",
3904
+ size: baseSize * 0.35,
3905
+ rotation: 0
3906
+ }
3907
+ ];
3908
+ }
3909
+ },
3910
+ {
3911
+ name: "hexagon-ring",
3912
+ build: (rng, baseSize)=>{
3913
+ const members = [];
3914
+ const count = 5 + Math.floor(rng() * 2);
3915
+ const ringR = baseSize * 0.6;
3916
+ for(let i = 0; i < count; i++){
3917
+ const angle = i / count * Math.PI * 2;
3918
+ members.push({
3919
+ dx: Math.cos(angle) * ringR,
3920
+ dy: Math.sin(angle) * ringR,
3921
+ shape: "hexagon",
3922
+ size: baseSize * (0.25 + rng() * 0.1),
3923
+ rotation: angle * 180 / Math.PI
3924
+ });
3925
+ }
3926
+ return members;
3927
+ }
3928
+ },
3929
+ {
3930
+ name: "spiral-dots",
3931
+ build: (rng, baseSize)=>{
3932
+ const members = [];
3933
+ const count = 7 + Math.floor(rng() * 5);
3934
+ const turns = 1.5 + rng();
3935
+ for(let i = 0; i < count; i++){
3936
+ const t = i / count;
3937
+ const angle = t * Math.PI * 2 * turns;
3938
+ const r = t * baseSize * 0.7;
3939
+ members.push({
3940
+ dx: Math.cos(angle) * r,
3941
+ dy: Math.sin(angle) * r,
3942
+ shape: "circle",
3943
+ size: baseSize * (0.08 + (1 - t) * 0.12),
3944
+ rotation: 0
3945
+ });
3946
+ }
3947
+ return members;
3948
+ }
3949
+ },
3950
+ {
3951
+ name: "diamond-cluster",
3952
+ build: (rng, baseSize)=>{
3953
+ const gap = baseSize * 0.45;
3954
+ return [
3955
+ {
3956
+ dx: 0,
3957
+ dy: -gap,
3958
+ shape: "diamond",
3959
+ size: baseSize * 0.4,
3960
+ rotation: 0
3961
+ },
3962
+ {
3963
+ dx: gap,
3964
+ dy: 0,
3965
+ shape: "diamond",
3966
+ size: baseSize * 0.35,
3967
+ rotation: 15
3968
+ },
3969
+ {
3970
+ dx: 0,
3971
+ dy: gap,
3972
+ shape: "diamond",
3973
+ size: baseSize * 0.3,
3974
+ rotation: 30
3975
+ },
3976
+ {
3977
+ dx: -gap,
3978
+ dy: 0,
3979
+ shape: "diamond",
3980
+ size: baseSize * 0.35,
3981
+ rotation: -15
3982
+ }
3983
+ ];
3984
+ }
3985
+ },
3986
+ {
3987
+ name: "crescent-pair",
3988
+ build: (rng, baseSize)=>{
3989
+ const gap = baseSize * 0.5;
3990
+ return [
3991
+ {
3992
+ dx: -gap * 0.4,
3993
+ dy: 0,
3994
+ shape: "crescent",
3995
+ size: baseSize * 0.5,
3996
+ rotation: rng() * 30
3997
+ },
3998
+ {
3999
+ dx: gap * 0.4,
4000
+ dy: 0,
4001
+ shape: "crescent",
4002
+ size: baseSize * 0.45,
4003
+ rotation: 180 + rng() * 30
4004
+ }
4005
+ ];
4006
+ }
4007
+ }
4008
+ ];
2861
4009
  function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2862
4010
  const finalConfig = {
2863
4011
  ...(0, $81c1b644006d48ec$export$c2f8e0cc249a8d8f),
@@ -2889,6 +4037,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2889
4037
  const colorGrade = (0, $b5a262d09b87e373$export$6d1620b367f86f7a)(rng);
2890
4038
  // ── 0e. Light direction — consistent shadow angle ──────────────
2891
4039
  const lightAngle = rng() * Math.PI * 2;
4040
+ // ── 0f. Palette evolution — hue drift direction across layers ──
4041
+ const paletteHueShift = (rng() - 0.5) * 40; // -20° to +20° total drift
2892
4042
  const scaleFactor = Math.min(width, height) / 1024;
2893
4043
  const adjustedMinSize = minShapeSize * scaleFactor;
2894
4044
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -2944,6 +4094,65 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
2944
4094
  ctx.stroke();
2945
4095
  }
2946
4096
  ctx.globalCompositeOperation = "source-over";
4097
+ // ── 1c. Background pattern layer — subtle textured paper ───────
4098
+ const bgPatternRoll = rng();
4099
+ if (bgPatternRoll < 0.6) {
4100
+ ctx.save();
4101
+ ctx.globalCompositeOperation = "soft-light";
4102
+ const patternOpacity = 0.02 + rng() * 0.04;
4103
+ const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4104
+ if (bgPatternRoll < 0.2) {
4105
+ // Dot grid
4106
+ const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4107
+ const dotR = dotSpacing * 0.08;
4108
+ ctx.globalAlpha = patternOpacity;
4109
+ ctx.fillStyle = patternColor;
4110
+ for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4111
+ ctx.beginPath();
4112
+ ctx.arc(px, py, dotR, 0, Math.PI * 2);
4113
+ ctx.fill();
4114
+ }
4115
+ } else if (bgPatternRoll < 0.4) {
4116
+ // Diagonal lines
4117
+ const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4118
+ ctx.globalAlpha = patternOpacity;
4119
+ ctx.strokeStyle = patternColor;
4120
+ ctx.lineWidth = 0.5 * scaleFactor;
4121
+ const diag = Math.hypot(width, height);
4122
+ for(let d = -diag; d < diag; d += lineSpacing){
4123
+ ctx.beginPath();
4124
+ ctx.moveTo(d, 0);
4125
+ ctx.lineTo(d + height, height);
4126
+ ctx.stroke();
4127
+ }
4128
+ } else {
4129
+ // Tessellation — hexagonal grid of tiny shapes
4130
+ const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4131
+ const tessH = tessSize * Math.sqrt(3);
4132
+ ctx.globalAlpha = patternOpacity * 0.7;
4133
+ ctx.strokeStyle = patternColor;
4134
+ ctx.lineWidth = 0.4 * scaleFactor;
4135
+ for(let row = 0; row * tessH < height + tessH; row++){
4136
+ const offsetX = row % 2 * tessSize * 0.75;
4137
+ for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4138
+ const hx = col * tessSize * 1.5 + offsetX;
4139
+ const hy = row * tessH;
4140
+ ctx.beginPath();
4141
+ for(let s = 0; s < 6; s++){
4142
+ const angle = Math.PI / 3 * s - Math.PI / 6;
4143
+ const vx = hx + Math.cos(angle) * tessSize * 0.5;
4144
+ const vy = hy + Math.sin(angle) * tessSize * 0.5;
4145
+ if (s === 0) ctx.moveTo(vx, vy);
4146
+ else ctx.lineTo(vx, vy);
4147
+ }
4148
+ ctx.closePath();
4149
+ ctx.stroke();
4150
+ }
4151
+ }
4152
+ }
4153
+ ctx.restore();
4154
+ }
4155
+ ctx.globalCompositeOperation = "source-over";
2947
4156
  // ── 2. Composition mode ────────────────────────────────────────
2948
4157
  const compositionMode = $1f63dc64b5593c73$var$COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$COMPOSITION_MODES.length)];
2949
4158
  const symRoll = rng();
@@ -3004,6 +4213,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3004
4213
  ry + (nearest.y - ry) * pull
3005
4214
  ];
3006
4215
  }
4216
+ // ── 3b. Void zone decoration — intentional negative space ────
4217
+ for (const zone of voidZones){
4218
+ // Subtle halo ring around void zones
4219
+ ctx.globalAlpha = 0.04 + rng() * 0.04;
4220
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
4221
+ ctx.lineWidth = 1.5 * scaleFactor;
4222
+ ctx.beginPath();
4223
+ ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
4224
+ ctx.stroke();
4225
+ // ~50% chance: scatter tiny dots inside the void
4226
+ if (rng() < 0.5) {
4227
+ const dotCount = 3 + Math.floor(rng() * 6);
4228
+ ctx.globalAlpha = 0.06 + rng() * 0.04;
4229
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
4230
+ for(let d = 0; d < dotCount; d++){
4231
+ const angle = rng() * Math.PI * 2;
4232
+ const dist = rng() * zone.radius * 0.7;
4233
+ const dotR = (1 + rng() * 3) * scaleFactor;
4234
+ ctx.beginPath();
4235
+ ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
4236
+ ctx.fill();
4237
+ }
4238
+ }
4239
+ // ~30% chance: thin concentric ring inside
4240
+ if (rng() < 0.3) {
4241
+ ctx.globalAlpha = 0.03 + rng() * 0.03;
4242
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4243
+ ctx.lineWidth = 0.5 * scaleFactor;
4244
+ const innerR = zone.radius * (0.4 + rng() * 0.3);
4245
+ ctx.beginPath();
4246
+ ctx.arc(zone.x, zone.y, innerR, 0, Math.PI * 2);
4247
+ ctx.stroke();
4248
+ }
4249
+ }
4250
+ ctx.globalAlpha = 1;
3007
4251
  // ── 4. Flow field seed values ──────────────────────────────────
3008
4252
  const fieldAngleBase = rng() * Math.PI * 2;
3009
4253
  const fieldFreq = 0.5 + rng() * 2;
@@ -3071,6 +4315,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3071
4315
  const layerRenderStyle = rng() < 0.6 ? archetype.preferredStyles[Math.floor(rng() * archetype.preferredStyles.length)] : (0, $e0f99502ff383dd8$export$9fd4e64b2acd410e)(rng);
3072
4316
  // Atmospheric desaturation for later layers
3073
4317
  const atmosphericDesat = layerRatio * 0.3;
4318
+ // Depth-of-field simulation — later layers are "further away"
4319
+ // Reduce stroke widths and shift colors toward the background
4320
+ const dofFactor = 1 - layerRatio * 0.5; // 1.0 for front layer, 0.5 for back
4321
+ const dofStrokeScale = 0.4 + dofFactor * 0.6; // strokes thin out with depth
4322
+ const dofContrastReduction = layerRatio * 0.2; // colors fade toward bg
4323
+ // Color palette evolution — hue-rotate the hierarchy per layer
4324
+ const layerHierarchy = (0, $b5a262d09b87e373$export$703ba40a4347f77a)(colorHierarchy, layerRatio, paletteHueShift);
4325
+ // Focal depth: shapes near focal points get more detail
4326
+ const focalDetailBoost = (px, py)=>{
4327
+ let minFocalDist = Infinity;
4328
+ for (const fp of focalPoints){
4329
+ const d = Math.hypot(px - fp.x, py - fp.y);
4330
+ if (d < minFocalDist) minFocalDist = d;
4331
+ }
4332
+ const maxDist = Math.hypot(width, height) * 0.5;
4333
+ return Math.max(0, 1 - minFocalDist / maxDist); // 1.0 at focal, 0.0 at edges
4334
+ };
3074
4335
  for(let i = 0; i < numShapes; i++){
3075
4336
  // Position from composition mode, then focal bias
3076
4337
  const rawPos = $1f63dc64b5593c73$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
@@ -3101,9 +4362,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3101
4362
  rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
3102
4363
  }
3103
4364
  }
3104
- // Positional color from hierarchy + jitter
3105
- let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
3106
- const strokeBase = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4365
+ // Positional color from hierarchy + jitter (using evolved layer palette)
4366
+ let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, layerHierarchy, rng);
4367
+ const strokeBase = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng);
3107
4368
  // Desaturate colors on later layers for depth
3108
4369
  if (atmosphericDesat > 0) fillBase = (0, $b5a262d09b87e373$export$fb75607d98509d9)(fillBase, atmosphericDesat);
3109
4370
  // Temperature contrast: shift foreground shapes opposite to background
@@ -3113,8 +4374,10 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3113
4374
  // Semi-transparent fill
3114
4375
  const fillAlpha = 0.2 + rng() * 0.5;
3115
4376
  const transparentFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, fillAlpha);
3116
- const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor;
3117
- ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5);
4377
+ const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor * dofStrokeScale;
4378
+ // Depth-of-field: reduce opacity slightly for distant layers
4379
+ const dofOpacityScale = 1 - dofContrastReduction;
4380
+ ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5) * dofOpacityScale;
3118
4381
  // Glow on sacred shapes more often — scaled by archetype
3119
4382
  const isSacred = $1f63dc64b5593c73$var$SACRED_SHAPES.includes(shape);
3120
4383
  const baseGlowChance = isSacred ? 0.45 : 0.2;
@@ -3133,7 +4396,48 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3133
4396
  const shadowDist = hasGlow ? 0 : size * 0.02;
3134
4397
  const shadowOffX = shadowDist * Math.cos(lightAngle);
3135
4398
  const shadowOffY = shadowDist * Math.sin(lightAngle);
3136
- (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
4399
+ // ── 5a. Tangent placement — nudge toward nearest shape edge ──
4400
+ let finalX = x;
4401
+ let finalY = y;
4402
+ if (shapePositions.length > 0 && rng() < 0.25) {
4403
+ // Find nearest placed shape
4404
+ let nearestDist = Infinity;
4405
+ let nearestPos = null;
4406
+ for (const sp of shapePositions){
4407
+ const d = Math.hypot(x - sp.x, y - sp.y);
4408
+ if (d < nearestDist && d > 0) {
4409
+ nearestDist = d;
4410
+ nearestPos = sp;
4411
+ }
4412
+ }
4413
+ if (nearestPos) {
4414
+ // Target distance: edges kissing (sum of half-sizes)
4415
+ const targetDist = (size + nearestPos.size) * 0.5;
4416
+ if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
4417
+ const angle = Math.atan2(y - nearestPos.y, x - nearestPos.x);
4418
+ finalX = nearestPos.x + Math.cos(angle) * targetDist;
4419
+ finalY = nearestPos.y + Math.sin(angle) * targetDist;
4420
+ // Keep in bounds
4421
+ finalX = Math.max(0, Math.min(width, finalX));
4422
+ finalY = Math.max(0, Math.min(height, finalY));
4423
+ }
4424
+ }
4425
+ }
4426
+ // ── 5b. Shape mirroring — basic shapes get reflected copies ──
4427
+ const mirrorAxis = (0, $e0f99502ff383dd8$export$879206e23912d1a9)(rng);
4428
+ const isBasicShape = [
4429
+ "circle",
4430
+ "triangle",
4431
+ "square",
4432
+ "hexagon",
4433
+ "star",
4434
+ "diamond",
4435
+ "crescent",
4436
+ "penroseTile",
4437
+ "reuleauxTriangle"
4438
+ ].includes(shape);
4439
+ const shouldMirror = mirrorAxis !== null && isBasicShape && size > adjustedMaxSize * 0.2;
4440
+ const shapeConfig = {
3137
4441
  fillColor: transparentFill,
3138
4442
  strokeColor: strokeColor,
3139
4443
  strokeWidth: strokeWidth,
@@ -3145,22 +4449,47 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3145
4449
  gradientFillEnd: gradientEnd,
3146
4450
  renderStyle: finalRenderStyle,
3147
4451
  rng: rng
4452
+ };
4453
+ if (shouldMirror) (0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4454
+ ...shapeConfig,
4455
+ mirrorAxis: mirrorAxis,
4456
+ mirrorGap: size * (0.1 + rng() * 0.3)
3148
4457
  });
4458
+ else (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
4459
+ // ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
4460
+ if (rng() < 0.2 && size > adjustedMinSize * 2) {
4461
+ const glazePasses = 2 + Math.floor(rng() * 2);
4462
+ for(let g = 0; g < glazePasses; g++){
4463
+ const glazeScale = 1 - (g + 1) * 0.12; // progressively smaller
4464
+ const glazeAlpha = 0.08 + g * 0.04; // progressively more opaque toward center
4465
+ ctx.globalAlpha = glazeAlpha;
4466
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
4467
+ fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
4468
+ strokeColor: "rgba(0,0,0,0)",
4469
+ strokeWidth: 0,
4470
+ size: size * glazeScale,
4471
+ rotation: rotation,
4472
+ proportionType: "GOLDEN_RATIO",
4473
+ renderStyle: "fill-only",
4474
+ rng: rng
4475
+ });
4476
+ }
4477
+ }
3149
4478
  shapePositions.push({
3150
- x: x,
3151
- y: y,
4479
+ x: finalX,
4480
+ y: finalY,
3152
4481
  size: size,
3153
4482
  shape: shape
3154
4483
  });
3155
- // ── 5b. Size echo — large shapes spawn trailing smaller copies ──
4484
+ // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
3156
4485
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
3157
4486
  const echoCount = 2 + Math.floor(rng() * 2);
3158
4487
  const echoAngle = rng() * Math.PI * 2;
3159
4488
  for(let e = 0; e < echoCount; e++){
3160
4489
  const echoScale = 0.3 - e * 0.08;
3161
4490
  const echoDist = size * (0.6 + e * 0.4);
3162
- const echoX = x + Math.cos(echoAngle) * echoDist;
3163
- const echoY = y + Math.sin(echoAngle) * echoDist;
4491
+ const echoX = finalX + Math.cos(echoAngle) * echoDist;
4492
+ const echoY = finalY + Math.sin(echoAngle) * echoDist;
3164
4493
  const echoSize = size * Math.max(0.1, echoScale);
3165
4494
  if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
3166
4495
  ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
@@ -3182,8 +4511,11 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3182
4511
  });
3183
4512
  }
3184
4513
  }
3185
- // ── 5c. Recursive nesting ──────────────────────────────────
3186
- if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
4514
+ // ── 5d. Recursive nesting ──────────────────────────────────
4515
+ // Focal depth: shapes near focal points get more detail
4516
+ const focalProximity = focalDetailBoost(finalX, finalY);
4517
+ const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
4518
+ if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
3187
4519
  const innerCount = 1 + Math.floor(rng() * 3);
3188
4520
  for(let n = 0; n < innerCount; n++){
3189
4521
  // Pick inner shape from palette affinities
@@ -3195,7 +4527,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3195
4527
  const innerRot = rng() * 360;
3196
4528
  const innerFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
3197
4529
  ctx.globalAlpha = layerOpacity * 0.7;
3198
- (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, innerShape, x + innerOffX, y + innerOffY, {
4530
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
3199
4531
  fillColor: innerFill,
3200
4532
  strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
3201
4533
  strokeWidth: strokeWidth * 0.6,
@@ -3207,6 +4539,42 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3207
4539
  });
3208
4540
  }
3209
4541
  }
4542
+ // ── 5e. Shape constellations — pre-composed groups ─────────
4543
+ const constellationChance = 0.12 + focalProximity * 0.1; // 12-22% near focal
4544
+ if (size > adjustedMaxSize * 0.35 && rng() < constellationChance) {
4545
+ const constellation = $1f63dc64b5593c73$var$CONSTELLATIONS[Math.floor(rng() * $1f63dc64b5593c73$var$CONSTELLATIONS.length)];
4546
+ const members = constellation.build(rng, size);
4547
+ const groupRotation = rng() * Math.PI * 2;
4548
+ const cosR = Math.cos(groupRotation);
4549
+ const sinR = Math.sin(groupRotation);
4550
+ for (const member of members){
4551
+ // Rotate the group offset by the group rotation
4552
+ const mx = finalX + member.dx * cosR - member.dy * sinR;
4553
+ const my = finalY + member.dx * sinR + member.dy * cosR;
4554
+ if (mx < 0 || mx > width || my < 0 || my > height) continue;
4555
+ const memberFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
4556
+ const memberStroke = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
4557
+ ctx.globalAlpha = layerOpacity * 0.6;
4558
+ // Use the member's shape if available, otherwise fall back to palette
4559
+ const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
4560
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
4561
+ fillColor: memberFill,
4562
+ strokeColor: memberStroke,
4563
+ strokeWidth: strokeWidth * 0.7,
4564
+ size: member.size,
4565
+ rotation: member.rotation + groupRotation * 180 / Math.PI,
4566
+ proportionType: "GOLDEN_RATIO",
4567
+ renderStyle: (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng),
4568
+ rng: rng
4569
+ });
4570
+ shapePositions.push({
4571
+ x: mx,
4572
+ y: my,
4573
+ size: member.size,
4574
+ shape: memberShape
4575
+ });
4576
+ }
4577
+ }
3210
4578
  }
3211
4579
  }
3212
4580
  // Reset blend mode for post-processing passes
@@ -3277,7 +4645,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3277
4645
  prevY = fy;
3278
4646
  }
3279
4647
  }
3280
- // ── 6b. Apply symmetry mirroring ─────────────────────────────────
4648
+ // ── 6b. Motion/energy lines short directional bursts ─────────
4649
+ const energyArchetypes = [
4650
+ "dense-chaotic",
4651
+ "cosmic",
4652
+ "neon-glow",
4653
+ "bold-graphic"
4654
+ ];
4655
+ const hasEnergyLines = energyArchetypes.some((a)=>archetype.name.includes(a)) || rng() < 0.25;
4656
+ if (hasEnergyLines && shapePositions.length > 0) {
4657
+ const energyCount = 5 + Math.floor(rng() * 10);
4658
+ ctx.lineCap = "round";
4659
+ for(let e = 0; e < energyCount; e++){
4660
+ // Pick a random shape to radiate from
4661
+ const source = shapePositions[Math.floor(rng() * shapePositions.length)];
4662
+ const burstCount = 2 + Math.floor(rng() * 4);
4663
+ const baseAngle = flowAngle(source.x, source.y);
4664
+ for(let b = 0; b < burstCount; b++){
4665
+ const angle = baseAngle + (rng() - 0.5) * 1.2;
4666
+ const lineLen = (source.size * 0.3 + rng() * source.size * 0.5) * scaleFactor * 0.3;
4667
+ const startDist = source.size * 0.5;
4668
+ const sx = source.x + Math.cos(angle) * startDist;
4669
+ const sy = source.y + Math.sin(angle) * startDist;
4670
+ const ex = sx + Math.cos(angle) * lineLen;
4671
+ const ey = sy + Math.sin(angle) * lineLen;
4672
+ ctx.globalAlpha = 0.04 + rng() * 0.06;
4673
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
4674
+ ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
4675
+ ctx.beginPath();
4676
+ ctx.moveTo(sx, sy);
4677
+ ctx.lineTo(ex, ey);
4678
+ ctx.stroke();
4679
+ }
4680
+ }
4681
+ }
4682
+ // ── 6c. Apply symmetry mirroring ─────────────────────────────────
3281
4683
  if (symmetryMode !== "none") {
3282
4684
  const canvas = ctx.canvas;
3283
4685
  ctx.save();
@@ -3388,6 +4790,44 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
3388
4790
  ctx.restore();
3389
4791
  ctx.globalCompositeOperation = "source-over";
3390
4792
  }
4793
+ // ── 11. Signature mark — unique geometric chop from hash prefix ──
4794
+ {
4795
+ const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
4796
+ const sigSize = Math.min(width, height) * 0.025;
4797
+ // Bottom-right corner with padding
4798
+ const sigX = width - sigSize * 2.5;
4799
+ const sigY = height - sigSize * 2.5;
4800
+ const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
4801
+ const sigColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
4802
+ ctx.save();
4803
+ ctx.globalAlpha = 0.12 + sigRng() * 0.08;
4804
+ ctx.translate(sigX, sigY);
4805
+ ctx.strokeStyle = sigColor;
4806
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.06);
4807
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
4808
+ // Outer ring
4809
+ ctx.beginPath();
4810
+ ctx.arc(0, 0, sigSize, 0, Math.PI * 2);
4811
+ ctx.stroke();
4812
+ ctx.fill();
4813
+ // Inner geometric pattern — unique per hash
4814
+ ctx.beginPath();
4815
+ for(let s = 0; s < sigSegments; s++){
4816
+ const angle1 = sigRng() * Math.PI * 2;
4817
+ const angle2 = sigRng() * Math.PI * 2;
4818
+ const r1 = sigSize * (0.2 + sigRng() * 0.6);
4819
+ const r2 = sigSize * (0.2 + sigRng() * 0.6);
4820
+ ctx.moveTo(Math.cos(angle1) * r1, Math.sin(angle1) * r1);
4821
+ ctx.lineTo(Math.cos(angle2) * r2, Math.sin(angle2) * r2);
4822
+ }
4823
+ ctx.stroke();
4824
+ // Center dot
4825
+ ctx.beginPath();
4826
+ ctx.arc(0, 0, sigSize * 0.12, 0, Math.PI * 2);
4827
+ ctx.fillStyle = sigColor;
4828
+ ctx.fill();
4829
+ ctx.restore();
4830
+ }
3391
4831
  ctx.globalAlpha = 1;
3392
4832
  }
3393
4833