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/main.js CHANGED
@@ -541,6 +541,18 @@ function $d016ad53434219a1$export$6d1620b367f86f7a(rng) {
541
541
  intensity: intensity
542
542
  };
543
543
  }
544
+ function $d016ad53434219a1$export$1793a1bfbe4f6ff5(hex, degrees) {
545
+ const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
546
+ return $d016ad53434219a1$var$hslToHex((h + degrees + 360) % 360, s, l);
547
+ }
548
+ function $d016ad53434219a1$export$703ba40a4347f77a(base, layerRatio, hueShiftPerLayer) {
549
+ const shift = layerRatio * hueShiftPerLayer;
550
+ return {
551
+ dominant: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.dominant, shift),
552
+ secondary: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
553
+ accent: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
554
+ };
555
+ }
544
556
 
545
557
 
546
558
 
@@ -1331,35 +1343,32 @@ const $dd5df256f00f6199$export$c2fc138f94dd4b2a = {
1331
1343
  */ const $6222456bc073291c$export$580f80cfb9de73bc = (ctx, size, config)=>{
1332
1344
  const rng = config?.rng ?? Math.random;
1333
1345
  const r = size / 2;
1334
- const numPoints = 5 + Math.floor(rng() * 5); // 5-9 lobes
1346
+ const numPoints = 5 + Math.floor(rng() * 5);
1335
1347
  const points = [];
1336
1348
  for(let i = 0; i < numPoints; i++){
1337
1349
  const angle = i / numPoints * Math.PI * 2;
1338
- const jitter = 0.5 + rng() * 0.5; // radius varies 50-100%
1350
+ const jitter = 0.5 + rng() * 0.5;
1339
1351
  points.push({
1340
1352
  x: Math.cos(angle) * r * jitter,
1341
1353
  y: Math.sin(angle) * r * jitter
1342
1354
  });
1343
1355
  }
1344
1356
  ctx.beginPath();
1345
- // Start at midpoint between last and first point
1346
1357
  const last = points[points.length - 1];
1347
1358
  const first = points[0];
1348
1359
  ctx.moveTo((last.x + first.x) / 2, (last.y + first.y) / 2);
1349
1360
  for(let i = 0; i < numPoints; i++){
1350
1361
  const curr = points[i];
1351
1362
  const next = points[(i + 1) % numPoints];
1352
- const midX = (curr.x + next.x) / 2;
1353
- const midY = (curr.y + next.y) / 2;
1354
- ctx.quadraticCurveTo(curr.x, curr.y, midX, midY);
1363
+ ctx.quadraticCurveTo(curr.x, curr.y, (curr.x + next.x) / 2, (curr.y + next.y) / 2);
1355
1364
  }
1356
1365
  ctx.closePath();
1357
1366
  };
1358
1367
  const $6222456bc073291c$export$7a6094023f0902a6 = (ctx, size, config)=>{
1359
1368
  const rng = config?.rng ?? Math.random;
1360
1369
  const r = size / 2;
1361
- const sides = 3 + Math.floor(rng() * 10); // 3-12 sides
1362
- const jitterAmount = 0.1 + rng() * 0.4; // 10-50% vertex displacement
1370
+ const sides = 3 + Math.floor(rng() * 10);
1371
+ const jitterAmount = 0.1 + rng() * 0.4;
1363
1372
  ctx.beginPath();
1364
1373
  for(let i = 0; i < sides; i++){
1365
1374
  const angle = i / sides * Math.PI * 2 - Math.PI / 2;
@@ -1374,10 +1383,9 @@ const $6222456bc073291c$export$7a6094023f0902a6 = (ctx, size, config)=>{
1374
1383
  const $6222456bc073291c$export$ef56b4a8316e47d5 = (ctx, size, config)=>{
1375
1384
  const rng = config?.rng ?? Math.random;
1376
1385
  const r = size / 2;
1377
- // Frequency ratios small integers produce recognizable patterns
1378
- const freqA = 1 + Math.floor(rng() * 5); // 1-5
1379
- const freqB = 1 + Math.floor(rng() * 5); // 1-5
1380
- const phase = rng() * Math.PI; // phase offset
1386
+ const freqA = 1 + Math.floor(rng() * 5);
1387
+ const freqB = 1 + Math.floor(rng() * 5);
1388
+ const phase = rng() * Math.PI;
1381
1389
  const steps = 120;
1382
1390
  ctx.beginPath();
1383
1391
  for(let i = 0; i <= steps; i++){
@@ -1392,7 +1400,6 @@ const $6222456bc073291c$export$ef56b4a8316e47d5 = (ctx, size, config)=>{
1392
1400
  const $6222456bc073291c$export$1db9219b4f34658c = (ctx, size, config)=>{
1393
1401
  const rng = config?.rng ?? Math.random;
1394
1402
  const r = size / 2;
1395
- // Exponent range: 0.3 (spiky astroid) to 5 (rounded rectangle)
1396
1403
  const n = 0.3 + rng() * 4.7;
1397
1404
  const steps = 120;
1398
1405
  ctx.beginPath();
@@ -1400,7 +1407,6 @@ const $6222456bc073291c$export$1db9219b4f34658c = (ctx, size, config)=>{
1400
1407
  const t = i / steps * Math.PI * 2;
1401
1408
  const cosT = Math.cos(t);
1402
1409
  const sinT = Math.sin(t);
1403
- // Superellipse parametric form
1404
1410
  const x = Math.sign(cosT) * Math.pow(Math.abs(cosT), 2 / n) * r;
1405
1411
  const y = Math.sign(sinT) * Math.pow(Math.abs(sinT), 2 / n) * r;
1406
1412
  if (i === 0) ctx.moveTo(x, y);
@@ -1411,11 +1417,9 @@ const $6222456bc073291c$export$1db9219b4f34658c = (ctx, size, config)=>{
1411
1417
  const $6222456bc073291c$export$b027c64d22b01985 = (ctx, size, config)=>{
1412
1418
  const rng = config?.rng ?? Math.random;
1413
1419
  const scale = size / 2;
1414
- // R = outer radius, r = inner radius, d = pen distance from inner center
1415
1420
  const R = 1;
1416
- const r = 0.2 + rng() * 0.6; // 0.2-0.8
1417
- const d = 0.3 + rng() * 0.7; // 0.3-1.0
1418
- // Number of full rotations needed to close the curve
1421
+ const r = 0.2 + rng() * 0.6;
1422
+ const d = 0.3 + rng() * 0.7;
1419
1423
  const gcd = (a, b)=>{
1420
1424
  const ai = Math.round(a * 1000);
1421
1425
  const bi = Math.round(b * 1000);
@@ -1423,7 +1427,7 @@ const $6222456bc073291c$export$b027c64d22b01985 = (ctx, size, config)=>{
1423
1427
  return g(ai, bi) / 1000;
1424
1428
  };
1425
1429
  const period = r / gcd(R, r);
1426
- const maxT = Math.min(period, 10) * Math.PI * 2; // cap at 10 rotations
1430
+ const maxT = Math.min(period, 10) * Math.PI * 2;
1427
1431
  const steps = Math.min(600, Math.floor(maxT * 20));
1428
1432
  ctx.beginPath();
1429
1433
  for(let i = 0; i <= steps; i++){
@@ -1438,9 +1442,9 @@ const $6222456bc073291c$export$b027c64d22b01985 = (ctx, size, config)=>{
1438
1442
  const $6222456bc073291c$export$7608ccd03bfb705d = (ctx, size, config)=>{
1439
1443
  const rng = config?.rng ?? Math.random;
1440
1444
  const r = size / 2;
1441
- const rings = 2 + Math.floor(rng() * 4); // 2-5 rings
1442
- const freq = 3 + Math.floor(rng() * 12); // 3-14 waves per ring
1443
- const amp = 0.05 + rng() * 0.15; // 5-20% of radius
1445
+ const rings = 2 + Math.floor(rng() * 4);
1446
+ const freq = 3 + Math.floor(rng() * 12);
1447
+ const amp = 0.05 + rng() * 0.15;
1444
1448
  ctx.beginPath();
1445
1449
  for(let ring = 0; ring < rings; ring++){
1446
1450
  const baseR = r * (0.3 + ring / rings * 0.7);
@@ -1458,7 +1462,7 @@ const $6222456bc073291c$export$7608ccd03bfb705d = (ctx, size, config)=>{
1458
1462
  const $6222456bc073291c$export$11a377e7498bb523 = (ctx, size, config)=>{
1459
1463
  const rng = config?.rng ?? Math.random;
1460
1464
  const r = size / 2;
1461
- const k = 2 + Math.floor(rng() * 6); // 2-7 petal parameter
1465
+ const k = 2 + Math.floor(rng() * 6);
1462
1466
  const steps = 200;
1463
1467
  ctx.beginPath();
1464
1468
  for(let i = 0; i <= steps; i++){
@@ -1471,6 +1475,308 @@ const $6222456bc073291c$export$11a377e7498bb523 = (ctx, size, config)=>{
1471
1475
  }
1472
1476
  ctx.closePath();
1473
1477
  };
1478
+ const $6222456bc073291c$export$76b6526575ea179b = (ctx, size, config)=>{
1479
+ const rng = config?.rng ?? Math.random;
1480
+ const r = size / 2;
1481
+ const shardCount = 4 + Math.floor(rng() * 5); // 4-8 shards
1482
+ ctx.beginPath();
1483
+ for(let s = 0; s < shardCount; s++){
1484
+ const baseAngle = s / shardCount * Math.PI * 2 + (rng() - 0.5) * 0.3;
1485
+ const dist = r * (0.15 + rng() * 0.35);
1486
+ const cx = Math.cos(baseAngle) * dist;
1487
+ const cy = Math.sin(baseAngle) * dist;
1488
+ const shardSize = r * (0.2 + rng() * 0.4);
1489
+ const verts = 3 + Math.floor(rng() * 3); // 3-5 vertices per shard
1490
+ const shardAngleOffset = rng() * Math.PI * 2;
1491
+ for(let v = 0; v < verts; v++){
1492
+ const angle = shardAngleOffset + v / verts * Math.PI * 2;
1493
+ // Elongate shards along their radial direction
1494
+ const stretch = v % 2 === 0 ? 1.0 : 0.3 + rng() * 0.4;
1495
+ const px = cx + Math.cos(angle) * shardSize * stretch;
1496
+ const py = cy + Math.sin(angle) * shardSize * stretch;
1497
+ if (v === 0) ctx.moveTo(px, py);
1498
+ else ctx.lineTo(px, py);
1499
+ }
1500
+ ctx.closePath();
1501
+ }
1502
+ };
1503
+ const $6222456bc073291c$export$ed9ff98da5b05073 = (ctx, size, config)=>{
1504
+ const rng = config?.rng ?? Math.random;
1505
+ const r = size / 2;
1506
+ const edgeCount = 5 + Math.floor(rng() * 4); // 5-8 edges
1507
+ const points = [];
1508
+ // Generate edge midpoints at varying distances
1509
+ for(let i = 0; i < edgeCount; i++){
1510
+ const angle = i / edgeCount * Math.PI * 2 + (rng() - 0.5) * 0.4;
1511
+ const dist = r * (0.6 + rng() * 0.4);
1512
+ points.push({
1513
+ angle: angle,
1514
+ x: Math.cos(angle) * dist,
1515
+ y: Math.sin(angle) * dist
1516
+ });
1517
+ }
1518
+ // Sort by angle for proper winding
1519
+ points.sort((a, b)=>a.angle - b.angle);
1520
+ ctx.beginPath();
1521
+ ctx.moveTo(points[0].x, points[0].y);
1522
+ for(let i = 1; i < points.length; i++)ctx.lineTo(points[i].x, points[i].y);
1523
+ ctx.closePath();
1524
+ };
1525
+ const $6222456bc073291c$export$e0452d9a794fe7e5 = (ctx, size, config)=>{
1526
+ const rng = config?.rng ?? Math.random;
1527
+ const r = size / 2;
1528
+ const biteSize = 0.6 + rng() * 0.3; // 60-90% of radius
1529
+ const biteOffset = r * (0.3 + rng() * 0.4);
1530
+ const biteAngle = rng() * Math.PI * 2;
1531
+ // Outer circle
1532
+ ctx.beginPath();
1533
+ ctx.arc(0, 0, r, 0, Math.PI * 2);
1534
+ // Subtract inner circle using even-odd rule
1535
+ const bx = Math.cos(biteAngle) * biteOffset;
1536
+ const by = Math.sin(biteAngle) * biteOffset;
1537
+ // Draw inner circle counter-clockwise for subtraction
1538
+ ctx.moveTo(bx + r * biteSize, by);
1539
+ ctx.arc(bx, by, r * biteSize, 0, Math.PI * 2, true);
1540
+ };
1541
+ const $6222456bc073291c$export$38bfe5eb52137e01 = (ctx, size, config)=>{
1542
+ const rng = config?.rng ?? Math.random;
1543
+ const r = size / 2;
1544
+ const segments = 12 + Math.floor(rng() * 8);
1545
+ const startAngle = rng() * Math.PI * 2;
1546
+ const curvature = (rng() - 0.5) * 0.4;
1547
+ // Build spine points
1548
+ const spine = [];
1549
+ let angle = startAngle;
1550
+ let px = 0, py = 0;
1551
+ for(let i = 0; i <= segments; i++){
1552
+ spine.push({
1553
+ x: px,
1554
+ y: py
1555
+ });
1556
+ const stepLen = r / segments * (1.5 + rng() * 0.5);
1557
+ angle += curvature + (rng() - 0.5) * 0.6;
1558
+ px += Math.cos(angle) * stepLen;
1559
+ py += Math.sin(angle) * stepLen;
1560
+ }
1561
+ // Build tapered outline by offsetting perpendicular to spine
1562
+ ctx.beginPath();
1563
+ const leftSide = [];
1564
+ const rightSide = [];
1565
+ for(let i = 0; i < spine.length; i++){
1566
+ const t = i / (spine.length - 1);
1567
+ const width = r * 0.12 * (1 - t * 0.9); // taper from thick to thin
1568
+ const next = spine[Math.min(i + 1, spine.length - 1)];
1569
+ const dx = next.x - spine[i].x;
1570
+ const dy = next.y - spine[i].y;
1571
+ const len = Math.hypot(dx, dy) || 1;
1572
+ const nx = -dy / len;
1573
+ const ny = dx / len;
1574
+ leftSide.push({
1575
+ x: spine[i].x + nx * width,
1576
+ y: spine[i].y + ny * width
1577
+ });
1578
+ rightSide.push({
1579
+ x: spine[i].x - nx * width,
1580
+ y: spine[i].y - ny * width
1581
+ });
1582
+ }
1583
+ ctx.moveTo(leftSide[0].x, leftSide[0].y);
1584
+ for(let i = 1; i < leftSide.length; i++)ctx.lineTo(leftSide[i].x, leftSide[i].y);
1585
+ for(let i = rightSide.length - 1; i >= 0; i--)ctx.lineTo(rightSide[i].x, rightSide[i].y);
1586
+ ctx.closePath();
1587
+ };
1588
+ const $6222456bc073291c$export$105caa8cfd63c422 = (ctx, size, config)=>{
1589
+ const rng = config?.rng ?? Math.random;
1590
+ const r = size / 2;
1591
+ const lobeCount = 4 + Math.floor(rng() * 4); // 4-7 lobes
1592
+ const spineAngle = rng() * Math.PI * 2;
1593
+ const spineLen = r * 0.6;
1594
+ ctx.beginPath();
1595
+ for(let i = 0; i < lobeCount; i++){
1596
+ const t = i / (lobeCount - 1) - 0.5; // -0.5 to 0.5
1597
+ const sx = Math.cos(spineAngle) * spineLen * t;
1598
+ const sy = Math.sin(spineAngle) * spineLen * t;
1599
+ // Offset perpendicular for cloud shape
1600
+ const perpAngle = spineAngle + Math.PI / 2;
1601
+ const perpOff = (rng() - 0.3) * r * 0.3;
1602
+ const cx = sx + Math.cos(perpAngle) * perpOff;
1603
+ const cy = sy + Math.sin(perpAngle) * perpOff;
1604
+ const lobeR = r * (0.25 + rng() * 0.2);
1605
+ ctx.moveTo(cx + lobeR, cy);
1606
+ ctx.arc(cx, cy, lobeR, 0, Math.PI * 2);
1607
+ }
1608
+ };
1609
+ const $6222456bc073291c$export$e181e5bd3c539569 = (ctx, size, config)=>{
1610
+ const rng = config?.rng ?? Math.random;
1611
+ const r = size / 2;
1612
+ const spikeCount = 8 + Math.floor(rng() * 8); // 8-15 spikes
1613
+ const points = [];
1614
+ for(let i = 0; i < spikeCount; i++){
1615
+ const angle = i / spikeCount * Math.PI * 2;
1616
+ const isSpike = i % 2 === 0;
1617
+ const dist = isSpike ? r * (0.5 + rng() * 0.5 // spikes reach 50-100% of radius
1618
+ ) : r * (0.15 + rng() * 0.2); // valleys at 15-35%
1619
+ points.push({
1620
+ x: Math.cos(angle) * dist,
1621
+ y: Math.sin(angle) * dist
1622
+ });
1623
+ }
1624
+ ctx.beginPath();
1625
+ ctx.moveTo(points[0].x, points[0].y);
1626
+ for(let i = 0; i < points.length; i++){
1627
+ const curr = points[i];
1628
+ const next = points[(i + 1) % points.length];
1629
+ const cpx = (curr.x + next.x) / 2 + (rng() - 0.5) * r * 0.15;
1630
+ const cpy = (curr.y + next.y) / 2 + (rng() - 0.5) * r * 0.15;
1631
+ ctx.quadraticCurveTo(cpx, cpy, next.x, next.y);
1632
+ }
1633
+ ctx.closePath();
1634
+ };
1635
+ const $6222456bc073291c$export$155b4780b4c6bb7b = (ctx, size, config)=>{
1636
+ const rng = config?.rng ?? Math.random;
1637
+ const r = size / 2;
1638
+ const subdivisions = 1 + Math.floor(rng() * 3); // 1-3
1639
+ // Start with icosahedron vertices projected to 2D
1640
+ const baseVerts = 6 + subdivisions * 4;
1641
+ const points = [];
1642
+ for(let i = 0; i < baseVerts; i++){
1643
+ const angle = i / baseVerts * Math.PI * 2;
1644
+ const ring = i % 2 === 0 ? 1.0 : 0.5 + rng() * 0.3;
1645
+ points.push({
1646
+ x: Math.cos(angle) * r * ring,
1647
+ y: Math.sin(angle) * r * ring
1648
+ });
1649
+ }
1650
+ ctx.beginPath();
1651
+ // Draw triangulated mesh — connect each point to neighbors and center
1652
+ for(let i = 0; i < points.length; i++){
1653
+ const next = points[(i + 1) % points.length];
1654
+ ctx.moveTo(points[i].x, points[i].y);
1655
+ ctx.lineTo(next.x, next.y);
1656
+ // Connect to center
1657
+ ctx.moveTo(points[i].x, points[i].y);
1658
+ ctx.lineTo(0, 0);
1659
+ // Cross-connect to create triangulation
1660
+ if (i % 2 === 0 && i + 2 < points.length) {
1661
+ ctx.moveTo(points[i].x, points[i].y);
1662
+ ctx.lineTo(points[i + 2].x, points[i + 2].y);
1663
+ }
1664
+ }
1665
+ // Outer ring
1666
+ ctx.moveTo(points[0].x, points[0].y);
1667
+ for(let i = 1; i < points.length; i++)ctx.lineTo(points[i].x, points[i].y);
1668
+ ctx.closePath();
1669
+ };
1670
+ const $6222456bc073291c$export$9a7e648f11155172 = (ctx, size, config)=>{
1671
+ const rng = config?.rng ?? Math.random;
1672
+ const r = size / 2;
1673
+ const phi = (1 + Math.sqrt(5)) / 2; // golden ratio
1674
+ const isKite = rng() < 0.5;
1675
+ ctx.beginPath();
1676
+ if (isKite) {
1677
+ // Kite: two golden triangles joined at base
1678
+ const topY = -r;
1679
+ const bottomY = r * (1 / phi);
1680
+ const midY = r * (1 / phi - 1) * 0.3;
1681
+ const wingX = r * 0.6;
1682
+ ctx.moveTo(0, topY);
1683
+ ctx.lineTo(wingX, midY);
1684
+ ctx.lineTo(0, bottomY);
1685
+ ctx.lineTo(-wingX, midY);
1686
+ } else {
1687
+ // Dart: concave quadrilateral
1688
+ const topY = -r;
1689
+ const bottomY = r * 0.3;
1690
+ const midY = -r * 0.1;
1691
+ const wingX = r * 0.5;
1692
+ ctx.moveTo(0, topY);
1693
+ ctx.lineTo(wingX, midY);
1694
+ ctx.lineTo(0, bottomY);
1695
+ ctx.lineTo(-wingX, midY);
1696
+ }
1697
+ ctx.closePath();
1698
+ };
1699
+ const $6222456bc073291c$export$1fc0aedbabd73399 = (ctx, size, config)=>{
1700
+ const r = size / 2;
1701
+ // Vertices of equilateral triangle
1702
+ const verts = [];
1703
+ for(let i = 0; i < 3; i++){
1704
+ const angle = i / 3 * Math.PI * 2 - Math.PI / 2;
1705
+ verts.push({
1706
+ x: Math.cos(angle) * r * 0.7,
1707
+ y: Math.sin(angle) * r * 0.7
1708
+ });
1709
+ }
1710
+ // Side length = distance between vertices
1711
+ const sideLen = Math.hypot(verts[1].x - verts[0].x, verts[1].y - verts[0].y);
1712
+ ctx.beginPath();
1713
+ for(let i = 0; i < 3; i++){
1714
+ const from = verts[(i + 1) % 3];
1715
+ const to = verts[(i + 2) % 3];
1716
+ const center = verts[i];
1717
+ const startAngle = Math.atan2(from.y - center.y, from.x - center.x);
1718
+ const endAngle = Math.atan2(to.y - center.y, to.x - center.x);
1719
+ if (i === 0) ctx.moveTo(from.x, from.y);
1720
+ ctx.arc(center.x, center.y, sideLen, startAngle, endAngle);
1721
+ }
1722
+ ctx.closePath();
1723
+ };
1724
+ const $6222456bc073291c$export$ef7b5e0c19a21fd1 = (ctx, size, config)=>{
1725
+ const rng = config?.rng ?? Math.random;
1726
+ const r = size / 2;
1727
+ const dotCount = 15 + Math.floor(rng() * 25); // 15-39 dots
1728
+ const clusterTightness = 0.3 + rng() * 0.5;
1729
+ ctx.beginPath();
1730
+ for(let i = 0; i < dotCount; i++){
1731
+ // Gaussian-ish distribution via Box-Muller approximation
1732
+ const u1 = Math.max(0.001, rng());
1733
+ const u2 = rng();
1734
+ const mag = Math.sqrt(-2 * Math.log(u1)) * clusterTightness;
1735
+ const angle = u2 * Math.PI * 2;
1736
+ const dx = Math.cos(angle) * mag * r;
1737
+ const dy = Math.sin(angle) * mag * r;
1738
+ const dotR = r * (0.02 + rng() * 0.04);
1739
+ ctx.moveTo(dx + dotR, dy);
1740
+ ctx.arc(dx, dy, dotR, 0, Math.PI * 2);
1741
+ }
1742
+ };
1743
+ const $6222456bc073291c$export$f15df8ab60dfcc9a = (ctx, size, config)=>{
1744
+ const rng = config?.rng ?? Math.random;
1745
+ const r = size / 2;
1746
+ const angle1 = rng() * Math.PI;
1747
+ const angle2 = angle1 + Math.PI / 2 + (rng() - 0.5) * 0.3;
1748
+ const spacing = r * (0.08 + rng() * 0.08);
1749
+ const hasCross = rng() < 0.6;
1750
+ // Draw bounding shape (ellipse)
1751
+ const rx = r * (0.7 + rng() * 0.3);
1752
+ const ry = r * (0.5 + rng() * 0.3);
1753
+ // Outer boundary
1754
+ ctx.beginPath();
1755
+ ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
1756
+ // Hatch lines clipped to the ellipse
1757
+ const cos1 = Math.cos(angle1);
1758
+ const sin1 = Math.sin(angle1);
1759
+ for(let d = -r; d <= r; d += spacing){
1760
+ const lx1 = d * cos1 - r * sin1;
1761
+ const ly1 = d * sin1 + r * cos1;
1762
+ const lx2 = d * cos1 + r * sin1;
1763
+ const ly2 = d * sin1 - r * cos1;
1764
+ ctx.moveTo(lx1, ly1);
1765
+ ctx.lineTo(lx2, ly2);
1766
+ }
1767
+ if (hasCross) {
1768
+ const cos2 = Math.cos(angle2);
1769
+ const sin2 = Math.sin(angle2);
1770
+ for(let d = -r; d <= r; d += spacing * 1.3){
1771
+ const lx1 = d * cos2 - r * sin2;
1772
+ const ly1 = d * sin2 + r * cos2;
1773
+ const lx2 = d * cos2 + r * sin2;
1774
+ const ly2 = d * sin2 - r * cos2;
1775
+ ctx.moveTo(lx1, ly1);
1776
+ ctx.lineTo(lx2, ly2);
1777
+ }
1778
+ }
1779
+ };
1474
1780
  const $6222456bc073291c$export$40cfb4c637f2fbb5 = {
1475
1781
  blob: $6222456bc073291c$export$580f80cfb9de73bc,
1476
1782
  ngon: $6222456bc073291c$export$7a6094023f0902a6,
@@ -1478,7 +1784,18 @@ const $6222456bc073291c$export$40cfb4c637f2fbb5 = {
1478
1784
  superellipse: $6222456bc073291c$export$1db9219b4f34658c,
1479
1785
  spirograph: $6222456bc073291c$export$b027c64d22b01985,
1480
1786
  waveRing: $6222456bc073291c$export$7608ccd03bfb705d,
1481
- rose: $6222456bc073291c$export$11a377e7498bb523
1787
+ rose: $6222456bc073291c$export$11a377e7498bb523,
1788
+ shardField: $6222456bc073291c$export$76b6526575ea179b,
1789
+ voronoiCell: $6222456bc073291c$export$ed9ff98da5b05073,
1790
+ crescent: $6222456bc073291c$export$e0452d9a794fe7e5,
1791
+ tendril: $6222456bc073291c$export$38bfe5eb52137e01,
1792
+ cloudForm: $6222456bc073291c$export$105caa8cfd63c422,
1793
+ inkSplat: $6222456bc073291c$export$e181e5bd3c539569,
1794
+ geodesicDome: $6222456bc073291c$export$155b4780b4c6bb7b,
1795
+ penroseTile: $6222456bc073291c$export$9a7e648f11155172,
1796
+ reuleauxTriangle: $6222456bc073291c$export$1fc0aedbabd73399,
1797
+ dotCluster: $6222456bc073291c$export$ef7b5e0c19a21fd1,
1798
+ crosshatchPatch: $6222456bc073291c$export$f15df8ab60dfcc9a
1482
1799
  };
1483
1800
 
1484
1801
 
@@ -1513,7 +1830,14 @@ const $c3de8257a8baa3b0$var$RENDER_STYLES = [
1513
1830
  "dashed",
1514
1831
  "watercolor",
1515
1832
  "hatched",
1516
- "incomplete"
1833
+ "incomplete",
1834
+ "stipple",
1835
+ "stencil",
1836
+ "noise-grain",
1837
+ "wood-grain",
1838
+ "marble-vein",
1839
+ "fabric-weave",
1840
+ "hand-drawn"
1517
1841
  ];
1518
1842
  function $c3de8257a8baa3b0$export$9fd4e64b2acd410e(rng) {
1519
1843
  return $c3de8257a8baa3b0$var$RENDER_STYLES[Math.floor(rng() * $c3de8257a8baa3b0$var$RENDER_STYLES.length)];
@@ -1687,6 +2011,240 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
1687
2011
  ctx.lineDashOffset = 0;
1688
2012
  break;
1689
2013
  }
2014
+ case "stipple":
2015
+ {
2016
+ // Dot-fill texture — clip to shape, then scatter dots
2017
+ const savedAlphaS = ctx.globalAlpha;
2018
+ ctx.globalAlpha = savedAlphaS * 0.15;
2019
+ ctx.fill(); // ghost fill
2020
+ ctx.globalAlpha = savedAlphaS;
2021
+ ctx.save();
2022
+ ctx.clip();
2023
+ const dotSpacing = Math.max(2, size * 0.03);
2024
+ const extent = size * 0.55;
2025
+ ctx.globalAlpha = savedAlphaS * 0.7;
2026
+ for(let dx = -extent; dx <= extent; dx += dotSpacing)for(let dy = -extent; dy <= extent; dy += dotSpacing){
2027
+ // Jitter each dot position for organic feel
2028
+ const jx = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
2029
+ const jy = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
2030
+ const dotR = rng ? dotSpacing * (0.15 + rng() * 0.2) : dotSpacing * 0.2;
2031
+ ctx.beginPath();
2032
+ ctx.arc(dx + jx, dy + jy, dotR, 0, Math.PI * 2);
2033
+ ctx.fill();
2034
+ }
2035
+ ctx.restore();
2036
+ ctx.globalAlpha = savedAlphaS;
2037
+ // Outline
2038
+ ctx.globalAlpha *= 0.4;
2039
+ ctx.stroke();
2040
+ ctx.globalAlpha /= 0.4;
2041
+ break;
2042
+ }
2043
+ case "stencil":
2044
+ {
2045
+ // Negative-space cutout — fill a rectangle, then erase the shape
2046
+ const savedAlphaSt = ctx.globalAlpha;
2047
+ // Fill a bounding area with the stroke color
2048
+ ctx.globalAlpha = savedAlphaSt * 0.5;
2049
+ ctx.fillStyle = strokeColor;
2050
+ ctx.fillRect(-size * 0.6, -size * 0.6, size * 1.2, size * 1.2);
2051
+ // Cut out the shape using destination-out
2052
+ ctx.globalCompositeOperation = "destination-out";
2053
+ ctx.globalAlpha = 1;
2054
+ ctx.fill();
2055
+ ctx.globalCompositeOperation = "source-over";
2056
+ ctx.globalAlpha = savedAlphaSt;
2057
+ // Subtle outline of the cutout
2058
+ ctx.globalAlpha *= 0.3;
2059
+ ctx.stroke();
2060
+ ctx.globalAlpha /= 0.3;
2061
+ break;
2062
+ }
2063
+ case "noise-grain":
2064
+ {
2065
+ // Procedural noise grain texture clipped to shape boundary
2066
+ const savedAlphaN = ctx.globalAlpha;
2067
+ ctx.globalAlpha = savedAlphaN * 0.25;
2068
+ ctx.fill(); // base tint
2069
+ ctx.globalAlpha = savedAlphaN;
2070
+ ctx.save();
2071
+ ctx.clip();
2072
+ const grainSpacing = Math.max(1.5, size * 0.015);
2073
+ const extentN = size * 0.55;
2074
+ ctx.globalAlpha = savedAlphaN * 0.6;
2075
+ for(let gx = -extentN; gx <= extentN; gx += grainSpacing)for(let gy = -extentN; gy <= extentN; gy += grainSpacing){
2076
+ if (!rng) break;
2077
+ const jx = (rng() - 0.5) * grainSpacing * 1.2;
2078
+ const jy = (rng() - 0.5) * grainSpacing * 1.2;
2079
+ const brightness = rng() > 0.5 ? 255 : 0;
2080
+ const dotAlpha = 0.15 + rng() * 0.35;
2081
+ ctx.globalAlpha = savedAlphaN * dotAlpha;
2082
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
2083
+ const dotSize = grainSpacing * (0.3 + rng() * 0.5);
2084
+ ctx.fillRect(gx + jx, gy + jy, dotSize, dotSize);
2085
+ }
2086
+ ctx.restore();
2087
+ ctx.fillStyle = fillColor;
2088
+ ctx.globalAlpha = savedAlphaN;
2089
+ ctx.globalAlpha *= 0.4;
2090
+ ctx.stroke();
2091
+ ctx.globalAlpha /= 0.4;
2092
+ break;
2093
+ }
2094
+ case "wood-grain":
2095
+ {
2096
+ // Parallel wavy lines simulating wood grain, clipped to shape
2097
+ const savedAlphaW = ctx.globalAlpha;
2098
+ ctx.globalAlpha = savedAlphaW * 0.2;
2099
+ ctx.fill(); // base tint
2100
+ ctx.globalAlpha = savedAlphaW;
2101
+ ctx.save();
2102
+ ctx.clip();
2103
+ const grainLineSpacing = Math.max(2, size * 0.035);
2104
+ const extentW = size * 0.55;
2105
+ const waveFreq = rng ? 3 + rng() * 5 : 5;
2106
+ const waveAmp = rng ? size * (0.01 + rng() * 0.03) : size * 0.02;
2107
+ const grainAngle = rng ? rng() * Math.PI : Math.PI * 0.25;
2108
+ ctx.lineWidth = Math.max(0.5, strokeWidth * 0.3);
2109
+ ctx.globalAlpha = savedAlphaW * 0.5;
2110
+ const cosG = Math.cos(grainAngle);
2111
+ const sinG = Math.sin(grainAngle);
2112
+ for(let d = -extentW; d <= extentW; d += grainLineSpacing){
2113
+ ctx.beginPath();
2114
+ for(let t = -extentW; t <= extentW; t += 2){
2115
+ const wave = Math.sin(t / extentW * waveFreq * Math.PI) * waveAmp;
2116
+ const px = t * cosG - (d + wave) * sinG;
2117
+ const py = t * sinG + (d + wave) * cosG;
2118
+ if (t === -extentW) ctx.moveTo(px, py);
2119
+ else ctx.lineTo(px, py);
2120
+ }
2121
+ ctx.stroke();
2122
+ }
2123
+ ctx.restore();
2124
+ ctx.globalAlpha = savedAlphaW;
2125
+ ctx.globalAlpha *= 0.35;
2126
+ ctx.stroke();
2127
+ ctx.globalAlpha /= 0.35;
2128
+ break;
2129
+ }
2130
+ case "marble-vein":
2131
+ {
2132
+ // Branching vein lines on a soft fill, clipped to shape
2133
+ const savedAlphaM = ctx.globalAlpha;
2134
+ ctx.globalAlpha = savedAlphaM * 0.35;
2135
+ ctx.fill(); // soft base
2136
+ ctx.globalAlpha = savedAlphaM;
2137
+ ctx.save();
2138
+ ctx.clip();
2139
+ const veinCount = rng ? 2 + Math.floor(rng() * 3) : 3;
2140
+ const extentM = size * 0.45;
2141
+ ctx.lineWidth = Math.max(0.5, strokeWidth * 0.5);
2142
+ ctx.globalAlpha = savedAlphaM * 0.4;
2143
+ for(let v = 0; v < veinCount; v++){
2144
+ const startX = rng ? (rng() - 0.5) * extentM * 2 : 0;
2145
+ const startY = rng ? -extentM + rng() * extentM * 0.5 : -extentM;
2146
+ let vx = startX;
2147
+ let vy = startY;
2148
+ const steps = 15 + (rng ? Math.floor(rng() * 15) : 10);
2149
+ const stepLen = size * 0.04;
2150
+ ctx.beginPath();
2151
+ ctx.moveTo(vx, vy);
2152
+ for(let s = 0; s < steps; s++){
2153
+ const drift = rng ? (rng() - 0.5) * stepLen * 1.5 : 0;
2154
+ vx += drift;
2155
+ vy += stepLen;
2156
+ ctx.lineTo(vx, vy);
2157
+ // Branch ~20% of the time
2158
+ if (rng && rng() < 0.2 && s > 2 && s < steps - 3) {
2159
+ const branchDir = rng() < 0.5 ? -1 : 1;
2160
+ let bx = vx;
2161
+ let by = vy;
2162
+ const bSteps = 3 + Math.floor(rng() * 5);
2163
+ ctx.moveTo(bx, by);
2164
+ for(let bs = 0; bs < bSteps; bs++){
2165
+ bx += branchDir * stepLen * (0.5 + rng() * 0.5);
2166
+ by += stepLen * 0.6;
2167
+ ctx.lineTo(bx, by);
2168
+ }
2169
+ ctx.moveTo(vx, vy); // return to main vein
2170
+ }
2171
+ }
2172
+ ctx.stroke();
2173
+ }
2174
+ ctx.restore();
2175
+ ctx.globalAlpha = savedAlphaM;
2176
+ ctx.globalAlpha *= 0.3;
2177
+ ctx.stroke();
2178
+ ctx.globalAlpha /= 0.3;
2179
+ break;
2180
+ }
2181
+ case "fabric-weave":
2182
+ {
2183
+ // Interlocking horizontal/vertical threads clipped to shape
2184
+ const savedAlphaF = ctx.globalAlpha;
2185
+ ctx.globalAlpha = savedAlphaF * 0.15;
2186
+ ctx.fill(); // ghost base
2187
+ ctx.globalAlpha = savedAlphaF;
2188
+ ctx.save();
2189
+ ctx.clip();
2190
+ const threadSpacing = Math.max(2, size * 0.04);
2191
+ const extentF = size * 0.55;
2192
+ ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
2193
+ ctx.globalAlpha = savedAlphaF * 0.55;
2194
+ // Horizontal threads
2195
+ for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2196
+ ctx.beginPath();
2197
+ ctx.moveTo(-extentF, y);
2198
+ ctx.lineTo(extentF, y);
2199
+ ctx.stroke();
2200
+ }
2201
+ // Vertical threads (offset by half spacing for weave effect)
2202
+ ctx.globalAlpha = savedAlphaF * 0.45;
2203
+ ctx.strokeStyle = fillColor;
2204
+ for(let x = -extentF; x <= extentF; x += threadSpacing * 2){
2205
+ ctx.beginPath();
2206
+ for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2207
+ // Over-under: draw segment, skip segment
2208
+ ctx.moveTo(x, y);
2209
+ ctx.lineTo(x, y + threadSpacing);
2210
+ }
2211
+ ctx.stroke();
2212
+ }
2213
+ ctx.strokeStyle = strokeColor;
2214
+ ctx.restore();
2215
+ ctx.globalAlpha = savedAlphaF;
2216
+ ctx.globalAlpha *= 0.3;
2217
+ ctx.stroke();
2218
+ ctx.globalAlpha /= 0.3;
2219
+ break;
2220
+ }
2221
+ case "hand-drawn":
2222
+ {
2223
+ // Wobbly hand-drawn edge treatment — fill normally, then redraw
2224
+ // the outline with perturbed control points for a sketchy feel
2225
+ const savedAlphaHD = ctx.globalAlpha;
2226
+ ctx.globalAlpha = savedAlphaHD * 0.85;
2227
+ ctx.fill();
2228
+ ctx.globalAlpha = savedAlphaHD;
2229
+ // Draw 2-3 slightly offset wobbly strokes for a sketchy look
2230
+ const wobblePasses = 2 + (rng ? Math.floor(rng() * 2) : 0);
2231
+ ctx.lineWidth = strokeWidth * 0.8;
2232
+ for(let wp = 0; wp < wobblePasses; wp++){
2233
+ ctx.globalAlpha = savedAlphaHD * (0.4 - wp * 0.1);
2234
+ ctx.save();
2235
+ // Slight random offset per pass
2236
+ const wobbleX = rng ? (rng() - 0.5) * size * 0.02 : 0;
2237
+ const wobbleY = rng ? (rng() - 0.5) * size * 0.02 : 0;
2238
+ ctx.translate(wobbleX, wobbleY);
2239
+ // Slightly different scale per pass for edge variation
2240
+ const wobbleScale = 1 + (rng ? (rng() - 0.5) * 0.03 : 0);
2241
+ ctx.scale(wobbleScale, wobbleScale);
2242
+ ctx.stroke();
2243
+ ctx.restore();
2244
+ }
2245
+ ctx.globalAlpha = savedAlphaHD;
2246
+ break;
2247
+ }
1690
2248
  case "fill-and-stroke":
1691
2249
  default:
1692
2250
  ctx.fill();
@@ -1733,6 +2291,64 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1733
2291
  });
1734
2292
  ctx.restore();
1735
2293
  }
2294
+ function $c3de8257a8baa3b0$export$8bd8bbd1a8e53689(ctx, shape, x, y, config) {
2295
+ const { mirrorAxis: mirrorAxis = "horizontal", mirrorGap: mirrorGap = 0 } = config;
2296
+ // Draw the primary shape
2297
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config);
2298
+ // Draw the mirrored copy
2299
+ ctx.save();
2300
+ const savedAlpha = ctx.globalAlpha;
2301
+ ctx.globalAlpha = savedAlpha * 0.7; // mirror is slightly softer
2302
+ switch(mirrorAxis){
2303
+ case "horizontal":
2304
+ // Reflect across vertical axis at shape position
2305
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y + mirrorGap, {
2306
+ ...config,
2307
+ rotation: -(config.rotation || 0),
2308
+ size: config.size * 0.95
2309
+ });
2310
+ break;
2311
+ case "vertical":
2312
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x + mirrorGap, y, {
2313
+ ...config,
2314
+ rotation: 180 - (config.rotation || 0),
2315
+ size: config.size * 0.95
2316
+ });
2317
+ break;
2318
+ case "diagonal":
2319
+ // Reflect across 45° axis
2320
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x + mirrorGap * 0.7, y + mirrorGap * 0.7, {
2321
+ ...config,
2322
+ rotation: 90 - (config.rotation || 0),
2323
+ size: config.size * 0.9
2324
+ });
2325
+ break;
2326
+ case "radial-4":
2327
+ // Four-way radial mirror
2328
+ for(let i = 1; i < 4; i++){
2329
+ const angle = i / 4 * Math.PI * 2;
2330
+ const mx = x + Math.cos(angle) * mirrorGap;
2331
+ const my = y + Math.sin(angle) * mirrorGap;
2332
+ ctx.globalAlpha = savedAlpha * (0.7 - i * 0.1);
2333
+ $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, mx, my, {
2334
+ ...config,
2335
+ rotation: (config.rotation || 0) + i * 90,
2336
+ size: config.size * (0.95 - i * 0.05)
2337
+ });
2338
+ }
2339
+ break;
2340
+ }
2341
+ ctx.globalAlpha = savedAlpha;
2342
+ ctx.restore();
2343
+ }
2344
+ function $c3de8257a8baa3b0$export$879206e23912d1a9(rng) {
2345
+ const roll = rng();
2346
+ if (roll < 0.60) return null;
2347
+ if (roll < 0.75) return "horizontal";
2348
+ if (roll < 0.87) return "vertical";
2349
+ if (roll < 0.95) return "diagonal";
2350
+ return "radial-4";
2351
+ }
1736
2352
 
1737
2353
 
1738
2354
 
@@ -1764,7 +2380,8 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
1764
2380
  bestStyles: [
1765
2381
  "fill-only",
1766
2382
  "watercolor",
1767
- "fill-and-stroke"
2383
+ "fill-and-stroke",
2384
+ "hand-drawn"
1768
2385
  ]
1769
2386
  },
1770
2387
  square: {
@@ -1801,7 +2418,8 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
1801
2418
  bestStyles: [
1802
2419
  "fill-and-stroke",
1803
2420
  "fill-only",
1804
- "watercolor"
2421
+ "watercolor",
2422
+ "hand-drawn"
1805
2423
  ]
1806
2424
  },
1807
2425
  hexagon: {
@@ -2181,7 +2799,8 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
2181
2799
  bestStyles: [
2182
2800
  "fill-only",
2183
2801
  "watercolor",
2184
- "fill-and-stroke"
2802
+ "fill-and-stroke",
2803
+ "hand-drawn"
2185
2804
  ]
2186
2805
  },
2187
2806
  ngon: {
@@ -2235,7 +2854,8 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
2235
2854
  bestStyles: [
2236
2855
  "fill-only",
2237
2856
  "watercolor",
2238
- "fill-and-stroke"
2857
+ "fill-and-stroke",
2858
+ "wood-grain"
2239
2859
  ]
2240
2860
  },
2241
2861
  spirograph: {
@@ -2291,84 +2911,355 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
2291
2911
  "fill-only",
2292
2912
  "watercolor"
2293
2913
  ]
2294
- }
2295
- };
2296
- function $e73976f898150d4d$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
2297
- const available = shapeNames.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]);
2298
- // Pick a seed shape — tier 1 shapes that are hero candidates
2299
- const heroPool = available.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s].tier === 1 && $e73976f898150d4d$export$4343b39fe47bd82c[s].heroCandidate);
2300
- const seedShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : available[Math.floor(rng() * available.length)];
2301
- const seedProfile = $e73976f898150d4d$export$4343b39fe47bd82c[seedShape];
2302
- // Primary: seed shape + its direct affinities (tier 1-2 only)
2303
- const primaryCandidates = [
2304
- seedShape,
2305
- ...seedProfile.affinities
2306
- ].filter((s)=>available.includes(s)).filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s].tier <= 2);
2307
- const primary = [
2308
- ...new Set(primaryCandidates)
2309
- ].slice(0, 5);
2310
- // Supporting: affinities of affinities, plus same-category shapes
2311
- const supportingSet = new Set();
2312
- for (const p of primary){
2313
- const profile = $e73976f898150d4d$export$4343b39fe47bd82c[p];
2314
- if (!profile) continue;
2315
- for (const aff of profile.affinities)if (available.includes(aff) && !primary.includes(aff)) supportingSet.add(aff);
2316
- }
2317
- // Add same-category tier 1-2 shapes
2318
- for (const s of available){
2319
- const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
2320
- if (p.category === seedProfile.category && p.tier <= 2 && !primary.includes(s)) supportingSet.add(s);
2321
- }
2322
- const supporting = [
2323
- ...supportingSet
2324
- ].slice(0, 6);
2325
- // Accents: tier 1 shapes from other categories for contrast
2326
- const usedCategories = new Set([
2327
- ...primary,
2328
- ...supporting
2329
- ].map((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]?.category));
2330
- const accentCandidates = available.filter((s)=>!primary.includes(s) && !supporting.includes(s) && $e73976f898150d4d$export$4343b39fe47bd82c[s].tier <= 2 && !usedCategories.has($e73976f898150d4d$export$4343b39fe47bd82c[s].category));
2331
- // Shuffle and take a few
2332
- const accents = [];
2333
- const shuffled = [
2334
- ...accentCandidates
2335
- ];
2336
- for(let i = shuffled.length - 1; i > 0; i--){
2337
- const j = Math.floor(rng() * (i + 1));
2338
- [shuffled[i], shuffled[j]] = [
2339
- shuffled[j],
2340
- shuffled[i]
2341
- ];
2342
- }
2343
- accents.push(...shuffled.slice(0, 3));
2344
- // For certain archetypes, bias the palette
2345
- if (archetypeName === "geometric-precision") // Remove blobs and organic shapes from primary
2346
- return {
2347
- primary: primary.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]?.category !== "procedural" || s === "ngon"),
2348
- supporting: supporting.filter((s)=>s !== "blob"),
2349
- accents: accents
2350
- };
2351
- if (archetypeName === "organic-flow") {
2352
- // Boost procedural/organic shapes
2353
- const organicBoost = available.filter((s)=>[
2354
- "blob",
2355
- "superellipse",
2356
- "waveRing",
2357
- "rose"
2358
- ].includes(s) && !primary.includes(s));
2359
- return {
2360
- primary: [
2361
- ...primary,
2362
- ...organicBoost.slice(0, 2)
2363
- ],
2364
- supporting: supporting,
2365
- accents: accents
2366
- };
2367
- }
2368
- return {
2369
- primary: primary,
2370
- supporting: supporting,
2371
- accents: accents
2914
+ },
2915
+ // ── New procedural shapes ─────────────────────────────────────
2916
+ shardField: {
2917
+ tier: 2,
2918
+ minSizeFraction: 0.1,
2919
+ maxSizeFraction: 0.7,
2920
+ affinities: [
2921
+ "voronoiCell",
2922
+ "diamond",
2923
+ "triangle",
2924
+ "penroseTile"
2925
+ ],
2926
+ category: "procedural",
2927
+ heroCandidate: false,
2928
+ bestStyles: [
2929
+ "fill-and-stroke",
2930
+ "stroke-only",
2931
+ "fill-only"
2932
+ ]
2933
+ },
2934
+ voronoiCell: {
2935
+ tier: 1,
2936
+ minSizeFraction: 0.08,
2937
+ maxSizeFraction: 0.9,
2938
+ affinities: [
2939
+ "shardField",
2940
+ "ngon",
2941
+ "superellipse",
2942
+ "blob"
2943
+ ],
2944
+ category: "procedural",
2945
+ heroCandidate: false,
2946
+ bestStyles: [
2947
+ "fill-and-stroke",
2948
+ "fill-only",
2949
+ "watercolor",
2950
+ "marble-vein"
2951
+ ]
2952
+ },
2953
+ crescent: {
2954
+ tier: 1,
2955
+ minSizeFraction: 0.1,
2956
+ maxSizeFraction: 1.0,
2957
+ affinities: [
2958
+ "circle",
2959
+ "blob",
2960
+ "cloudForm",
2961
+ "vesicaPiscis"
2962
+ ],
2963
+ category: "procedural",
2964
+ heroCandidate: true,
2965
+ bestStyles: [
2966
+ "fill-only",
2967
+ "watercolor",
2968
+ "fill-and-stroke"
2969
+ ]
2970
+ },
2971
+ tendril: {
2972
+ tier: 2,
2973
+ minSizeFraction: 0.1,
2974
+ maxSizeFraction: 0.8,
2975
+ affinities: [
2976
+ "blob",
2977
+ "inkSplat",
2978
+ "lissajous",
2979
+ "fibonacciSpiral"
2980
+ ],
2981
+ category: "procedural",
2982
+ heroCandidate: false,
2983
+ bestStyles: [
2984
+ "fill-only",
2985
+ "watercolor",
2986
+ "fill-and-stroke"
2987
+ ]
2988
+ },
2989
+ cloudForm: {
2990
+ tier: 1,
2991
+ minSizeFraction: 0.15,
2992
+ maxSizeFraction: 1.0,
2993
+ affinities: [
2994
+ "blob",
2995
+ "circle",
2996
+ "crescent",
2997
+ "superellipse"
2998
+ ],
2999
+ category: "procedural",
3000
+ heroCandidate: false,
3001
+ bestStyles: [
3002
+ "fill-only",
3003
+ "watercolor"
3004
+ ]
3005
+ },
3006
+ inkSplat: {
3007
+ tier: 2,
3008
+ minSizeFraction: 0.1,
3009
+ maxSizeFraction: 0.8,
3010
+ affinities: [
3011
+ "blob",
3012
+ "tendril",
3013
+ "shardField",
3014
+ "star"
3015
+ ],
3016
+ category: "procedural",
3017
+ heroCandidate: false,
3018
+ bestStyles: [
3019
+ "fill-only",
3020
+ "watercolor",
3021
+ "fill-and-stroke"
3022
+ ]
3023
+ },
3024
+ geodesicDome: {
3025
+ tier: 2,
3026
+ minSizeFraction: 0.2,
3027
+ maxSizeFraction: 0.9,
3028
+ affinities: [
3029
+ "metatronsCube",
3030
+ "platonicSolid",
3031
+ "hexagon",
3032
+ "triangle"
3033
+ ],
3034
+ category: "procedural",
3035
+ heroCandidate: true,
3036
+ bestStyles: [
3037
+ "stroke-only",
3038
+ "dashed",
3039
+ "double-stroke"
3040
+ ]
3041
+ },
3042
+ penroseTile: {
3043
+ tier: 2,
3044
+ minSizeFraction: 0.06,
3045
+ maxSizeFraction: 0.6,
3046
+ affinities: [
3047
+ "diamond",
3048
+ "triangle",
3049
+ "shardField",
3050
+ "voronoiCell"
3051
+ ],
3052
+ category: "procedural",
3053
+ heroCandidate: false,
3054
+ bestStyles: [
3055
+ "fill-and-stroke",
3056
+ "fill-only",
3057
+ "double-stroke"
3058
+ ]
3059
+ },
3060
+ reuleauxTriangle: {
3061
+ tier: 1,
3062
+ minSizeFraction: 0.08,
3063
+ maxSizeFraction: 1.0,
3064
+ affinities: [
3065
+ "triangle",
3066
+ "circle",
3067
+ "superellipse",
3068
+ "vesicaPiscis"
3069
+ ],
3070
+ category: "procedural",
3071
+ heroCandidate: true,
3072
+ bestStyles: [
3073
+ "fill-and-stroke",
3074
+ "fill-only",
3075
+ "watercolor"
3076
+ ]
3077
+ },
3078
+ dotCluster: {
3079
+ tier: 3,
3080
+ minSizeFraction: 0.05,
3081
+ maxSizeFraction: 0.5,
3082
+ affinities: [
3083
+ "cloudForm",
3084
+ "inkSplat",
3085
+ "blob"
3086
+ ],
3087
+ category: "procedural",
3088
+ heroCandidate: false,
3089
+ bestStyles: [
3090
+ "fill-only",
3091
+ "stipple"
3092
+ ]
3093
+ },
3094
+ crosshatchPatch: {
3095
+ tier: 3,
3096
+ minSizeFraction: 0.1,
3097
+ maxSizeFraction: 0.6,
3098
+ affinities: [
3099
+ "voronoiCell",
3100
+ "ngon",
3101
+ "superellipse"
3102
+ ],
3103
+ category: "procedural",
3104
+ heroCandidate: false,
3105
+ bestStyles: [
3106
+ "stroke-only",
3107
+ "hatched",
3108
+ "fabric-weave"
3109
+ ]
3110
+ }
3111
+ };
3112
+ function $e73976f898150d4d$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
3113
+ const available = shapeNames.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]);
3114
+ // Pick a seed shape — tier 1 shapes that are hero candidates
3115
+ const heroPool = available.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s].tier === 1 && $e73976f898150d4d$export$4343b39fe47bd82c[s].heroCandidate);
3116
+ const seedShape = heroPool.length > 0 ? heroPool[Math.floor(rng() * heroPool.length)] : available[Math.floor(rng() * available.length)];
3117
+ const seedProfile = $e73976f898150d4d$export$4343b39fe47bd82c[seedShape];
3118
+ // Primary: seed shape + its direct affinities (tier 1-2 only)
3119
+ const primaryCandidates = [
3120
+ seedShape,
3121
+ ...seedProfile.affinities
3122
+ ].filter((s)=>available.includes(s)).filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s].tier <= 2);
3123
+ const primary = [
3124
+ ...new Set(primaryCandidates)
3125
+ ].slice(0, 5);
3126
+ // Supporting: affinities of affinities, plus same-category shapes
3127
+ const supportingSet = new Set();
3128
+ for (const p of primary){
3129
+ const profile = $e73976f898150d4d$export$4343b39fe47bd82c[p];
3130
+ if (!profile) continue;
3131
+ for (const aff of profile.affinities)if (available.includes(aff) && !primary.includes(aff)) supportingSet.add(aff);
3132
+ }
3133
+ // Add same-category tier 1-2 shapes
3134
+ for (const s of available){
3135
+ const p = $e73976f898150d4d$export$4343b39fe47bd82c[s];
3136
+ if (p.category === seedProfile.category && p.tier <= 2 && !primary.includes(s)) supportingSet.add(s);
3137
+ }
3138
+ const supporting = [
3139
+ ...supportingSet
3140
+ ].slice(0, 6);
3141
+ // Accents: tier 1 shapes from other categories for contrast
3142
+ const usedCategories = new Set([
3143
+ ...primary,
3144
+ ...supporting
3145
+ ].map((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]?.category));
3146
+ const accentCandidates = available.filter((s)=>!primary.includes(s) && !supporting.includes(s) && $e73976f898150d4d$export$4343b39fe47bd82c[s].tier <= 2 && !usedCategories.has($e73976f898150d4d$export$4343b39fe47bd82c[s].category));
3147
+ // Shuffle and take a few
3148
+ const accents = [];
3149
+ const shuffled = [
3150
+ ...accentCandidates
3151
+ ];
3152
+ for(let i = shuffled.length - 1; i > 0; i--){
3153
+ const j = Math.floor(rng() * (i + 1));
3154
+ [shuffled[i], shuffled[j]] = [
3155
+ shuffled[j],
3156
+ shuffled[i]
3157
+ ];
3158
+ }
3159
+ accents.push(...shuffled.slice(0, 3));
3160
+ // For certain archetypes, bias the palette
3161
+ if (archetypeName === "geometric-precision") // Remove blobs and organic shapes from primary
3162
+ return {
3163
+ primary: primary.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]?.category !== "procedural" || s === "ngon"),
3164
+ supporting: supporting.filter((s)=>s !== "blob"),
3165
+ accents: accents
3166
+ };
3167
+ if (archetypeName === "organic-flow") {
3168
+ // Boost procedural/organic shapes
3169
+ const organicBoost = available.filter((s)=>[
3170
+ "blob",
3171
+ "superellipse",
3172
+ "waveRing",
3173
+ "rose"
3174
+ ].includes(s) && !primary.includes(s));
3175
+ return {
3176
+ primary: [
3177
+ ...primary,
3178
+ ...organicBoost.slice(0, 2)
3179
+ ],
3180
+ supporting: supporting,
3181
+ accents: accents
3182
+ };
3183
+ }
3184
+ if (archetypeName === "shattered-glass") {
3185
+ // Favor angular, fragmented shapes
3186
+ const shardBoost = available.filter((s)=>[
3187
+ "shardField",
3188
+ "voronoiCell",
3189
+ "penroseTile",
3190
+ "diamond",
3191
+ "triangle",
3192
+ "ngon"
3193
+ ].includes(s) && !primary.includes(s));
3194
+ return {
3195
+ primary: [
3196
+ ...primary.filter((s)=>s !== "blob" && s !== "cloudForm"),
3197
+ ...shardBoost.slice(0, 3)
3198
+ ],
3199
+ supporting: supporting.filter((s)=>s !== "blob" && s !== "cloudForm"),
3200
+ accents: accents
3201
+ };
3202
+ }
3203
+ if (archetypeName === "botanical") {
3204
+ // Favor organic, flowing shapes
3205
+ const botanicalBoost = available.filter((s)=>[
3206
+ "tendril",
3207
+ "cloudForm",
3208
+ "blob",
3209
+ "crescent",
3210
+ "rose",
3211
+ "inkSplat"
3212
+ ].includes(s) && !primary.includes(s));
3213
+ return {
3214
+ primary: [
3215
+ ...primary,
3216
+ ...botanicalBoost.slice(0, 3)
3217
+ ],
3218
+ supporting: supporting,
3219
+ accents: accents
3220
+ };
3221
+ }
3222
+ if (archetypeName === "stipple-portrait") {
3223
+ // Favor small, dot-friendly shapes
3224
+ const stippleBoost = available.filter((s)=>[
3225
+ "dotCluster",
3226
+ "circle",
3227
+ "crosshatchPatch",
3228
+ "voronoiCell",
3229
+ "blob"
3230
+ ].includes(s) && !primary.includes(s));
3231
+ return {
3232
+ primary: [
3233
+ ...primary,
3234
+ ...stippleBoost.slice(0, 3)
3235
+ ],
3236
+ supporting: supporting,
3237
+ accents: accents
3238
+ };
3239
+ }
3240
+ if (archetypeName === "celestial") {
3241
+ // Favor sacred geometry and cosmic shapes
3242
+ const celestialBoost = available.filter((s)=>[
3243
+ "crescent",
3244
+ "geodesicDome",
3245
+ "mandala",
3246
+ "flowerOfLife",
3247
+ "spirograph",
3248
+ "fibonacciSpiral"
3249
+ ].includes(s) && !primary.includes(s));
3250
+ return {
3251
+ primary: [
3252
+ ...primary,
3253
+ ...celestialBoost.slice(0, 3)
3254
+ ],
3255
+ supporting: supporting,
3256
+ accents: accents
3257
+ };
3258
+ }
3259
+ return {
3260
+ primary: primary,
3261
+ supporting: supporting,
3262
+ accents: accents
2372
3263
  };
2373
3264
  }
2374
3265
  function $e73976f898150d4d$export$3c37d9a045754d0e(palette, rng, sizeFraction) {
@@ -2702,10 +3593,138 @@ const $f89bc858f7202849$var$ARCHETYPES = [
2702
3593
  glowMultiplier: 1,
2703
3594
  sizePower: 1.8,
2704
3595
  invertForeground: false
3596
+ },
3597
+ {
3598
+ name: "shattered-glass",
3599
+ gridSize: 8,
3600
+ layers: 3,
3601
+ baseOpacity: 0.85,
3602
+ opacityReduction: 0.1,
3603
+ minShapeSize: 15,
3604
+ maxShapeSize: 250,
3605
+ backgroundStyle: "solid-dark",
3606
+ paletteMode: "high-contrast",
3607
+ preferredStyles: [
3608
+ "fill-and-stroke",
3609
+ "stroke-only",
3610
+ "fill-only"
3611
+ ],
3612
+ flowLineMultiplier: 0,
3613
+ heroShape: false,
3614
+ glowMultiplier: 0.3,
3615
+ sizePower: 1.0,
3616
+ invertForeground: false
3617
+ },
3618
+ {
3619
+ name: "botanical",
3620
+ gridSize: 4,
3621
+ layers: 4,
3622
+ baseOpacity: 0.5,
3623
+ opacityReduction: 0.06,
3624
+ minShapeSize: 30,
3625
+ maxShapeSize: 400,
3626
+ backgroundStyle: "radial-light",
3627
+ paletteMode: "earth",
3628
+ preferredStyles: [
3629
+ "watercolor",
3630
+ "fill-only",
3631
+ "incomplete"
3632
+ ],
3633
+ flowLineMultiplier: 3,
3634
+ heroShape: true,
3635
+ glowMultiplier: 0.2,
3636
+ sizePower: 1.6,
3637
+ invertForeground: false
3638
+ },
3639
+ {
3640
+ name: "stipple-portrait",
3641
+ gridSize: 9,
3642
+ layers: 2,
3643
+ baseOpacity: 0.8,
3644
+ opacityReduction: 0.05,
3645
+ minShapeSize: 5,
3646
+ maxShapeSize: 120,
3647
+ backgroundStyle: "solid-light",
3648
+ paletteMode: "monochrome",
3649
+ preferredStyles: [
3650
+ "stipple",
3651
+ "fill-only",
3652
+ "hatched"
3653
+ ],
3654
+ flowLineMultiplier: 0,
3655
+ heroShape: false,
3656
+ glowMultiplier: 0,
3657
+ sizePower: 2.8,
3658
+ invertForeground: false
3659
+ },
3660
+ {
3661
+ name: "celestial",
3662
+ gridSize: 7,
3663
+ layers: 5,
3664
+ baseOpacity: 0.45,
3665
+ opacityReduction: 0.04,
3666
+ minShapeSize: 8,
3667
+ maxShapeSize: 450,
3668
+ backgroundStyle: "radial-dark",
3669
+ paletteMode: "neon",
3670
+ preferredStyles: [
3671
+ "fill-only",
3672
+ "watercolor",
3673
+ "stroke-only",
3674
+ "incomplete"
3675
+ ],
3676
+ flowLineMultiplier: 2,
3677
+ heroShape: true,
3678
+ glowMultiplier: 2.5,
3679
+ sizePower: 2.2,
3680
+ invertForeground: false
2705
3681
  }
2706
3682
  ];
3683
+ /**
3684
+ * Linearly interpolate between two archetype numeric parameters.
3685
+ */ function $f89bc858f7202849$var$lerpNum(a, b, t) {
3686
+ return a + (b - a) * t;
3687
+ }
3688
+ /**
3689
+ * Blend two archetypes by interpolating their numeric parameters
3690
+ * and merging their style arrays.
3691
+ */ function $f89bc858f7202849$var$blendArchetypes(a, b, t) {
3692
+ // Merge preferred styles — unique union, primary archetype first
3693
+ const mergedStyles = [
3694
+ ...new Set([
3695
+ ...a.preferredStyles,
3696
+ ...b.preferredStyles
3697
+ ])
3698
+ ];
3699
+ return {
3700
+ name: `${a.name}+${b.name}`,
3701
+ gridSize: Math.round($f89bc858f7202849$var$lerpNum(a.gridSize, b.gridSize, t)),
3702
+ layers: Math.round($f89bc858f7202849$var$lerpNum(a.layers, b.layers, t)),
3703
+ baseOpacity: $f89bc858f7202849$var$lerpNum(a.baseOpacity, b.baseOpacity, t),
3704
+ opacityReduction: $f89bc858f7202849$var$lerpNum(a.opacityReduction, b.opacityReduction, t),
3705
+ minShapeSize: Math.round($f89bc858f7202849$var$lerpNum(a.minShapeSize, b.minShapeSize, t)),
3706
+ maxShapeSize: Math.round($f89bc858f7202849$var$lerpNum(a.maxShapeSize, b.maxShapeSize, t)),
3707
+ backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
3708
+ paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
3709
+ preferredStyles: mergedStyles,
3710
+ flowLineMultiplier: $f89bc858f7202849$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
3711
+ heroShape: t < 0.5 ? a.heroShape : b.heroShape,
3712
+ glowMultiplier: $f89bc858f7202849$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
3713
+ sizePower: $f89bc858f7202849$var$lerpNum(a.sizePower, b.sizePower, t),
3714
+ invertForeground: t < 0.5 ? a.invertForeground : b.invertForeground
3715
+ };
3716
+ }
2707
3717
  function $f89bc858f7202849$export$f1142fd7da4d6590(rng) {
2708
- return $f89bc858f7202849$var$ARCHETYPES[Math.floor(rng() * $f89bc858f7202849$var$ARCHETYPES.length)];
3718
+ const primary = $f89bc858f7202849$var$ARCHETYPES[Math.floor(rng() * $f89bc858f7202849$var$ARCHETYPES.length)];
3719
+ // ~15% chance of blending with a second archetype
3720
+ if (rng() < 0.15) {
3721
+ const secondary = $f89bc858f7202849$var$ARCHETYPES[Math.floor(rng() * $f89bc858f7202849$var$ARCHETYPES.length)];
3722
+ if (secondary.name !== primary.name) {
3723
+ const blendT = 0.25 + rng() * 0.25; // 25-50% blend toward secondary
3724
+ return $f89bc858f7202849$var$blendArchetypes(primary, secondary, blendT);
3725
+ }
3726
+ }
3727
+ return primary;
2709
3728
  }
2710
3729
 
2711
3730
 
@@ -2881,6 +3900,135 @@ function $4f72c5a314eddf25$var$drawBackground(ctx, style, bgStart, bgEnd, width,
2881
3900
  }
2882
3901
  }
2883
3902
  }
3903
+ const $4f72c5a314eddf25$var$CONSTELLATIONS = [
3904
+ {
3905
+ name: "flanked-triangle",
3906
+ build: (rng, baseSize)=>{
3907
+ const gap = baseSize * (0.6 + rng() * 0.3);
3908
+ return [
3909
+ {
3910
+ dx: 0,
3911
+ dy: 0,
3912
+ shape: "triangle",
3913
+ size: baseSize,
3914
+ rotation: rng() * 360
3915
+ },
3916
+ {
3917
+ dx: -gap,
3918
+ dy: gap * 0.3,
3919
+ shape: "circle",
3920
+ size: baseSize * 0.35,
3921
+ rotation: 0
3922
+ },
3923
+ {
3924
+ dx: gap,
3925
+ dy: gap * 0.3,
3926
+ shape: "circle",
3927
+ size: baseSize * 0.35,
3928
+ rotation: 0
3929
+ }
3930
+ ];
3931
+ }
3932
+ },
3933
+ {
3934
+ name: "hexagon-ring",
3935
+ build: (rng, baseSize)=>{
3936
+ const members = [];
3937
+ const count = 5 + Math.floor(rng() * 2);
3938
+ const ringR = baseSize * 0.6;
3939
+ for(let i = 0; i < count; i++){
3940
+ const angle = i / count * Math.PI * 2;
3941
+ members.push({
3942
+ dx: Math.cos(angle) * ringR,
3943
+ dy: Math.sin(angle) * ringR,
3944
+ shape: "hexagon",
3945
+ size: baseSize * (0.25 + rng() * 0.1),
3946
+ rotation: angle * 180 / Math.PI
3947
+ });
3948
+ }
3949
+ return members;
3950
+ }
3951
+ },
3952
+ {
3953
+ name: "spiral-dots",
3954
+ build: (rng, baseSize)=>{
3955
+ const members = [];
3956
+ const count = 7 + Math.floor(rng() * 5);
3957
+ const turns = 1.5 + rng();
3958
+ for(let i = 0; i < count; i++){
3959
+ const t = i / count;
3960
+ const angle = t * Math.PI * 2 * turns;
3961
+ const r = t * baseSize * 0.7;
3962
+ members.push({
3963
+ dx: Math.cos(angle) * r,
3964
+ dy: Math.sin(angle) * r,
3965
+ shape: "circle",
3966
+ size: baseSize * (0.08 + (1 - t) * 0.12),
3967
+ rotation: 0
3968
+ });
3969
+ }
3970
+ return members;
3971
+ }
3972
+ },
3973
+ {
3974
+ name: "diamond-cluster",
3975
+ build: (rng, baseSize)=>{
3976
+ const gap = baseSize * 0.45;
3977
+ return [
3978
+ {
3979
+ dx: 0,
3980
+ dy: -gap,
3981
+ shape: "diamond",
3982
+ size: baseSize * 0.4,
3983
+ rotation: 0
3984
+ },
3985
+ {
3986
+ dx: gap,
3987
+ dy: 0,
3988
+ shape: "diamond",
3989
+ size: baseSize * 0.35,
3990
+ rotation: 15
3991
+ },
3992
+ {
3993
+ dx: 0,
3994
+ dy: gap,
3995
+ shape: "diamond",
3996
+ size: baseSize * 0.3,
3997
+ rotation: 30
3998
+ },
3999
+ {
4000
+ dx: -gap,
4001
+ dy: 0,
4002
+ shape: "diamond",
4003
+ size: baseSize * 0.35,
4004
+ rotation: -15
4005
+ }
4006
+ ];
4007
+ }
4008
+ },
4009
+ {
4010
+ name: "crescent-pair",
4011
+ build: (rng, baseSize)=>{
4012
+ const gap = baseSize * 0.5;
4013
+ return [
4014
+ {
4015
+ dx: -gap * 0.4,
4016
+ dy: 0,
4017
+ shape: "crescent",
4018
+ size: baseSize * 0.5,
4019
+ rotation: rng() * 30
4020
+ },
4021
+ {
4022
+ dx: gap * 0.4,
4023
+ dy: 0,
4024
+ shape: "crescent",
4025
+ size: baseSize * 0.45,
4026
+ rotation: 180 + rng() * 30
4027
+ }
4028
+ ];
4029
+ }
4030
+ }
4031
+ ];
2884
4032
  function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2885
4033
  const finalConfig = {
2886
4034
  ...(0, $93cf69256c93baa9$export$c2f8e0cc249a8d8f),
@@ -2912,6 +4060,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2912
4060
  const colorGrade = (0, $d016ad53434219a1$export$6d1620b367f86f7a)(rng);
2913
4061
  // ── 0e. Light direction — consistent shadow angle ──────────────
2914
4062
  const lightAngle = rng() * Math.PI * 2;
4063
+ // ── 0f. Palette evolution — hue drift direction across layers ──
4064
+ const paletteHueShift = (rng() - 0.5) * 40; // -20° to +20° total drift
2915
4065
  const scaleFactor = Math.min(width, height) / 1024;
2916
4066
  const adjustedMinSize = minShapeSize * scaleFactor;
2917
4067
  const adjustedMaxSize = maxShapeSize * scaleFactor;
@@ -2967,6 +4117,65 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
2967
4117
  ctx.stroke();
2968
4118
  }
2969
4119
  ctx.globalCompositeOperation = "source-over";
4120
+ // ── 1c. Background pattern layer — subtle textured paper ───────
4121
+ const bgPatternRoll = rng();
4122
+ if (bgPatternRoll < 0.6) {
4123
+ ctx.save();
4124
+ ctx.globalCompositeOperation = "soft-light";
4125
+ const patternOpacity = 0.02 + rng() * 0.04;
4126
+ const patternColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4127
+ if (bgPatternRoll < 0.2) {
4128
+ // Dot grid
4129
+ const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4130
+ const dotR = dotSpacing * 0.08;
4131
+ ctx.globalAlpha = patternOpacity;
4132
+ ctx.fillStyle = patternColor;
4133
+ for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4134
+ ctx.beginPath();
4135
+ ctx.arc(px, py, dotR, 0, Math.PI * 2);
4136
+ ctx.fill();
4137
+ }
4138
+ } else if (bgPatternRoll < 0.4) {
4139
+ // Diagonal lines
4140
+ const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4141
+ ctx.globalAlpha = patternOpacity;
4142
+ ctx.strokeStyle = patternColor;
4143
+ ctx.lineWidth = 0.5 * scaleFactor;
4144
+ const diag = Math.hypot(width, height);
4145
+ for(let d = -diag; d < diag; d += lineSpacing){
4146
+ ctx.beginPath();
4147
+ ctx.moveTo(d, 0);
4148
+ ctx.lineTo(d + height, height);
4149
+ ctx.stroke();
4150
+ }
4151
+ } else {
4152
+ // Tessellation — hexagonal grid of tiny shapes
4153
+ const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4154
+ const tessH = tessSize * Math.sqrt(3);
4155
+ ctx.globalAlpha = patternOpacity * 0.7;
4156
+ ctx.strokeStyle = patternColor;
4157
+ ctx.lineWidth = 0.4 * scaleFactor;
4158
+ for(let row = 0; row * tessH < height + tessH; row++){
4159
+ const offsetX = row % 2 * tessSize * 0.75;
4160
+ for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4161
+ const hx = col * tessSize * 1.5 + offsetX;
4162
+ const hy = row * tessH;
4163
+ ctx.beginPath();
4164
+ for(let s = 0; s < 6; s++){
4165
+ const angle = Math.PI / 3 * s - Math.PI / 6;
4166
+ const vx = hx + Math.cos(angle) * tessSize * 0.5;
4167
+ const vy = hy + Math.sin(angle) * tessSize * 0.5;
4168
+ if (s === 0) ctx.moveTo(vx, vy);
4169
+ else ctx.lineTo(vx, vy);
4170
+ }
4171
+ ctx.closePath();
4172
+ ctx.stroke();
4173
+ }
4174
+ }
4175
+ }
4176
+ ctx.restore();
4177
+ }
4178
+ ctx.globalCompositeOperation = "source-over";
2970
4179
  // ── 2. Composition mode ────────────────────────────────────────
2971
4180
  const compositionMode = $4f72c5a314eddf25$var$COMPOSITION_MODES[Math.floor(rng() * $4f72c5a314eddf25$var$COMPOSITION_MODES.length)];
2972
4181
  const symRoll = rng();
@@ -3027,6 +4236,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3027
4236
  ry + (nearest.y - ry) * pull
3028
4237
  ];
3029
4238
  }
4239
+ // ── 3b. Void zone decoration — intentional negative space ────
4240
+ for (const zone of voidZones){
4241
+ // Subtle halo ring around void zones
4242
+ ctx.globalAlpha = 0.04 + rng() * 0.04;
4243
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
4244
+ ctx.lineWidth = 1.5 * scaleFactor;
4245
+ ctx.beginPath();
4246
+ ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
4247
+ ctx.stroke();
4248
+ // ~50% chance: scatter tiny dots inside the void
4249
+ if (rng() < 0.5) {
4250
+ const dotCount = 3 + Math.floor(rng() * 6);
4251
+ ctx.globalAlpha = 0.06 + rng() * 0.04;
4252
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
4253
+ for(let d = 0; d < dotCount; d++){
4254
+ const angle = rng() * Math.PI * 2;
4255
+ const dist = rng() * zone.radius * 0.7;
4256
+ const dotR = (1 + rng() * 3) * scaleFactor;
4257
+ ctx.beginPath();
4258
+ ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
4259
+ ctx.fill();
4260
+ }
4261
+ }
4262
+ // ~30% chance: thin concentric ring inside
4263
+ if (rng() < 0.3) {
4264
+ ctx.globalAlpha = 0.03 + rng() * 0.03;
4265
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4266
+ ctx.lineWidth = 0.5 * scaleFactor;
4267
+ const innerR = zone.radius * (0.4 + rng() * 0.3);
4268
+ ctx.beginPath();
4269
+ ctx.arc(zone.x, zone.y, innerR, 0, Math.PI * 2);
4270
+ ctx.stroke();
4271
+ }
4272
+ }
4273
+ ctx.globalAlpha = 1;
3030
4274
  // ── 4. Flow field seed values ──────────────────────────────────
3031
4275
  const fieldAngleBase = rng() * Math.PI * 2;
3032
4276
  const fieldFreq = 0.5 + rng() * 2;
@@ -3094,6 +4338,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3094
4338
  const layerRenderStyle = rng() < 0.6 ? archetype.preferredStyles[Math.floor(rng() * archetype.preferredStyles.length)] : (0, $c3de8257a8baa3b0$export$9fd4e64b2acd410e)(rng);
3095
4339
  // Atmospheric desaturation for later layers
3096
4340
  const atmosphericDesat = layerRatio * 0.3;
4341
+ // Depth-of-field simulation — later layers are "further away"
4342
+ // Reduce stroke widths and shift colors toward the background
4343
+ const dofFactor = 1 - layerRatio * 0.5; // 1.0 for front layer, 0.5 for back
4344
+ const dofStrokeScale = 0.4 + dofFactor * 0.6; // strokes thin out with depth
4345
+ const dofContrastReduction = layerRatio * 0.2; // colors fade toward bg
4346
+ // Color palette evolution — hue-rotate the hierarchy per layer
4347
+ const layerHierarchy = (0, $d016ad53434219a1$export$703ba40a4347f77a)(colorHierarchy, layerRatio, paletteHueShift);
4348
+ // Focal depth: shapes near focal points get more detail
4349
+ const focalDetailBoost = (px, py)=>{
4350
+ let minFocalDist = Infinity;
4351
+ for (const fp of focalPoints){
4352
+ const d = Math.hypot(px - fp.x, py - fp.y);
4353
+ if (d < minFocalDist) minFocalDist = d;
4354
+ }
4355
+ const maxDist = Math.hypot(width, height) * 0.5;
4356
+ return Math.max(0, 1 - minFocalDist / maxDist); // 1.0 at focal, 0.0 at edges
4357
+ };
3097
4358
  for(let i = 0; i < numShapes; i++){
3098
4359
  // Position from composition mode, then focal bias
3099
4360
  const rawPos = $4f72c5a314eddf25$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
@@ -3124,9 +4385,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3124
4385
  rotation = rotation + (angleToHero - rotation) * blendFactor * 0.4;
3125
4386
  }
3126
4387
  }
3127
- // Positional color from hierarchy + jitter
3128
- let fillBase = $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, colorHierarchy, rng);
3129
- const strokeBase = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4388
+ // Positional color from hierarchy + jitter (using evolved layer palette)
4389
+ let fillBase = $4f72c5a314eddf25$var$getPositionalColor(x, y, width, height, layerHierarchy, rng);
4390
+ const strokeBase = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(layerHierarchy, rng);
3130
4391
  // Desaturate colors on later layers for depth
3131
4392
  if (atmosphericDesat > 0) fillBase = (0, $d016ad53434219a1$export$fb75607d98509d9)(fillBase, atmosphericDesat);
3132
4393
  // Temperature contrast: shift foreground shapes opposite to background
@@ -3136,8 +4397,10 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3136
4397
  // Semi-transparent fill
3137
4398
  const fillAlpha = 0.2 + rng() * 0.5;
3138
4399
  const transparentFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, fillAlpha);
3139
- const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor;
3140
- ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5);
4400
+ const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor * dofStrokeScale;
4401
+ // Depth-of-field: reduce opacity slightly for distant layers
4402
+ const dofOpacityScale = 1 - dofContrastReduction;
4403
+ ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5) * dofOpacityScale;
3141
4404
  // Glow on sacred shapes more often — scaled by archetype
3142
4405
  const isSacred = $4f72c5a314eddf25$var$SACRED_SHAPES.includes(shape);
3143
4406
  const baseGlowChance = isSacred ? 0.45 : 0.2;
@@ -3156,7 +4419,48 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3156
4419
  const shadowDist = hasGlow ? 0 : size * 0.02;
3157
4420
  const shadowOffX = shadowDist * Math.cos(lightAngle);
3158
4421
  const shadowOffY = shadowDist * Math.sin(lightAngle);
3159
- (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
4422
+ // ── 5a. Tangent placement — nudge toward nearest shape edge ──
4423
+ let finalX = x;
4424
+ let finalY = y;
4425
+ if (shapePositions.length > 0 && rng() < 0.25) {
4426
+ // Find nearest placed shape
4427
+ let nearestDist = Infinity;
4428
+ let nearestPos = null;
4429
+ for (const sp of shapePositions){
4430
+ const d = Math.hypot(x - sp.x, y - sp.y);
4431
+ if (d < nearestDist && d > 0) {
4432
+ nearestDist = d;
4433
+ nearestPos = sp;
4434
+ }
4435
+ }
4436
+ if (nearestPos) {
4437
+ // Target distance: edges kissing (sum of half-sizes)
4438
+ const targetDist = (size + nearestPos.size) * 0.5;
4439
+ if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
4440
+ const angle = Math.atan2(y - nearestPos.y, x - nearestPos.x);
4441
+ finalX = nearestPos.x + Math.cos(angle) * targetDist;
4442
+ finalY = nearestPos.y + Math.sin(angle) * targetDist;
4443
+ // Keep in bounds
4444
+ finalX = Math.max(0, Math.min(width, finalX));
4445
+ finalY = Math.max(0, Math.min(height, finalY));
4446
+ }
4447
+ }
4448
+ }
4449
+ // ── 5b. Shape mirroring — basic shapes get reflected copies ──
4450
+ const mirrorAxis = (0, $c3de8257a8baa3b0$export$879206e23912d1a9)(rng);
4451
+ const isBasicShape = [
4452
+ "circle",
4453
+ "triangle",
4454
+ "square",
4455
+ "hexagon",
4456
+ "star",
4457
+ "diamond",
4458
+ "crescent",
4459
+ "penroseTile",
4460
+ "reuleauxTriangle"
4461
+ ].includes(shape);
4462
+ const shouldMirror = mirrorAxis !== null && isBasicShape && size > adjustedMaxSize * 0.2;
4463
+ const shapeConfig = {
3160
4464
  fillColor: transparentFill,
3161
4465
  strokeColor: strokeColor,
3162
4466
  strokeWidth: strokeWidth,
@@ -3168,22 +4472,47 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3168
4472
  gradientFillEnd: gradientEnd,
3169
4473
  renderStyle: finalRenderStyle,
3170
4474
  rng: rng
4475
+ };
4476
+ if (shouldMirror) (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4477
+ ...shapeConfig,
4478
+ mirrorAxis: mirrorAxis,
4479
+ mirrorGap: size * (0.1 + rng() * 0.3)
3171
4480
  });
4481
+ else (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
4482
+ // ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
4483
+ if (rng() < 0.2 && size > adjustedMinSize * 2) {
4484
+ const glazePasses = 2 + Math.floor(rng() * 2);
4485
+ for(let g = 0; g < glazePasses; g++){
4486
+ const glazeScale = 1 - (g + 1) * 0.12; // progressively smaller
4487
+ const glazeAlpha = 0.08 + g * 0.04; // progressively more opaque toward center
4488
+ ctx.globalAlpha = glazeAlpha;
4489
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
4490
+ fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
4491
+ strokeColor: "rgba(0,0,0,0)",
4492
+ strokeWidth: 0,
4493
+ size: size * glazeScale,
4494
+ rotation: rotation,
4495
+ proportionType: "GOLDEN_RATIO",
4496
+ renderStyle: "fill-only",
4497
+ rng: rng
4498
+ });
4499
+ }
4500
+ }
3172
4501
  shapePositions.push({
3173
- x: x,
3174
- y: y,
4502
+ x: finalX,
4503
+ y: finalY,
3175
4504
  size: size,
3176
4505
  shape: shape
3177
4506
  });
3178
- // ── 5b. Size echo — large shapes spawn trailing smaller copies ──
4507
+ // ── 5c. Size echo — large shapes spawn trailing smaller copies ──
3179
4508
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
3180
4509
  const echoCount = 2 + Math.floor(rng() * 2);
3181
4510
  const echoAngle = rng() * Math.PI * 2;
3182
4511
  for(let e = 0; e < echoCount; e++){
3183
4512
  const echoScale = 0.3 - e * 0.08;
3184
4513
  const echoDist = size * (0.6 + e * 0.4);
3185
- const echoX = x + Math.cos(echoAngle) * echoDist;
3186
- const echoY = y + Math.sin(echoAngle) * echoDist;
4514
+ const echoX = finalX + Math.cos(echoAngle) * echoDist;
4515
+ const echoY = finalY + Math.sin(echoAngle) * echoDist;
3187
4516
  const echoSize = size * Math.max(0.1, echoScale);
3188
4517
  if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
3189
4518
  ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
@@ -3205,8 +4534,11 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3205
4534
  });
3206
4535
  }
3207
4536
  }
3208
- // ── 5c. Recursive nesting ──────────────────────────────────
3209
- if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
4537
+ // ── 5d. Recursive nesting ──────────────────────────────────
4538
+ // Focal depth: shapes near focal points get more detail
4539
+ const focalProximity = focalDetailBoost(finalX, finalY);
4540
+ const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
4541
+ if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
3210
4542
  const innerCount = 1 + Math.floor(rng() * 3);
3211
4543
  for(let n = 0; n < innerCount; n++){
3212
4544
  // Pick inner shape from palette affinities
@@ -3218,7 +4550,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3218
4550
  const innerRot = rng() * 360;
3219
4551
  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);
3220
4552
  ctx.globalAlpha = layerOpacity * 0.7;
3221
- (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, innerShape, x + innerOffX, y + innerOffY, {
4553
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
3222
4554
  fillColor: innerFill,
3223
4555
  strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.5),
3224
4556
  strokeWidth: strokeWidth * 0.6,
@@ -3230,6 +4562,42 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3230
4562
  });
3231
4563
  }
3232
4564
  }
4565
+ // ── 5e. Shape constellations — pre-composed groups ─────────
4566
+ const constellationChance = 0.12 + focalProximity * 0.1; // 12-22% near focal
4567
+ if (size > adjustedMaxSize * 0.35 && rng() < constellationChance) {
4568
+ const constellation = $4f72c5a314eddf25$var$CONSTELLATIONS[Math.floor(rng() * $4f72c5a314eddf25$var$CONSTELLATIONS.length)];
4569
+ const members = constellation.build(rng, size);
4570
+ const groupRotation = rng() * Math.PI * 2;
4571
+ const cosR = Math.cos(groupRotation);
4572
+ const sinR = Math.sin(groupRotation);
4573
+ for (const member of members){
4574
+ // Rotate the group offset by the group rotation
4575
+ const mx = finalX + member.dx * cosR - member.dy * sinR;
4576
+ const my = finalY + member.dx * sinR + member.dy * cosR;
4577
+ if (mx < 0 || mx > width || my < 0 || my > height) continue;
4578
+ const memberFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
4579
+ const memberStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
4580
+ ctx.globalAlpha = layerOpacity * 0.6;
4581
+ // Use the member's shape if available, otherwise fall back to palette
4582
+ const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
4583
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
4584
+ fillColor: memberFill,
4585
+ strokeColor: memberStroke,
4586
+ strokeWidth: strokeWidth * 0.7,
4587
+ size: member.size,
4588
+ rotation: member.rotation + groupRotation * 180 / Math.PI,
4589
+ proportionType: "GOLDEN_RATIO",
4590
+ renderStyle: (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng),
4591
+ rng: rng
4592
+ });
4593
+ shapePositions.push({
4594
+ x: mx,
4595
+ y: my,
4596
+ size: member.size,
4597
+ shape: memberShape
4598
+ });
4599
+ }
4600
+ }
3233
4601
  }
3234
4602
  }
3235
4603
  // Reset blend mode for post-processing passes
@@ -3300,7 +4668,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3300
4668
  prevY = fy;
3301
4669
  }
3302
4670
  }
3303
- // ── 6b. Apply symmetry mirroring ─────────────────────────────────
4671
+ // ── 6b. Motion/energy lines short directional bursts ─────────
4672
+ const energyArchetypes = [
4673
+ "dense-chaotic",
4674
+ "cosmic",
4675
+ "neon-glow",
4676
+ "bold-graphic"
4677
+ ];
4678
+ const hasEnergyLines = energyArchetypes.some((a)=>archetype.name.includes(a)) || rng() < 0.25;
4679
+ if (hasEnergyLines && shapePositions.length > 0) {
4680
+ const energyCount = 5 + Math.floor(rng() * 10);
4681
+ ctx.lineCap = "round";
4682
+ for(let e = 0; e < energyCount; e++){
4683
+ // Pick a random shape to radiate from
4684
+ const source = shapePositions[Math.floor(rng() * shapePositions.length)];
4685
+ const burstCount = 2 + Math.floor(rng() * 4);
4686
+ const baseAngle = flowAngle(source.x, source.y);
4687
+ for(let b = 0; b < burstCount; b++){
4688
+ const angle = baseAngle + (rng() - 0.5) * 1.2;
4689
+ const lineLen = (source.size * 0.3 + rng() * source.size * 0.5) * scaleFactor * 0.3;
4690
+ const startDist = source.size * 0.5;
4691
+ const sx = source.x + Math.cos(angle) * startDist;
4692
+ const sy = source.y + Math.sin(angle) * startDist;
4693
+ const ex = sx + Math.cos(angle) * lineLen;
4694
+ const ey = sy + Math.sin(angle) * lineLen;
4695
+ ctx.globalAlpha = 0.04 + rng() * 0.06;
4696
+ ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
4697
+ ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
4698
+ ctx.beginPath();
4699
+ ctx.moveTo(sx, sy);
4700
+ ctx.lineTo(ex, ey);
4701
+ ctx.stroke();
4702
+ }
4703
+ }
4704
+ }
4705
+ // ── 6c. Apply symmetry mirroring ─────────────────────────────────
3304
4706
  if (symmetryMode !== "none") {
3305
4707
  const canvas = ctx.canvas;
3306
4708
  ctx.save();
@@ -3411,6 +4813,44 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
3411
4813
  ctx.restore();
3412
4814
  ctx.globalCompositeOperation = "source-over";
3413
4815
  }
4816
+ // ── 11. Signature mark — unique geometric chop from hash prefix ──
4817
+ {
4818
+ const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));
4819
+ const sigSize = Math.min(width, height) * 0.025;
4820
+ // Bottom-right corner with padding
4821
+ const sigX = width - sigSize * 2.5;
4822
+ const sigY = height - sigSize * 2.5;
4823
+ const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
4824
+ const sigColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
4825
+ ctx.save();
4826
+ ctx.globalAlpha = 0.12 + sigRng() * 0.08;
4827
+ ctx.translate(sigX, sigY);
4828
+ ctx.strokeStyle = sigColor;
4829
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.06);
4830
+ ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
4831
+ // Outer ring
4832
+ ctx.beginPath();
4833
+ ctx.arc(0, 0, sigSize, 0, Math.PI * 2);
4834
+ ctx.stroke();
4835
+ ctx.fill();
4836
+ // Inner geometric pattern — unique per hash
4837
+ ctx.beginPath();
4838
+ for(let s = 0; s < sigSegments; s++){
4839
+ const angle1 = sigRng() * Math.PI * 2;
4840
+ const angle2 = sigRng() * Math.PI * 2;
4841
+ const r1 = sigSize * (0.2 + sigRng() * 0.6);
4842
+ const r2 = sigSize * (0.2 + sigRng() * 0.6);
4843
+ ctx.moveTo(Math.cos(angle1) * r1, Math.sin(angle1) * r1);
4844
+ ctx.lineTo(Math.cos(angle2) * r2, Math.sin(angle2) * r2);
4845
+ }
4846
+ ctx.stroke();
4847
+ // Center dot
4848
+ ctx.beginPath();
4849
+ ctx.arc(0, 0, sigSize * 0.12, 0, Math.PI * 2);
4850
+ ctx.fillStyle = sigColor;
4851
+ ctx.fill();
4852
+ ctx.restore();
4853
+ }
3414
4854
  ctx.globalAlpha = 1;
3415
4855
  }
3416
4856