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