git-hash-art 0.7.0 → 0.9.0

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