git-hash-art 0.10.1 → 0.12.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/.github/workflows/deploy-www.yml +13 -3
- package/ALGORITHM.md +76 -24
- package/CHANGELOG.md +18 -0
- package/dist/browser.js +938 -251
- package/dist/browser.js.map +1 -1
- package/dist/main.js +940 -251
- package/dist/main.js.map +1 -1
- package/dist/module.js +940 -251
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/performance.test.ts +243 -0
- package/src/__tests__/phase-timing.test.ts +260 -0
- package/src/__tests__/profile-pipeline.test.ts +160 -0
- package/src/lib/archetypes.ts +29 -0
- package/src/lib/canvas/colors.ts +30 -11
- package/src/lib/canvas/draw.ts +147 -50
- package/src/lib/canvas/shapes/complex.ts +19 -10
- package/src/lib/canvas/shapes/sacred.ts +16 -17
- package/src/lib/render.ts +663 -204
package/dist/browser.js
CHANGED
|
@@ -508,13 +508,21 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
// ── Standalone color utilities ──────────────────────────────────────
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
511
|
+
// ── Cached hex→RGB parse — avoids repeated parseInt/substring on hot path ──
|
|
512
|
+
const $b5a262d09b87e373$var$_rgbCache = new Map();
|
|
513
|
+
const $b5a262d09b87e373$var$_RGB_CACHE_MAX = 512;
|
|
514
|
+
/** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. Cached. */ function $b5a262d09b87e373$var$hexToRgb(hex) {
|
|
515
|
+
let cached = $b5a262d09b87e373$var$_rgbCache.get(hex);
|
|
516
|
+
if (cached) return cached;
|
|
517
|
+
const c = hex.charAt(0) === "#" ? hex.substring(1) : hex;
|
|
518
|
+
cached = [
|
|
514
519
|
parseInt(c.substring(0, 2), 16),
|
|
515
520
|
parseInt(c.substring(2, 4), 16),
|
|
516
521
|
parseInt(c.substring(4, 6), 16)
|
|
517
522
|
];
|
|
523
|
+
if ($b5a262d09b87e373$var$_rgbCache.size >= $b5a262d09b87e373$var$_RGB_CACHE_MAX) $b5a262d09b87e373$var$_rgbCache.clear();
|
|
524
|
+
$b5a262d09b87e373$var$_rgbCache.set(hex, cached);
|
|
525
|
+
return cached;
|
|
518
526
|
}
|
|
519
527
|
/** Format [r, g, b] back to #RRGGBB. */ function $b5a262d09b87e373$var$rgbToHex(r, g, b) {
|
|
520
528
|
const clamp = (v)=>Math.max(0, Math.min(255, Math.round(v)));
|
|
@@ -571,7 +579,9 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
571
579
|
}
|
|
572
580
|
function $b5a262d09b87e373$export$f2121afcad3d553f(hex, alpha) {
|
|
573
581
|
const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex);
|
|
574
|
-
|
|
582
|
+
// Quantize alpha to 3 decimal places without toFixed overhead
|
|
583
|
+
const a = Math.round(alpha * 1000) / 1000;
|
|
584
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
575
585
|
}
|
|
576
586
|
function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
|
|
577
587
|
if (colors.length < 3) return {
|
|
@@ -580,15 +590,17 @@ function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
|
|
|
580
590
|
accent: colors[colors.length - 1] || "#888888",
|
|
581
591
|
all: colors
|
|
582
592
|
};
|
|
583
|
-
// Pick dominant as the color
|
|
593
|
+
// Pick dominant as the color with the highest chroma (saturation × distance from gray)
|
|
594
|
+
// This selects the most visually prominent color rather than the average
|
|
584
595
|
const hsls = colors.map((c)=>$b5a262d09b87e373$var$hexToHsl(c));
|
|
585
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
586
596
|
let dominantIdx = 0;
|
|
587
|
-
let
|
|
597
|
+
let maxChroma = -1;
|
|
588
598
|
for(let i = 0; i < hsls.length; i++){
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
599
|
+
// Chroma approximation: saturation × how far lightness is from 50% (gray)
|
|
600
|
+
const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
|
|
601
|
+
const chroma = hsls[i][1] * lightnessVibrancy;
|
|
602
|
+
if (chroma > maxChroma) {
|
|
603
|
+
maxChroma = chroma;
|
|
592
604
|
dominantIdx = i;
|
|
593
605
|
}
|
|
594
606
|
}
|
|
@@ -649,12 +661,21 @@ function $b5a262d09b87e373$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
|
649
661
|
const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
|
|
650
662
|
return $b5a262d09b87e373$var$hslToHex($b5a262d09b87e373$var$shiftHueToward(h, target, amount), s, l);
|
|
651
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Compute relative luminance of a hex color (0 = black, 1 = white).
|
|
666
|
+
* Uses the sRGB luminance formula from WCAG. Cached.
|
|
667
|
+
*/ const $b5a262d09b87e373$var$_lumCache = new Map();
|
|
652
668
|
function $b5a262d09b87e373$export$5c6e3c2b59b7fbbe(hex) {
|
|
669
|
+
let cached = $b5a262d09b87e373$var$_lumCache.get(hex);
|
|
670
|
+
if (cached !== undefined) return cached;
|
|
653
671
|
const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex).map((c)=>{
|
|
654
672
|
const s = c / 255;
|
|
655
673
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
656
674
|
});
|
|
657
|
-
|
|
675
|
+
cached = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
676
|
+
if ($b5a262d09b87e373$var$_lumCache.size >= 512) $b5a262d09b87e373$var$_lumCache.clear();
|
|
677
|
+
$b5a262d09b87e373$var$_lumCache.set(hex, cached);
|
|
678
|
+
return cached;
|
|
658
679
|
}
|
|
659
680
|
function $b5a262d09b87e373$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
|
|
660
681
|
const fgLum = $b5a262d09b87e373$export$5c6e3c2b59b7fbbe(fgHex);
|
|
@@ -1090,21 +1111,31 @@ const $f0f1a7293548e501$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
|
|
|
1090
1111
|
(0, $ce2c52df8af02e62$export$e46c5570db033611)(ctx, size, finalConfig);
|
|
1091
1112
|
const gridSize = 8;
|
|
1092
1113
|
const unit = size / gridSize;
|
|
1114
|
+
const radius = unit / 2;
|
|
1115
|
+
// Pre-compute the 8 star-point angle pairs (cos/sin) — avoids 648 trig calls
|
|
1116
|
+
const starPoints = [];
|
|
1117
|
+
for(let k = 0; k < 8; k++){
|
|
1118
|
+
const angle = Math.PI / 4 * k;
|
|
1119
|
+
const angle2 = angle + Math.PI / 4;
|
|
1120
|
+
starPoints.push({
|
|
1121
|
+
c1: Math.cos(angle) * radius,
|
|
1122
|
+
s1: Math.sin(angle) * radius,
|
|
1123
|
+
c2: Math.cos(angle2) * radius,
|
|
1124
|
+
s2: Math.sin(angle2) * radius
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1093
1127
|
ctx.beginPath();
|
|
1094
1128
|
// Create base grid
|
|
1095
|
-
for(let i = 0; i <= gridSize; i++)
|
|
1129
|
+
for(let i = 0; i <= gridSize; i++){
|
|
1096
1130
|
const x = (i - gridSize / 2) * unit;
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const y2 = y + radius * Math.sin(angle + Math.PI / 4);
|
|
1106
|
-
ctx.moveTo(x1, y1);
|
|
1107
|
-
ctx.lineTo(x2, y2);
|
|
1131
|
+
for(let j = 0; j <= gridSize; j++){
|
|
1132
|
+
const y = (j - gridSize / 2) * unit;
|
|
1133
|
+
// Draw star pattern at each intersection using pre-computed offsets
|
|
1134
|
+
for(let k = 0; k < 8; k++){
|
|
1135
|
+
const sp = starPoints[k];
|
|
1136
|
+
ctx.moveTo(x + sp.c1, y + sp.s1);
|
|
1137
|
+
ctx.lineTo(x + sp.c2, y + sp.s2);
|
|
1138
|
+
}
|
|
1108
1139
|
}
|
|
1109
1140
|
}
|
|
1110
1141
|
ctx.stroke();
|
|
@@ -1424,20 +1455,23 @@ const $77711f013715e6da$export$eeae7765f05012e2 = (ctx, size)=>{
|
|
|
1424
1455
|
const $77711f013715e6da$export$3355220a8108efc3 = (ctx, size)=>{
|
|
1425
1456
|
const outerRadius = size / 2;
|
|
1426
1457
|
const innerRadius = size / 4;
|
|
1427
|
-
|
|
1458
|
+
// Adaptive step count: fewer segments for small shapes where detail isn't visible.
|
|
1459
|
+
// 36×36 = 1296 segments at full size; at size < 60 we drop to 16×16 = 256.
|
|
1460
|
+
const steps = size < 60 ? 16 : size < 150 ? 24 : 36;
|
|
1461
|
+
const TWO_PI = Math.PI * 2;
|
|
1462
|
+
const angleStep = TWO_PI / steps;
|
|
1428
1463
|
ctx.beginPath();
|
|
1429
1464
|
for(let i = 0; i < steps; i++){
|
|
1430
|
-
const angle1 = i
|
|
1431
|
-
|
|
1465
|
+
const angle1 = i * angleStep;
|
|
1466
|
+
const cosA = Math.cos(angle1);
|
|
1467
|
+
const sinA = Math.sin(angle1);
|
|
1432
1468
|
for(let j = 0; j < steps; j++){
|
|
1433
|
-
const phi1 = j
|
|
1434
|
-
const phi2 =
|
|
1435
|
-
const
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
ctx.moveTo(x1, y1);
|
|
1440
|
-
ctx.lineTo(x2, y2);
|
|
1469
|
+
const phi1 = j * angleStep;
|
|
1470
|
+
const phi2 = phi1 + angleStep;
|
|
1471
|
+
const r1 = outerRadius + innerRadius * Math.cos(phi1);
|
|
1472
|
+
const r2 = outerRadius + innerRadius * Math.cos(phi2);
|
|
1473
|
+
ctx.moveTo(r1 * cosA, r1 * sinA);
|
|
1474
|
+
ctx.lineTo(r2 * cosA, r2 * sinA);
|
|
1441
1475
|
}
|
|
1442
1476
|
}
|
|
1443
1477
|
};
|
|
@@ -2000,6 +2034,43 @@ const $e0f99502ff383dd8$var$RENDER_STYLES = [
|
|
|
2000
2034
|
function $e0f99502ff383dd8$export$9fd4e64b2acd410e(rng) {
|
|
2001
2035
|
return $e0f99502ff383dd8$var$RENDER_STYLES[Math.floor(rng() * $e0f99502ff383dd8$var$RENDER_STYLES.length)];
|
|
2002
2036
|
}
|
|
2037
|
+
const $e0f99502ff383dd8$export$2f738f61a8c15e07 = {
|
|
2038
|
+
"fill-and-stroke": 1,
|
|
2039
|
+
"fill-only": 0.5,
|
|
2040
|
+
"stroke-only": 1,
|
|
2041
|
+
"double-stroke": 1.5,
|
|
2042
|
+
"dashed": 1,
|
|
2043
|
+
"watercolor": 7,
|
|
2044
|
+
"hatched": 3,
|
|
2045
|
+
"incomplete": 1,
|
|
2046
|
+
"stipple": 90,
|
|
2047
|
+
"stencil": 2,
|
|
2048
|
+
"noise-grain": 400,
|
|
2049
|
+
"wood-grain": 10,
|
|
2050
|
+
"marble-vein": 4,
|
|
2051
|
+
"fabric-weave": 6,
|
|
2052
|
+
"hand-drawn": 5
|
|
2053
|
+
};
|
|
2054
|
+
function $e0f99502ff383dd8$export$909ab0580e273f19(style) {
|
|
2055
|
+
switch(style){
|
|
2056
|
+
case "noise-grain":
|
|
2057
|
+
return "hatched";
|
|
2058
|
+
case "stipple":
|
|
2059
|
+
return "dashed";
|
|
2060
|
+
case "wood-grain":
|
|
2061
|
+
return "hatched";
|
|
2062
|
+
case "watercolor":
|
|
2063
|
+
return "fill-and-stroke";
|
|
2064
|
+
case "fabric-weave":
|
|
2065
|
+
return "hatched";
|
|
2066
|
+
case "hand-drawn":
|
|
2067
|
+
return "fill-and-stroke";
|
|
2068
|
+
case "marble-vein":
|
|
2069
|
+
return "stroke-only";
|
|
2070
|
+
default:
|
|
2071
|
+
return style;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2003
2074
|
function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
2004
2075
|
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
|
|
2005
2076
|
ctx.save();
|
|
@@ -2119,6 +2190,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2119
2190
|
case "hatched":
|
|
2120
2191
|
{
|
|
2121
2192
|
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
2193
|
+
// Optimized: batch all parallel lines into a single path per pass
|
|
2122
2194
|
const savedAlphaH = ctx.globalAlpha;
|
|
2123
2195
|
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
2124
2196
|
ctx.fill();
|
|
@@ -2130,28 +2202,28 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2130
2202
|
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
2131
2203
|
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
2132
2204
|
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
2133
|
-
// Draw parallel lines across the bounding box
|
|
2205
|
+
// Draw parallel lines across the bounding box — batched into single path
|
|
2134
2206
|
const extent = size * 0.8;
|
|
2135
2207
|
const cos = Math.cos(hatchAngle);
|
|
2136
2208
|
const sin = Math.sin(hatchAngle);
|
|
2209
|
+
ctx.beginPath();
|
|
2137
2210
|
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
2138
|
-
ctx.beginPath();
|
|
2139
2211
|
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
2140
2212
|
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
2141
|
-
ctx.stroke();
|
|
2142
2213
|
}
|
|
2214
|
+
ctx.stroke();
|
|
2143
2215
|
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
2144
2216
|
if (!rng || rng() < 0.5) {
|
|
2145
2217
|
const crossAngle = hatchAngle + Math.PI / 2;
|
|
2146
2218
|
const cos2 = Math.cos(crossAngle);
|
|
2147
2219
|
const sin2 = Math.sin(crossAngle);
|
|
2148
2220
|
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
2221
|
+
ctx.beginPath();
|
|
2149
2222
|
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
2150
|
-
ctx.beginPath();
|
|
2151
2223
|
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
2152
2224
|
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
2153
|
-
ctx.stroke();
|
|
2154
2225
|
}
|
|
2226
|
+
ctx.stroke();
|
|
2155
2227
|
}
|
|
2156
2228
|
ctx.restore();
|
|
2157
2229
|
ctx.globalAlpha = savedAlphaH;
|
|
@@ -2189,6 +2261,8 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2189
2261
|
case "stipple":
|
|
2190
2262
|
{
|
|
2191
2263
|
// Dot-fill texture — clip to shape, then scatter dots
|
|
2264
|
+
// Optimized: use fillRect instead of arc for dots (much cheaper to render),
|
|
2265
|
+
// and cap total dot count to avoid O(size²) blowup on large shapes.
|
|
2192
2266
|
const savedAlphaS = ctx.globalAlpha;
|
|
2193
2267
|
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
2194
2268
|
ctx.fill(); // ghost fill
|
|
@@ -2196,16 +2270,20 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2196
2270
|
ctx.save();
|
|
2197
2271
|
ctx.clip();
|
|
2198
2272
|
const dotSpacing = Math.max(2, size * 0.03);
|
|
2199
|
-
const
|
|
2273
|
+
const extentS = size * 0.55;
|
|
2274
|
+
// Cap total dots: beyond ~900 (30×30 grid) the visual density plateaus
|
|
2275
|
+
const maxDotsPerAxis = Math.min(Math.ceil(extentS * 2 / dotSpacing), 30);
|
|
2276
|
+
const actualSpacing = extentS * 2 / maxDotsPerAxis;
|
|
2200
2277
|
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
2201
|
-
for(let
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2278
|
+
for(let xi = 0; xi < maxDotsPerAxis; xi++){
|
|
2279
|
+
const dx = -extentS + xi * actualSpacing;
|
|
2280
|
+
for(let yi = 0; yi < maxDotsPerAxis; yi++){
|
|
2281
|
+
const dy = -extentS + yi * actualSpacing;
|
|
2282
|
+
const jx = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2283
|
+
const jy = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2284
|
+
const dotD = rng ? actualSpacing * (0.3 + rng() * 0.4) : actualSpacing * 0.4;
|
|
2285
|
+
ctx.fillRect(dx + jx - dotD * 0.5, dy + jy - dotD * 0.5, dotD, dotD);
|
|
2286
|
+
}
|
|
2209
2287
|
}
|
|
2210
2288
|
ctx.restore();
|
|
2211
2289
|
ctx.globalAlpha = savedAlphaS;
|
|
@@ -2238,6 +2316,9 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2238
2316
|
case "noise-grain":
|
|
2239
2317
|
{
|
|
2240
2318
|
// Procedural noise grain texture clipped to shape boundary
|
|
2319
|
+
// Optimized: cap grid to max 40×40 = 1600 dots (was unbounded at O(size²)),
|
|
2320
|
+
// quantize alpha into buckets to minimize globalAlpha state changes,
|
|
2321
|
+
// and batch dots by brightness (black/white) × alpha bucket
|
|
2241
2322
|
const savedAlphaN = ctx.globalAlpha;
|
|
2242
2323
|
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
2243
2324
|
ctx.fill(); // base tint
|
|
@@ -2246,17 +2327,47 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2246
2327
|
ctx.clip();
|
|
2247
2328
|
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
2248
2329
|
const extentN = size * 0.55;
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
const
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2330
|
+
if (rng) {
|
|
2331
|
+
// Cap grid to max 40 dots per axis — beyond this the grain is
|
|
2332
|
+
// visually indistinguishable but cost scales quadratically.
|
|
2333
|
+
const maxGrainPerAxis = Math.min(Math.ceil(extentN * 2 / grainSpacing), 40);
|
|
2334
|
+
const actualGrainSpacing = extentN * 2 / maxGrainPerAxis;
|
|
2335
|
+
// 4 alpha buckets: 0.2, 0.3, 0.4, 0.5 — covers the 0.15-0.50 range
|
|
2336
|
+
const BUCKETS = 4;
|
|
2337
|
+
const bucketMin = 0.15;
|
|
2338
|
+
const bucketRange = 0.35;
|
|
2339
|
+
// [black_bucket0, black_bucket1, ..., white_bucket0, ...]
|
|
2340
|
+
const buckets = [];
|
|
2341
|
+
for(let i = 0; i < BUCKETS * 2; i++)buckets.push([]);
|
|
2342
|
+
for(let xi = 0; xi < maxGrainPerAxis; xi++){
|
|
2343
|
+
const gx = -extentN + xi * actualGrainSpacing;
|
|
2344
|
+
for(let yi = 0; yi < maxGrainPerAxis; yi++){
|
|
2345
|
+
const gy = -extentN + yi * actualGrainSpacing;
|
|
2346
|
+
const jx = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2347
|
+
const jy = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2348
|
+
const isWhite = rng() > 0.5;
|
|
2349
|
+
const dotAlpha = bucketMin + rng() * bucketRange;
|
|
2350
|
+
const dotSize = actualGrainSpacing * (0.3 + rng() * 0.5);
|
|
2351
|
+
const bucketIdx = Math.min(BUCKETS - 1, Math.floor((dotAlpha - bucketMin) / bucketRange * BUCKETS));
|
|
2352
|
+
const offset = isWhite ? BUCKETS : 0;
|
|
2353
|
+
buckets[offset + bucketIdx].push({
|
|
2354
|
+
x: gx + jx,
|
|
2355
|
+
y: gy + jy,
|
|
2356
|
+
s: dotSize
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
// Render each bucket: 2 colors × 4 alpha levels = 8 state changes total
|
|
2361
|
+
for(let color = 0; color < 2; color++){
|
|
2362
|
+
ctx.fillStyle = color === 0 ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
2363
|
+
for(let b = 0; b < BUCKETS; b++){
|
|
2364
|
+
const dots = buckets[color * BUCKETS + b];
|
|
2365
|
+
if (dots.length === 0) continue;
|
|
2366
|
+
const alpha = bucketMin + (b + 0.5) / BUCKETS * bucketRange;
|
|
2367
|
+
ctx.globalAlpha = savedAlphaN * alpha;
|
|
2368
|
+
for(let i = 0; i < dots.length; i++)ctx.fillRect(dots[i].x, dots[i].y, dots[i].s, dots[i].s);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2260
2371
|
}
|
|
2261
2372
|
ctx.restore();
|
|
2262
2373
|
ctx.fillStyle = fillColor;
|
|
@@ -2269,6 +2380,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2269
2380
|
case "wood-grain":
|
|
2270
2381
|
{
|
|
2271
2382
|
// Parallel wavy lines simulating wood grain, clipped to shape
|
|
2383
|
+
// Optimized: batch all grain lines into a single path, increased step from 2 to 4
|
|
2272
2384
|
const savedAlphaW = ctx.globalAlpha;
|
|
2273
2385
|
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
2274
2386
|
ctx.fill(); // base tint
|
|
@@ -2284,17 +2396,19 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2284
2396
|
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
2285
2397
|
const cosG = Math.cos(grainAngle);
|
|
2286
2398
|
const sinG = Math.sin(grainAngle);
|
|
2399
|
+
const waveCoeff = waveFreq * Math.PI;
|
|
2400
|
+
const invExtentW = 1 / extentW;
|
|
2401
|
+
// Batch all grain lines into a single path
|
|
2402
|
+
ctx.beginPath();
|
|
2287
2403
|
for(let d = -extentW; d <= extentW; d += grainLineSpacing){
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
const
|
|
2292
|
-
|
|
2293
|
-
if (t === -extentW) ctx.moveTo(px, py);
|
|
2294
|
-
else ctx.lineTo(px, py);
|
|
2404
|
+
const firstWave = Math.sin(-extentW * invExtentW * waveCoeff) * waveAmp;
|
|
2405
|
+
ctx.moveTo(-extentW * cosG - (d + firstWave) * sinG, -extentW * sinG + (d + firstWave) * cosG);
|
|
2406
|
+
for(let t = -extentW + 4; t <= extentW; t += 4){
|
|
2407
|
+
const wave = Math.sin(t * invExtentW * waveCoeff) * waveAmp;
|
|
2408
|
+
ctx.lineTo(t * cosG - (d + wave) * sinG, t * sinG + (d + wave) * cosG);
|
|
2295
2409
|
}
|
|
2296
|
-
ctx.stroke();
|
|
2297
2410
|
}
|
|
2411
|
+
ctx.stroke();
|
|
2298
2412
|
ctx.restore();
|
|
2299
2413
|
ctx.globalAlpha = savedAlphaW;
|
|
2300
2414
|
ctx.globalAlpha *= 0.35;
|
|
@@ -2356,6 +2470,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2356
2470
|
case "fabric-weave":
|
|
2357
2471
|
{
|
|
2358
2472
|
// Interlocking horizontal/vertical threads clipped to shape
|
|
2473
|
+
// Optimized: batch all horizontal threads into one path, all vertical into another
|
|
2359
2474
|
const savedAlphaF = ctx.globalAlpha;
|
|
2360
2475
|
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
2361
2476
|
ctx.fill(); // ghost base
|
|
@@ -2365,26 +2480,24 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2365
2480
|
const threadSpacing = Math.max(2, size * 0.04);
|
|
2366
2481
|
const extentF = size * 0.55;
|
|
2367
2482
|
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
2483
|
+
// Horizontal threads — batched
|
|
2368
2484
|
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
2369
|
-
|
|
2485
|
+
ctx.beginPath();
|
|
2370
2486
|
for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2371
|
-
ctx.beginPath();
|
|
2372
2487
|
ctx.moveTo(-extentF, y);
|
|
2373
2488
|
ctx.lineTo(extentF, y);
|
|
2374
|
-
ctx.stroke();
|
|
2375
2489
|
}
|
|
2376
|
-
|
|
2490
|
+
ctx.stroke();
|
|
2491
|
+
// Vertical threads (offset by half spacing for weave effect) — batched
|
|
2377
2492
|
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
2378
2493
|
ctx.strokeStyle = fillColor;
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
ctx.lineTo(x, y + threadSpacing);
|
|
2385
|
-
}
|
|
2386
|
-
ctx.stroke();
|
|
2494
|
+
ctx.beginPath();
|
|
2495
|
+
for(let x = -extentF; x <= extentF; x += threadSpacing * 2)for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2496
|
+
// Over-under: draw segment, skip segment
|
|
2497
|
+
ctx.moveTo(x, y);
|
|
2498
|
+
ctx.lineTo(x, y + threadSpacing);
|
|
2387
2499
|
}
|
|
2500
|
+
ctx.stroke();
|
|
2388
2501
|
ctx.strokeStyle = strokeColor;
|
|
2389
2502
|
ctx.restore();
|
|
2390
2503
|
ctx.globalAlpha = savedAlphaF;
|
|
@@ -2450,14 +2563,17 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2450
2563
|
ctx.translate(x, y);
|
|
2451
2564
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2452
2565
|
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2453
|
-
|
|
2566
|
+
// Skip shadow entirely for small shapes (< 20px) — the blur is expensive
|
|
2567
|
+
// and visually imperceptible at that scale.
|
|
2568
|
+
const useShadow = size >= 20;
|
|
2569
|
+
if (useShadow && lightAngle !== undefined) {
|
|
2454
2570
|
const shadowDist = size * 0.035;
|
|
2455
2571
|
const shadowBlurR = size * 0.06;
|
|
2456
2572
|
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2457
2573
|
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2458
2574
|
ctx.shadowBlur = shadowBlurR;
|
|
2459
2575
|
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2460
|
-
} else if (glowRadius > 0) {
|
|
2576
|
+
} else if (useShadow && glowRadius > 0) {
|
|
2461
2577
|
// Glow / shadow effect (legacy path)
|
|
2462
2578
|
ctx.shadowBlur = glowRadius;
|
|
2463
2579
|
ctx.shadowColor = glowColor || fillColor;
|
|
@@ -2481,17 +2597,24 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2481
2597
|
$e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2482
2598
|
}
|
|
2483
2599
|
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2600
|
+
// Only reset if we actually set shadow (avoids unnecessary state changes)
|
|
2601
|
+
if (useShadow && (lightAngle !== undefined || glowRadius > 0)) {
|
|
2602
|
+
ctx.shadowBlur = 0;
|
|
2603
|
+
ctx.shadowOffsetX = 0;
|
|
2604
|
+
ctx.shadowOffsetY = 0;
|
|
2605
|
+
ctx.shadowColor = "transparent";
|
|
2606
|
+
}
|
|
2607
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2608
|
+
// Skip for small shapes (< 30px) — gradient creation + composite op
|
|
2609
|
+
// switch is expensive and the highlight is invisible at small sizes.
|
|
2610
|
+
if (lightAngle !== undefined && size > 30 && rng) {
|
|
2490
2611
|
const hlRadius = size * 0.35;
|
|
2491
2612
|
const hlDist = size * 0.15;
|
|
2492
2613
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2493
2614
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2494
2615
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2616
|
+
// Use a simple white highlight — the per-shape hex parse was expensive
|
|
2617
|
+
// and the visual difference from tinted highlights is negligible.
|
|
2495
2618
|
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2496
2619
|
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2497
2620
|
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
@@ -3556,6 +3679,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3556
3679
|
"watercolor",
|
|
3557
3680
|
"fill-only"
|
|
3558
3681
|
],
|
|
3682
|
+
preferredCompositions: [
|
|
3683
|
+
"clustered",
|
|
3684
|
+
"flow-field",
|
|
3685
|
+
"radial"
|
|
3686
|
+
],
|
|
3559
3687
|
flowLineMultiplier: 2.5,
|
|
3560
3688
|
heroShape: false,
|
|
3561
3689
|
glowMultiplier: 0.5,
|
|
@@ -3577,6 +3705,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3577
3705
|
"stroke-only",
|
|
3578
3706
|
"incomplete"
|
|
3579
3707
|
],
|
|
3708
|
+
preferredCompositions: [
|
|
3709
|
+
"golden-spiral",
|
|
3710
|
+
"grid-subdivision"
|
|
3711
|
+
],
|
|
3580
3712
|
flowLineMultiplier: 0.3,
|
|
3581
3713
|
heroShape: true,
|
|
3582
3714
|
glowMultiplier: 0,
|
|
@@ -3598,6 +3730,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3598
3730
|
"fill-only",
|
|
3599
3731
|
"incomplete"
|
|
3600
3732
|
],
|
|
3733
|
+
preferredCompositions: [
|
|
3734
|
+
"flow-field",
|
|
3735
|
+
"golden-spiral",
|
|
3736
|
+
"spiral"
|
|
3737
|
+
],
|
|
3601
3738
|
flowLineMultiplier: 4,
|
|
3602
3739
|
heroShape: false,
|
|
3603
3740
|
glowMultiplier: 0.3,
|
|
@@ -3620,6 +3757,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3620
3757
|
"double-stroke",
|
|
3621
3758
|
"hatched"
|
|
3622
3759
|
],
|
|
3760
|
+
preferredCompositions: [
|
|
3761
|
+
"grid-subdivision",
|
|
3762
|
+
"radial"
|
|
3763
|
+
],
|
|
3623
3764
|
flowLineMultiplier: 0,
|
|
3624
3765
|
heroShape: false,
|
|
3625
3766
|
glowMultiplier: 0,
|
|
@@ -3641,6 +3782,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3641
3782
|
"incomplete",
|
|
3642
3783
|
"fill-only"
|
|
3643
3784
|
],
|
|
3785
|
+
preferredCompositions: [
|
|
3786
|
+
"golden-spiral",
|
|
3787
|
+
"radial",
|
|
3788
|
+
"spiral"
|
|
3789
|
+
],
|
|
3644
3790
|
flowLineMultiplier: 1.5,
|
|
3645
3791
|
heroShape: true,
|
|
3646
3792
|
glowMultiplier: 2,
|
|
@@ -3661,6 +3807,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3661
3807
|
"fill-and-stroke",
|
|
3662
3808
|
"double-stroke"
|
|
3663
3809
|
],
|
|
3810
|
+
preferredCompositions: [
|
|
3811
|
+
"grid-subdivision",
|
|
3812
|
+
"golden-spiral"
|
|
3813
|
+
],
|
|
3664
3814
|
flowLineMultiplier: 0,
|
|
3665
3815
|
heroShape: true,
|
|
3666
3816
|
glowMultiplier: 0,
|
|
@@ -3682,6 +3832,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3682
3832
|
"double-stroke",
|
|
3683
3833
|
"dashed"
|
|
3684
3834
|
],
|
|
3835
|
+
preferredCompositions: [
|
|
3836
|
+
"radial",
|
|
3837
|
+
"spiral",
|
|
3838
|
+
"clustered"
|
|
3839
|
+
],
|
|
3685
3840
|
flowLineMultiplier: 2,
|
|
3686
3841
|
heroShape: true,
|
|
3687
3842
|
glowMultiplier: 3,
|
|
@@ -3704,6 +3859,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3704
3859
|
"stroke-only",
|
|
3705
3860
|
"dashed"
|
|
3706
3861
|
],
|
|
3862
|
+
preferredCompositions: [
|
|
3863
|
+
"flow-field",
|
|
3864
|
+
"grid-subdivision",
|
|
3865
|
+
"clustered"
|
|
3866
|
+
],
|
|
3707
3867
|
flowLineMultiplier: 1.5,
|
|
3708
3868
|
heroShape: false,
|
|
3709
3869
|
glowMultiplier: 0,
|
|
@@ -3725,6 +3885,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3725
3885
|
"watercolor",
|
|
3726
3886
|
"fill-and-stroke"
|
|
3727
3887
|
],
|
|
3888
|
+
preferredCompositions: [
|
|
3889
|
+
"radial",
|
|
3890
|
+
"spiral",
|
|
3891
|
+
"golden-spiral"
|
|
3892
|
+
],
|
|
3728
3893
|
flowLineMultiplier: 3,
|
|
3729
3894
|
heroShape: true,
|
|
3730
3895
|
glowMultiplier: 2.5,
|
|
@@ -3746,6 +3911,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3746
3911
|
"fill-only",
|
|
3747
3912
|
"incomplete"
|
|
3748
3913
|
],
|
|
3914
|
+
preferredCompositions: [
|
|
3915
|
+
"golden-spiral",
|
|
3916
|
+
"flow-field",
|
|
3917
|
+
"radial"
|
|
3918
|
+
],
|
|
3749
3919
|
flowLineMultiplier: 0.5,
|
|
3750
3920
|
heroShape: false,
|
|
3751
3921
|
glowMultiplier: 0.3,
|
|
@@ -3767,6 +3937,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3767
3937
|
"stroke-only",
|
|
3768
3938
|
"dashed"
|
|
3769
3939
|
],
|
|
3940
|
+
preferredCompositions: [
|
|
3941
|
+
"grid-subdivision",
|
|
3942
|
+
"radial"
|
|
3943
|
+
],
|
|
3770
3944
|
flowLineMultiplier: 0,
|
|
3771
3945
|
heroShape: false,
|
|
3772
3946
|
glowMultiplier: 0,
|
|
@@ -3788,6 +3962,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3788
3962
|
"fill-only",
|
|
3789
3963
|
"double-stroke"
|
|
3790
3964
|
],
|
|
3965
|
+
preferredCompositions: [
|
|
3966
|
+
"grid-subdivision",
|
|
3967
|
+
"clustered"
|
|
3968
|
+
],
|
|
3791
3969
|
flowLineMultiplier: 0,
|
|
3792
3970
|
heroShape: true,
|
|
3793
3971
|
glowMultiplier: 0,
|
|
@@ -3809,6 +3987,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3809
3987
|
"watercolor",
|
|
3810
3988
|
"fill-only"
|
|
3811
3989
|
],
|
|
3990
|
+
preferredCompositions: [
|
|
3991
|
+
"radial",
|
|
3992
|
+
"golden-spiral",
|
|
3993
|
+
"flow-field"
|
|
3994
|
+
],
|
|
3812
3995
|
flowLineMultiplier: 1,
|
|
3813
3996
|
heroShape: true,
|
|
3814
3997
|
glowMultiplier: 1,
|
|
@@ -3830,6 +4013,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3830
4013
|
"stroke-only",
|
|
3831
4014
|
"fill-only"
|
|
3832
4015
|
],
|
|
4016
|
+
preferredCompositions: [
|
|
4017
|
+
"clustered",
|
|
4018
|
+
"grid-subdivision",
|
|
4019
|
+
"radial"
|
|
4020
|
+
],
|
|
3833
4021
|
flowLineMultiplier: 0,
|
|
3834
4022
|
heroShape: false,
|
|
3835
4023
|
glowMultiplier: 0.3,
|
|
@@ -3851,6 +4039,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3851
4039
|
"fill-only",
|
|
3852
4040
|
"incomplete"
|
|
3853
4041
|
],
|
|
4042
|
+
preferredCompositions: [
|
|
4043
|
+
"flow-field",
|
|
4044
|
+
"golden-spiral",
|
|
4045
|
+
"spiral"
|
|
4046
|
+
],
|
|
3854
4047
|
flowLineMultiplier: 3,
|
|
3855
4048
|
heroShape: true,
|
|
3856
4049
|
glowMultiplier: 0.2,
|
|
@@ -3872,6 +4065,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3872
4065
|
"fill-only",
|
|
3873
4066
|
"hatched"
|
|
3874
4067
|
],
|
|
4068
|
+
preferredCompositions: [
|
|
4069
|
+
"radial",
|
|
4070
|
+
"clustered",
|
|
4071
|
+
"flow-field"
|
|
4072
|
+
],
|
|
3875
4073
|
flowLineMultiplier: 0,
|
|
3876
4074
|
heroShape: false,
|
|
3877
4075
|
glowMultiplier: 0,
|
|
@@ -3894,6 +4092,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3894
4092
|
"stroke-only",
|
|
3895
4093
|
"incomplete"
|
|
3896
4094
|
],
|
|
4095
|
+
preferredCompositions: [
|
|
4096
|
+
"spiral",
|
|
4097
|
+
"radial",
|
|
4098
|
+
"golden-spiral"
|
|
4099
|
+
],
|
|
3897
4100
|
flowLineMultiplier: 2,
|
|
3898
4101
|
heroShape: true,
|
|
3899
4102
|
glowMultiplier: 2.5,
|
|
@@ -3917,6 +4120,12 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3917
4120
|
...b.preferredStyles
|
|
3918
4121
|
])
|
|
3919
4122
|
];
|
|
4123
|
+
const mergedCompositions = [
|
|
4124
|
+
...new Set([
|
|
4125
|
+
...a.preferredCompositions,
|
|
4126
|
+
...b.preferredCompositions
|
|
4127
|
+
])
|
|
4128
|
+
];
|
|
3920
4129
|
return {
|
|
3921
4130
|
name: `${a.name}+${b.name}`,
|
|
3922
4131
|
gridSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3928,6 +4137,7 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3928
4137
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3929
4138
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3930
4139
|
preferredStyles: mergedStyles,
|
|
4140
|
+
preferredCompositions: mergedCompositions,
|
|
3931
4141
|
flowLineMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3932
4142
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3933
4143
|
glowMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3949,6 +4159,46 @@ function $68a238ccd77f2bcd$export$f1142fd7da4d6590(rng) {
|
|
|
3949
4159
|
}
|
|
3950
4160
|
|
|
3951
4161
|
|
|
4162
|
+
// ── Render style cost weights (normalized: fill-and-stroke = 1) ─────
|
|
4163
|
+
// Based on benchmark measurements. Used by the complexity budget to
|
|
4164
|
+
// cap total rendering work and downgrade expensive styles when needed.
|
|
4165
|
+
const $1f63dc64b5593c73$var$RENDER_STYLE_COST = {
|
|
4166
|
+
"fill-and-stroke": 1,
|
|
4167
|
+
"fill-only": 0.5,
|
|
4168
|
+
"stroke-only": 1,
|
|
4169
|
+
"double-stroke": 1.5,
|
|
4170
|
+
"dashed": 1,
|
|
4171
|
+
"watercolor": 7,
|
|
4172
|
+
"hatched": 3,
|
|
4173
|
+
"incomplete": 1,
|
|
4174
|
+
"stipple": 90,
|
|
4175
|
+
"stencil": 2,
|
|
4176
|
+
"noise-grain": 400,
|
|
4177
|
+
"wood-grain": 10,
|
|
4178
|
+
"marble-vein": 4,
|
|
4179
|
+
"fabric-weave": 6,
|
|
4180
|
+
"hand-drawn": 5
|
|
4181
|
+
};
|
|
4182
|
+
function $1f63dc64b5593c73$var$downgradeRenderStyle(style) {
|
|
4183
|
+
switch(style){
|
|
4184
|
+
case "noise-grain":
|
|
4185
|
+
return "hatched";
|
|
4186
|
+
case "stipple":
|
|
4187
|
+
return "dashed";
|
|
4188
|
+
case "wood-grain":
|
|
4189
|
+
return "hatched";
|
|
4190
|
+
case "watercolor":
|
|
4191
|
+
return "fill-and-stroke";
|
|
4192
|
+
case "fabric-weave":
|
|
4193
|
+
return "hatched";
|
|
4194
|
+
case "hand-drawn":
|
|
4195
|
+
return "fill-and-stroke";
|
|
4196
|
+
case "marble-vein":
|
|
4197
|
+
return "stroke-only";
|
|
4198
|
+
default:
|
|
4199
|
+
return style;
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
3952
4202
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
3953
4203
|
const $1f63dc64b5593c73$var$SACRED_SHAPES = [
|
|
3954
4204
|
"mandala",
|
|
@@ -3961,7 +4211,8 @@ const $1f63dc64b5593c73$var$SACRED_SHAPES = [
|
|
|
3961
4211
|
"torus",
|
|
3962
4212
|
"eggOfLife"
|
|
3963
4213
|
];
|
|
3964
|
-
|
|
4214
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4215
|
+
const $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES = [
|
|
3965
4216
|
"radial",
|
|
3966
4217
|
"flow-field",
|
|
3967
4218
|
"spiral",
|
|
@@ -4063,7 +4314,67 @@ function $1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones) {
|
|
|
4063
4314
|
}
|
|
4064
4315
|
return false;
|
|
4065
4316
|
}
|
|
4066
|
-
// ──
|
|
4317
|
+
// ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
|
|
4318
|
+
class $1f63dc64b5593c73$var$SpatialGrid {
|
|
4319
|
+
constructor(cellSize){
|
|
4320
|
+
this.cells = new Map();
|
|
4321
|
+
this.cellSize = cellSize;
|
|
4322
|
+
}
|
|
4323
|
+
key(cx, cy) {
|
|
4324
|
+
return `${cx},${cy}`;
|
|
4325
|
+
}
|
|
4326
|
+
insert(item) {
|
|
4327
|
+
const cx = Math.floor(item.x / this.cellSize);
|
|
4328
|
+
const cy = Math.floor(item.y / this.cellSize);
|
|
4329
|
+
const k = this.key(cx, cy);
|
|
4330
|
+
const cell = this.cells.get(k);
|
|
4331
|
+
if (cell) cell.push(item);
|
|
4332
|
+
else this.cells.set(k, [
|
|
4333
|
+
item
|
|
4334
|
+
]);
|
|
4335
|
+
}
|
|
4336
|
+
/** Count items within radius of (x, y) */ countNear(x, y, radius) {
|
|
4337
|
+
const r2 = radius * radius;
|
|
4338
|
+
const minCx = Math.floor((x - radius) / this.cellSize);
|
|
4339
|
+
const maxCx = Math.floor((x + radius) / this.cellSize);
|
|
4340
|
+
const minCy = Math.floor((y - radius) / this.cellSize);
|
|
4341
|
+
const maxCy = Math.floor((y + radius) / this.cellSize);
|
|
4342
|
+
let count = 0;
|
|
4343
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4344
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4345
|
+
if (!cell) continue;
|
|
4346
|
+
for (const p of cell){
|
|
4347
|
+
const dx = x - p.x;
|
|
4348
|
+
const dy = y - p.y;
|
|
4349
|
+
if (dx * dx + dy * dy < r2) count++;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
return count;
|
|
4353
|
+
}
|
|
4354
|
+
/** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
|
|
4355
|
+
const minCx = Math.floor((x - searchRadius) / this.cellSize);
|
|
4356
|
+
const maxCx = Math.floor((x + searchRadius) / this.cellSize);
|
|
4357
|
+
const minCy = Math.floor((y - searchRadius) / this.cellSize);
|
|
4358
|
+
const maxCy = Math.floor((y + searchRadius) / this.cellSize);
|
|
4359
|
+
let nearest = null;
|
|
4360
|
+
let bestDist2 = Infinity;
|
|
4361
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4362
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4363
|
+
if (!cell) continue;
|
|
4364
|
+
for (const p of cell){
|
|
4365
|
+
const dx = x - p.x;
|
|
4366
|
+
const dy = y - p.y;
|
|
4367
|
+
const d2 = dx * dx + dy * dy;
|
|
4368
|
+
if (d2 > 0 && d2 < bestDist2) {
|
|
4369
|
+
bestDist2 = d2;
|
|
4370
|
+
nearest = p;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
return nearest;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
// ── Helper: density check (legacy wrapper) ──────────────────────────
|
|
4067
4378
|
function $1f63dc64b5593c73$var$localDensity(x, y, positions, radius) {
|
|
4068
4379
|
let count = 0;
|
|
4069
4380
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4362,42 +4673,43 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4362
4673
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4363
4674
|
const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4364
4675
|
if (bgPatternRoll < 0.2) {
|
|
4365
|
-
// Dot grid
|
|
4676
|
+
// Dot grid — batched into a single path
|
|
4366
4677
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4367
4678
|
const dotR = dotSpacing * 0.08;
|
|
4368
4679
|
ctx.globalAlpha = patternOpacity;
|
|
4369
4680
|
ctx.fillStyle = patternColor;
|
|
4681
|
+
ctx.beginPath();
|
|
4370
4682
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4371
|
-
ctx.
|
|
4683
|
+
ctx.moveTo(px + dotR, py);
|
|
4372
4684
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4373
|
-
ctx.fill();
|
|
4374
4685
|
}
|
|
4686
|
+
ctx.fill();
|
|
4375
4687
|
} else if (bgPatternRoll < 0.4) {
|
|
4376
|
-
// Diagonal lines
|
|
4688
|
+
// Diagonal lines — batched into a single path
|
|
4377
4689
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4378
4690
|
ctx.globalAlpha = patternOpacity;
|
|
4379
4691
|
ctx.strokeStyle = patternColor;
|
|
4380
4692
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4381
4693
|
const diag = Math.hypot(width, height);
|
|
4694
|
+
ctx.beginPath();
|
|
4382
4695
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4383
|
-
ctx.beginPath();
|
|
4384
4696
|
ctx.moveTo(d, 0);
|
|
4385
4697
|
ctx.lineTo(d + height, height);
|
|
4386
|
-
ctx.stroke();
|
|
4387
4698
|
}
|
|
4699
|
+
ctx.stroke();
|
|
4388
4700
|
} else {
|
|
4389
|
-
// Tessellation — hexagonal grid
|
|
4701
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4390
4702
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4391
4703
|
const tessH = tessSize * Math.sqrt(3);
|
|
4392
4704
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4393
4705
|
ctx.strokeStyle = patternColor;
|
|
4394
4706
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4707
|
+
ctx.beginPath();
|
|
4395
4708
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4396
4709
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4397
4710
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4398
4711
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4399
4712
|
const hy = row * tessH;
|
|
4400
|
-
ctx.beginPath();
|
|
4401
4713
|
for(let s = 0; s < 6; s++){
|
|
4402
4714
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4403
4715
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4406,18 +4718,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4406
4718
|
else ctx.lineTo(vx, vy);
|
|
4407
4719
|
}
|
|
4408
4720
|
ctx.closePath();
|
|
4409
|
-
ctx.stroke();
|
|
4410
4721
|
}
|
|
4411
4722
|
}
|
|
4723
|
+
ctx.stroke();
|
|
4412
4724
|
}
|
|
4413
4725
|
ctx.restore();
|
|
4414
4726
|
}
|
|
4415
4727
|
ctx.globalCompositeOperation = "source-over";
|
|
4416
|
-
// ── 2. Composition mode
|
|
4417
|
-
const compositionMode = $1f63dc64b5593c73$var$
|
|
4728
|
+
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4729
|
+
const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES.length)];
|
|
4418
4730
|
const symRoll = rng();
|
|
4419
4731
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4420
|
-
// ── 3. Focal points + void zones
|
|
4732
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4421
4733
|
const THIRDS_POINTS = [
|
|
4422
4734
|
{
|
|
4423
4735
|
x: 1 / 3,
|
|
@@ -4450,9 +4762,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4450
4762
|
y: height * (0.2 + rng() * 0.6),
|
|
4451
4763
|
strength: 0.3 + rng() * 0.4
|
|
4452
4764
|
});
|
|
4453
|
-
|
|
4765
|
+
// Archetype-aware void zones: dense archetypes get fewer/no voids,
|
|
4766
|
+
// minimal archetypes get golden-ratio positioned voids
|
|
4767
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4768
|
+
const isMinimalArchetype = archetype.gridSize <= 3;
|
|
4769
|
+
const isDenseArchetype = archetype.gridSize >= 8;
|
|
4770
|
+
const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
|
|
4454
4771
|
const voidZones = [];
|
|
4455
|
-
for(let v = 0; v < numVoids; v++)
|
|
4772
|
+
for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
|
|
4773
|
+
// Place voids at golden-ratio positions for intentional negative space
|
|
4774
|
+
const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
|
|
4775
|
+
const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
|
|
4776
|
+
voidZones.push({
|
|
4777
|
+
x: width * (gx + (rng() - 0.5) * 0.05),
|
|
4778
|
+
y: height * (gy + (rng() - 0.5) * 0.05),
|
|
4779
|
+
radius: Math.min(width, height) * (0.08 + rng() * 0.08)
|
|
4780
|
+
});
|
|
4781
|
+
} else voidZones.push({
|
|
4456
4782
|
x: width * (0.15 + rng() * 0.7),
|
|
4457
4783
|
y: height * (0.15 + rng() * 0.7),
|
|
4458
4784
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4482,19 +4808,20 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4482
4808
|
ctx.beginPath();
|
|
4483
4809
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
4484
4810
|
ctx.stroke();
|
|
4485
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
4811
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
4486
4812
|
if (rng() < 0.5) {
|
|
4487
4813
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
4488
4814
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
4489
4815
|
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
4816
|
+
ctx.beginPath();
|
|
4490
4817
|
for(let d = 0; d < dotCount; d++){
|
|
4491
4818
|
const angle = rng() * Math.PI * 2;
|
|
4492
4819
|
const dist = rng() * zone.radius * 0.7;
|
|
4493
4820
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
4494
|
-
ctx.
|
|
4821
|
+
ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
|
|
4495
4822
|
ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
|
|
4496
|
-
ctx.fill();
|
|
4497
4823
|
}
|
|
4824
|
+
ctx.fill();
|
|
4498
4825
|
}
|
|
4499
4826
|
// ~30% chance: thin concentric ring inside
|
|
4500
4827
|
if (rng() < 0.3) {
|
|
@@ -4529,6 +4856,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4529
4856
|
}
|
|
4530
4857
|
// Track all placed shapes for density checks and connecting curves
|
|
4531
4858
|
const shapePositions = [];
|
|
4859
|
+
// Spatial grid for O(1) density and nearest-neighbor lookups
|
|
4860
|
+
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4861
|
+
const spatialGrid = new $1f63dc64b5593c73$var$SpatialGrid(densityCheckRadius);
|
|
4532
4862
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4533
4863
|
let heroCenter = null;
|
|
4534
4864
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4574,10 +4904,35 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4574
4904
|
size: heroSize,
|
|
4575
4905
|
shape: heroShape
|
|
4576
4906
|
});
|
|
4907
|
+
spatialGrid.insert({
|
|
4908
|
+
x: heroFocal.x,
|
|
4909
|
+
y: heroFocal.y,
|
|
4910
|
+
size: heroSize,
|
|
4911
|
+
shape: heroShape
|
|
4912
|
+
});
|
|
4577
4913
|
}
|
|
4578
4914
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4579
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4580
4915
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4916
|
+
// ── Complexity budget — caps total rendering work ──────────────
|
|
4917
|
+
// Budget scales with pixel area so larger canvases get proportionally
|
|
4918
|
+
// more headroom. The multiplier extras (glazing, echoes, nesting,
|
|
4919
|
+
// constellations, rhythm) are gated behind the budget; when it runs
|
|
4920
|
+
// low they are skipped. When it's exhausted, expensive render styles
|
|
4921
|
+
// are downgraded to cheaper alternatives.
|
|
4922
|
+
//
|
|
4923
|
+
// RNG values are always consumed even when skipping, so the
|
|
4924
|
+
// deterministic sequence for shapes that *do* render is preserved.
|
|
4925
|
+
const pixelArea = width * height;
|
|
4926
|
+
const BUDGET_PER_MEGAPIXEL = 6000; // cost units per 1M pixels
|
|
4927
|
+
let complexityBudget = pixelArea / 1000000 * BUDGET_PER_MEGAPIXEL;
|
|
4928
|
+
const totalBudget = complexityBudget;
|
|
4929
|
+
const budgetForExtras = complexityBudget * 0.25; // reserve 25% for multiplier extras
|
|
4930
|
+
let extrasSpent = 0;
|
|
4931
|
+
// Hard cap on clip-heavy render styles (stipple, noise-grain).
|
|
4932
|
+
// These generate O(size²) fillRect calls per shape and dominate
|
|
4933
|
+
// worst-case render time. Cap scales with pixel area.
|
|
4934
|
+
const MAX_CLIP_HEAVY_SHAPES = Math.max(4, Math.floor(8 * (pixelArea / 1000000)));
|
|
4935
|
+
let clipHeavyCount = 0;
|
|
4581
4936
|
for(let layer = 0; layer < layers; layer++){
|
|
4582
4937
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
4583
4938
|
const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
|
|
@@ -4615,7 +4970,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4615
4970
|
if ($1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones)) {
|
|
4616
4971
|
if (rng() < 0.85) continue;
|
|
4617
4972
|
}
|
|
4618
|
-
if (
|
|
4973
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4619
4974
|
if (rng() < 0.6) continue;
|
|
4620
4975
|
}
|
|
4621
4976
|
// Power distribution for size — archetype controls the curve
|
|
@@ -4666,7 +5021,26 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4666
5021
|
const shapeRenderStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
|
|
4667
5022
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
4668
5023
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
4669
|
-
|
|
5024
|
+
let finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
|
|
5025
|
+
// Budget check: downgrade expensive styles proportionally —
|
|
5026
|
+
// the more expensive the style, the earlier it gets downgraded.
|
|
5027
|
+
// noise-grain (400) downgrades when budget < 20% remaining,
|
|
5028
|
+
// stipple (90) when < 82%, wood-grain (10) when < 98%.
|
|
5029
|
+
let styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5030
|
+
if (styleCost > 3) {
|
|
5031
|
+
const downgradeThreshold = Math.min(0.85, styleCost / 500);
|
|
5032
|
+
if (complexityBudget < totalBudget * (1 - downgradeThreshold)) {
|
|
5033
|
+
finalRenderStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(finalRenderStyle);
|
|
5034
|
+
styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
// Hard cap: clip-heavy styles (stipple, noise-grain) are limited
|
|
5038
|
+
// to MAX_CLIP_HEAVY_SHAPES total across the entire render.
|
|
5039
|
+
if ((finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
5040
|
+
finalRenderStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(finalRenderStyle);
|
|
5041
|
+
styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5042
|
+
}
|
|
5043
|
+
if (finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") clipHeavyCount++;
|
|
4670
5044
|
// Consistent light direction — subtle shadow offset
|
|
4671
5045
|
const shadowDist = hasGlow ? 0 : size * 0.02;
|
|
4672
5046
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
@@ -4675,17 +5049,11 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4675
5049
|
let finalX = x;
|
|
4676
5050
|
let finalY = y;
|
|
4677
5051
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4678
|
-
//
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
for (const sp of shapePositions){
|
|
4682
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4683
|
-
if (d < nearestDist && d > 0) {
|
|
4684
|
-
nearestDist = d;
|
|
4685
|
-
nearestPos = sp;
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
5052
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
5053
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
5054
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4688
5055
|
if (nearestPos) {
|
|
5056
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4689
5057
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4690
5058
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4691
5059
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4727,30 +5095,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4727
5095
|
lightAngle: lightAngle,
|
|
4728
5096
|
scaleFactor: scaleFactor
|
|
4729
5097
|
};
|
|
4730
|
-
if (shouldMirror)
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
5098
|
+
if (shouldMirror) {
|
|
5099
|
+
(0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
5100
|
+
...shapeConfig,
|
|
5101
|
+
mirrorAxis: mirrorAxis,
|
|
5102
|
+
mirrorGap: size * (0.1 + rng() * 0.3)
|
|
5103
|
+
});
|
|
5104
|
+
complexityBudget -= styleCost * 2; // mirrored = 2 shapes
|
|
5105
|
+
} else {
|
|
5106
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
|
|
5107
|
+
complexityBudget -= styleCost;
|
|
5108
|
+
}
|
|
5109
|
+
// ── Extras budget gate — skip multiplier sections when over budget ──
|
|
5110
|
+
const extrasAllowed = extrasSpent < budgetForExtras;
|
|
4736
5111
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
4737
5112
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
4738
5113
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
5114
|
+
if (extrasAllowed) {
|
|
5115
|
+
for(let g = 0; g < glazePasses; g++){
|
|
5116
|
+
const glazeScale = 1 - (g + 1) * 0.12;
|
|
5117
|
+
const glazeAlpha = 0.08 + g * 0.04;
|
|
5118
|
+
ctx.globalAlpha = glazeAlpha;
|
|
5119
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
|
|
5120
|
+
fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
|
|
5121
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
5122
|
+
strokeWidth: 0,
|
|
5123
|
+
size: size * glazeScale,
|
|
5124
|
+
rotation: rotation,
|
|
5125
|
+
proportionType: "GOLDEN_RATIO",
|
|
5126
|
+
renderStyle: "fill-only",
|
|
5127
|
+
rng: rng
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
extrasSpent += glazePasses;
|
|
4753
5131
|
}
|
|
5132
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
4754
5133
|
}
|
|
4755
5134
|
shapePositions.push({
|
|
4756
5135
|
x: finalX,
|
|
@@ -4758,35 +5137,51 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4758
5137
|
size: size,
|
|
4759
5138
|
shape: shape
|
|
4760
5139
|
});
|
|
5140
|
+
spatialGrid.insert({
|
|
5141
|
+
x: finalX,
|
|
5142
|
+
y: finalY,
|
|
5143
|
+
size: size,
|
|
5144
|
+
shape: shape
|
|
5145
|
+
});
|
|
4761
5146
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4762
5147
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4763
5148
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
4764
5149
|
const echoAngle = rng() * Math.PI * 2;
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
5150
|
+
if (extrasAllowed) {
|
|
5151
|
+
for(let e = 0; e < echoCount; e++){
|
|
5152
|
+
const echoScale = 0.3 - e * 0.08;
|
|
5153
|
+
const echoDist = size * (0.6 + e * 0.4);
|
|
5154
|
+
const echoX = finalX + Math.cos(echoAngle) * echoDist;
|
|
5155
|
+
const echoY = finalY + Math.sin(echoAngle) * echoDist;
|
|
5156
|
+
const echoSize = size * Math.max(0.1, echoScale);
|
|
5157
|
+
if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
|
|
5158
|
+
ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
|
|
5159
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
|
|
5160
|
+
fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
|
|
5161
|
+
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.4),
|
|
5162
|
+
strokeWidth: strokeWidth * 0.6,
|
|
5163
|
+
size: echoSize,
|
|
5164
|
+
rotation: rotation + (e + 1) * 15,
|
|
5165
|
+
proportionType: "GOLDEN_RATIO",
|
|
5166
|
+
renderStyle: finalRenderStyle,
|
|
5167
|
+
rng: rng
|
|
5168
|
+
});
|
|
5169
|
+
shapePositions.push({
|
|
5170
|
+
x: echoX,
|
|
5171
|
+
y: echoY,
|
|
5172
|
+
size: echoSize,
|
|
5173
|
+
shape: shape
|
|
5174
|
+
});
|
|
5175
|
+
spatialGrid.insert({
|
|
5176
|
+
x: echoX,
|
|
5177
|
+
y: echoY,
|
|
5178
|
+
size: echoSize,
|
|
5179
|
+
shape: shape
|
|
5180
|
+
});
|
|
5181
|
+
}
|
|
5182
|
+
extrasSpent += echoCount * styleCost;
|
|
4789
5183
|
}
|
|
5184
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
4790
5185
|
}
|
|
4791
5186
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
4792
5187
|
// Focal depth: shapes near focal points get more detail
|
|
@@ -4794,7 +5189,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4794
5189
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
4795
5190
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
4796
5191
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
4797
|
-
for(let n = 0; n < innerCount; n++){
|
|
5192
|
+
if (extrasAllowed) for(let n = 0; n < innerCount; n++){
|
|
4798
5193
|
// Pick inner shape from palette affinities
|
|
4799
5194
|
const innerSizeFraction = size * 0.25 / adjustedMaxSize;
|
|
4800
5195
|
const innerShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
|
|
@@ -4803,6 +5198,10 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4803
5198
|
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
4804
5199
|
const innerRot = rng() * 360;
|
|
4805
5200
|
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);
|
|
5201
|
+
let innerStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng);
|
|
5202
|
+
// Apply clip-heavy cap to nested shapes too
|
|
5203
|
+
if ((innerStyle === "stipple" || innerStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) innerStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(innerStyle);
|
|
5204
|
+
if (innerStyle === "stipple" || innerStyle === "noise-grain") clipHeavyCount++;
|
|
4806
5205
|
ctx.globalAlpha = layerOpacity * 0.7;
|
|
4807
5206
|
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
|
|
4808
5207
|
fillColor: innerFill,
|
|
@@ -4811,9 +5210,21 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4811
5210
|
size: innerSize,
|
|
4812
5211
|
rotation: innerRot,
|
|
4813
5212
|
proportionType: "GOLDEN_RATIO",
|
|
4814
|
-
renderStyle:
|
|
5213
|
+
renderStyle: innerStyle,
|
|
4815
5214
|
rng: rng
|
|
4816
5215
|
});
|
|
5216
|
+
extrasSpent += $1f63dc64b5593c73$var$RENDER_STYLE_COST[innerStyle] ?? 1;
|
|
5217
|
+
}
|
|
5218
|
+
else // Drain RNG to keep determinism — each nested shape consumes ~8 rng calls
|
|
5219
|
+
for(let n = 0; n < innerCount; n++){
|
|
5220
|
+
rng();
|
|
5221
|
+
rng();
|
|
5222
|
+
rng();
|
|
5223
|
+
rng();
|
|
5224
|
+
rng();
|
|
5225
|
+
rng();
|
|
5226
|
+
rng();
|
|
5227
|
+
rng();
|
|
4817
5228
|
}
|
|
4818
5229
|
}
|
|
4819
5230
|
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
@@ -4822,41 +5233,113 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4822
5233
|
const constellation = $1f63dc64b5593c73$var$CONSTELLATIONS[Math.floor(rng() * $1f63dc64b5593c73$var$CONSTELLATIONS.length)];
|
|
4823
5234
|
const members = constellation.build(rng, size);
|
|
4824
5235
|
const groupRotation = rng() * Math.PI * 2;
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
5236
|
+
if (extrasAllowed) {
|
|
5237
|
+
const cosR = Math.cos(groupRotation);
|
|
5238
|
+
const sinR = Math.sin(groupRotation);
|
|
5239
|
+
for (const member of members){
|
|
5240
|
+
// Rotate the group offset by the group rotation
|
|
5241
|
+
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
5242
|
+
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
5243
|
+
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
5244
|
+
const memberFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
|
|
5245
|
+
const memberStroke = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
|
|
5246
|
+
ctx.globalAlpha = layerOpacity * 0.6;
|
|
5247
|
+
// Use the member's shape if available, otherwise fall back to palette
|
|
5248
|
+
const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
|
|
5249
|
+
let memberStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng);
|
|
5250
|
+
// Apply clip-heavy cap to constellation members too
|
|
5251
|
+
if ((memberStyle === "stipple" || memberStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) memberStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(memberStyle);
|
|
5252
|
+
if (memberStyle === "stipple" || memberStyle === "noise-grain") clipHeavyCount++;
|
|
5253
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
|
|
5254
|
+
fillColor: memberFill,
|
|
5255
|
+
strokeColor: memberStroke,
|
|
5256
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5257
|
+
size: member.size,
|
|
5258
|
+
rotation: member.rotation + groupRotation * 180 / Math.PI,
|
|
5259
|
+
proportionType: "GOLDEN_RATIO",
|
|
5260
|
+
renderStyle: memberStyle,
|
|
5261
|
+
rng: rng
|
|
5262
|
+
});
|
|
5263
|
+
shapePositions.push({
|
|
5264
|
+
x: mx,
|
|
5265
|
+
y: my,
|
|
5266
|
+
size: member.size,
|
|
5267
|
+
shape: memberShape
|
|
5268
|
+
});
|
|
5269
|
+
spatialGrid.insert({
|
|
5270
|
+
x: mx,
|
|
5271
|
+
y: my,
|
|
5272
|
+
size: member.size,
|
|
5273
|
+
shape: memberShape
|
|
5274
|
+
});
|
|
5275
|
+
extrasSpent += $1f63dc64b5593c73$var$RENDER_STYLE_COST[memberStyle] ?? 1;
|
|
5276
|
+
}
|
|
5277
|
+
} else // Drain RNG — each member consumes ~6 rng calls for colors/style
|
|
5278
|
+
for(let m = 0; m < members.length; m++){
|
|
5279
|
+
rng();
|
|
5280
|
+
rng();
|
|
5281
|
+
rng();
|
|
5282
|
+
rng();
|
|
5283
|
+
rng();
|
|
5284
|
+
rng();
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
5288
|
+
// ~12% of medium-large shapes spawn a rhythmic sequence
|
|
5289
|
+
if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
|
|
5290
|
+
const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
|
|
5291
|
+
const rhythmAngle = rng() * Math.PI * 2;
|
|
5292
|
+
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5293
|
+
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5294
|
+
const rhythmShape = shape; // same shape for visual rhythm
|
|
5295
|
+
if (extrasAllowed) {
|
|
5296
|
+
let rhythmSize = size * 0.6;
|
|
5297
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5298
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5299
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5300
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5301
|
+
if ($1f63dc64b5593c73$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5302
|
+
rhythmSize *= rhythmDecay;
|
|
5303
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5304
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5305
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5306
|
+
const rhythmFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5307
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5308
|
+
fillColor: rhythmFill,
|
|
5309
|
+
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5310
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5311
|
+
size: rhythmSize,
|
|
5312
|
+
rotation: rotation + (r + 1) * 12,
|
|
5313
|
+
proportionType: "GOLDEN_RATIO",
|
|
5314
|
+
renderStyle: finalRenderStyle,
|
|
5315
|
+
rng: rng
|
|
5316
|
+
});
|
|
5317
|
+
shapePositions.push({
|
|
5318
|
+
x: rx,
|
|
5319
|
+
y: ry,
|
|
5320
|
+
size: rhythmSize,
|
|
5321
|
+
shape: rhythmShape
|
|
5322
|
+
});
|
|
5323
|
+
spatialGrid.insert({
|
|
5324
|
+
x: rx,
|
|
5325
|
+
y: ry,
|
|
5326
|
+
size: rhythmSize,
|
|
5327
|
+
shape: rhythmShape
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
extrasSpent += rhythmCount * styleCost;
|
|
5331
|
+
} else // Drain RNG — each rhythm step consumes ~3 rng calls for colors
|
|
5332
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5333
|
+
rng();
|
|
5334
|
+
rng();
|
|
5335
|
+
rng();
|
|
4853
5336
|
}
|
|
4854
5337
|
}
|
|
4855
5338
|
}
|
|
4856
5339
|
}
|
|
4857
5340
|
// Reset blend mode for post-processing passes
|
|
4858
5341
|
ctx.globalCompositeOperation = "source-over";
|
|
4859
|
-
// ──
|
|
5342
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
4860
5343
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
4861
5344
|
// with a tinted background wash, creating a "peek through" effect.
|
|
4862
5345
|
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
@@ -4915,14 +5398,26 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4915
5398
|
}
|
|
4916
5399
|
}
|
|
4917
5400
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
5401
|
+
// Optimized: collect all segments into width-quantized buckets, then
|
|
5402
|
+
// render each bucket as a single batched path. This reduces
|
|
5403
|
+
// beginPath/stroke calls from O(segments) to O(buckets).
|
|
4918
5404
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
4919
5405
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
5406
|
+
// Width buckets — 6 buckets cover the taper×pressure range
|
|
5407
|
+
const FLOW_WIDTH_BUCKETS = 6;
|
|
5408
|
+
const flowBuckets = [];
|
|
5409
|
+
for(let b = 0; b < FLOW_WIDTH_BUCKETS; b++)flowBuckets.push([]);
|
|
5410
|
+
// Track the representative width for each bucket
|
|
5411
|
+
const flowBucketWidths = new Array(FLOW_WIDTH_BUCKETS);
|
|
5412
|
+
// Pre-compute max possible width for bucket assignment
|
|
5413
|
+
let globalMaxFlowWidth = 0;
|
|
4920
5414
|
for(let i = 0; i < numFlowLines; i++){
|
|
4921
5415
|
let fx = rng() * width;
|
|
4922
5416
|
let fy = rng() * height;
|
|
4923
5417
|
const steps = 30 + Math.floor(rng() * 40);
|
|
4924
5418
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
4925
5419
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
5420
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
4926
5421
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
4927
5422
|
const lineColorStart = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
4928
5423
|
const lineColorEnd = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
@@ -4937,20 +5432,29 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4937
5432
|
fx += Math.cos(angle) * stepLen;
|
|
4938
5433
|
fy += Math.sin(angle) * stepLen;
|
|
4939
5434
|
if (fx < 0 || fx > width || fy < 0 || fy > height) break;
|
|
5435
|
+
// Skip segments that pass through void zones
|
|
5436
|
+
if ($1f63dc64b5593c73$var$isInVoidZone(fx, fy, voidZones)) {
|
|
5437
|
+
prevX = fx;
|
|
5438
|
+
prevY = fy;
|
|
5439
|
+
continue;
|
|
5440
|
+
}
|
|
4940
5441
|
const t = s / steps;
|
|
4941
|
-
// Taper + pressure
|
|
4942
5442
|
const taper = 1 - t * 0.8;
|
|
4943
5443
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
4944
|
-
|
|
4945
|
-
|
|
5444
|
+
const segWidth = startWidth * taper * pressure;
|
|
5445
|
+
const segAlpha = lineAlpha * taper;
|
|
4946
5446
|
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);
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
5447
|
+
// Quantize width into bucket
|
|
5448
|
+
const bucketIdx = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(segWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5449
|
+
flowBuckets[bucketIdx].push({
|
|
5450
|
+
x1: prevX,
|
|
5451
|
+
y1: prevY,
|
|
5452
|
+
x2: fx,
|
|
5453
|
+
y2: fy,
|
|
5454
|
+
color: lineColor,
|
|
5455
|
+
alpha: segAlpha
|
|
5456
|
+
});
|
|
5457
|
+
flowBucketWidths[bucketIdx] = segWidth;
|
|
4954
5458
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
4955
5459
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
4956
5460
|
const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
|
|
@@ -4966,12 +5470,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4966
5470
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
4967
5471
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
4968
5472
|
const bTaper = 1 - bs / branchSteps * 0.9;
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5473
|
+
const bSegWidth = branchWidth * bTaper;
|
|
5474
|
+
const bAlpha = lineAlpha * taper * bTaper * 0.6;
|
|
5475
|
+
const bBucket = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(bSegWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5476
|
+
flowBuckets[bBucket].push({
|
|
5477
|
+
x1: bPrevX,
|
|
5478
|
+
y1: bPrevY,
|
|
5479
|
+
x2: bx,
|
|
5480
|
+
y2: by,
|
|
5481
|
+
color: lineColor,
|
|
5482
|
+
alpha: bAlpha
|
|
5483
|
+
});
|
|
5484
|
+
flowBucketWidths[bBucket] = bSegWidth;
|
|
4975
5485
|
bPrevX = bx;
|
|
4976
5486
|
bPrevY = by;
|
|
4977
5487
|
}
|
|
@@ -4980,7 +5490,40 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4980
5490
|
prevY = fy;
|
|
4981
5491
|
}
|
|
4982
5492
|
}
|
|
5493
|
+
// Render flow line buckets — one batched path per width bucket
|
|
5494
|
+
// Within each bucket, further sub-batch by quantized alpha (4 levels)
|
|
5495
|
+
ctx.lineCap = "round";
|
|
5496
|
+
const FLOW_ALPHA_BUCKETS = 4;
|
|
5497
|
+
for(let wb = 0; wb < FLOW_WIDTH_BUCKETS; wb++){
|
|
5498
|
+
const segs = flowBuckets[wb];
|
|
5499
|
+
if (segs.length === 0) continue;
|
|
5500
|
+
ctx.lineWidth = flowBucketWidths[wb];
|
|
5501
|
+
// Sub-bucket by alpha
|
|
5502
|
+
const alphaSubs = [];
|
|
5503
|
+
for(let a = 0; a < FLOW_ALPHA_BUCKETS; a++)alphaSubs.push([]);
|
|
5504
|
+
let maxAlpha = 0;
|
|
5505
|
+
for(let j = 0; j < segs.length; j++)if (segs[j].alpha > maxAlpha) maxAlpha = segs[j].alpha;
|
|
5506
|
+
for(let j = 0; j < segs.length; j++){
|
|
5507
|
+
const ai = Math.min(FLOW_ALPHA_BUCKETS - 1, Math.floor(segs[j].alpha / (maxAlpha || 1) * FLOW_ALPHA_BUCKETS));
|
|
5508
|
+
alphaSubs[ai].push(segs[j]);
|
|
5509
|
+
}
|
|
5510
|
+
for(let ai = 0; ai < FLOW_ALPHA_BUCKETS; ai++){
|
|
5511
|
+
const sub = alphaSubs[ai];
|
|
5512
|
+
if (sub.length === 0) continue;
|
|
5513
|
+
// Use the median segment's alpha and color as representative
|
|
5514
|
+
const rep = sub[Math.floor(sub.length / 2)];
|
|
5515
|
+
ctx.globalAlpha = rep.alpha;
|
|
5516
|
+
ctx.strokeStyle = rep.color;
|
|
5517
|
+
ctx.beginPath();
|
|
5518
|
+
for(let j = 0; j < sub.length; j++){
|
|
5519
|
+
ctx.moveTo(sub[j].x1, sub[j].y1);
|
|
5520
|
+
ctx.lineTo(sub[j].x2, sub[j].y2);
|
|
5521
|
+
}
|
|
5522
|
+
ctx.stroke();
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
4983
5525
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
5526
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
4984
5527
|
const energyArchetypes = [
|
|
4985
5528
|
"dense-chaotic",
|
|
4986
5529
|
"cosmic",
|
|
@@ -4991,8 +5534,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4991
5534
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
4992
5535
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
4993
5536
|
ctx.lineCap = "round";
|
|
5537
|
+
// Collect all energy segments with their computed state
|
|
5538
|
+
const ENERGY_ALPHA_BUCKETS = 3;
|
|
5539
|
+
const energyBuckets = [];
|
|
5540
|
+
for(let b = 0; b < ENERGY_ALPHA_BUCKETS; b++)energyBuckets.push([]);
|
|
5541
|
+
const energyAlphas = new Array(ENERGY_ALPHA_BUCKETS).fill(0);
|
|
4994
5542
|
for(let e = 0; e < energyCount; e++){
|
|
4995
|
-
// Pick a random shape to radiate from
|
|
4996
5543
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
4997
5544
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
4998
5545
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -5004,14 +5551,37 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5004
5551
|
const sy = source.y + Math.sin(angle) * startDist;
|
|
5005
5552
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
5006
5553
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5554
|
+
const eAlpha = 0.04 + rng() * 0.06;
|
|
5555
|
+
const eColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5556
|
+
const eLw = (0.5 + rng() * 1.5) * scaleFactor;
|
|
5557
|
+
// Quantize alpha into bucket
|
|
5558
|
+
const bi = Math.min(ENERGY_ALPHA_BUCKETS - 1, Math.floor((eAlpha - 0.04) / 0.06 * ENERGY_ALPHA_BUCKETS));
|
|
5559
|
+
energyBuckets[bi].push({
|
|
5560
|
+
x1: sx,
|
|
5561
|
+
y1: sy,
|
|
5562
|
+
x2: ex,
|
|
5563
|
+
y2: ey,
|
|
5564
|
+
color: eColor,
|
|
5565
|
+
lw: eLw
|
|
5566
|
+
});
|
|
5567
|
+
energyAlphas[bi] = eAlpha;
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
// Render batched energy lines
|
|
5571
|
+
for(let bi = 0; bi < ENERGY_ALPHA_BUCKETS; bi++){
|
|
5572
|
+
const segs = energyBuckets[bi];
|
|
5573
|
+
if (segs.length === 0) continue;
|
|
5574
|
+
ctx.globalAlpha = energyAlphas[bi];
|
|
5575
|
+
// Use median segment's color and width as representative
|
|
5576
|
+
const rep = segs[Math.floor(segs.length / 2)];
|
|
5577
|
+
ctx.strokeStyle = rep.color;
|
|
5578
|
+
ctx.lineWidth = rep.lw;
|
|
5579
|
+
ctx.beginPath();
|
|
5580
|
+
for(let j = 0; j < segs.length; j++){
|
|
5581
|
+
ctx.moveTo(segs[j].x1, segs[j].y1);
|
|
5582
|
+
ctx.lineTo(segs[j].x2, segs[j].y2);
|
|
5014
5583
|
}
|
|
5584
|
+
ctx.stroke();
|
|
5015
5585
|
}
|
|
5016
5586
|
}
|
|
5017
5587
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
@@ -5034,50 +5604,128 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5034
5604
|
}
|
|
5035
5605
|
ctx.restore();
|
|
5036
5606
|
}
|
|
5037
|
-
// ── 7. Noise texture overlay
|
|
5607
|
+
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
5608
|
+
// Optimized: cap density at large sizes (diminishing returns above ~2K dots),
|
|
5609
|
+
// skip inner pixelScale loop when scale=1, use Uint32Array for faster writes.
|
|
5038
5610
|
const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
|
|
5039
|
-
const
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
const
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5611
|
+
const rawNoiseDensity = Math.floor(width * height / 800);
|
|
5612
|
+
// Cap at 2500 dots — beyond this the visual effect is indistinguishable
|
|
5613
|
+
// but getImageData/putImageData cost scales with canvas size
|
|
5614
|
+
const noiseDensity = Math.min(rawNoiseDensity, 2500);
|
|
5615
|
+
try {
|
|
5616
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5617
|
+
const data = imageData.data;
|
|
5618
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5619
|
+
if (pixelScale === 1) // Fast path — no inner loop, direct pixel write
|
|
5620
|
+
// Pre-compute alpha blend as integer math (avoid float multiply per channel)
|
|
5621
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5622
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5623
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5624
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5625
|
+
// srcA in range [0.01, 0.04] — multiply by 256 for fixed-point
|
|
5626
|
+
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
5627
|
+
const invA256 = 256 - srcA256;
|
|
5628
|
+
const bSrc = brightness * srcA256; // pre-multiply brightness × alpha
|
|
5629
|
+
const idx = ny * width + nx << 2;
|
|
5630
|
+
data[idx] = data[idx] * invA256 + bSrc >> 8;
|
|
5631
|
+
data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
|
|
5632
|
+
data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
|
|
5633
|
+
}
|
|
5634
|
+
else for(let i = 0; i < noiseDensity; i++){
|
|
5635
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5636
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5637
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5638
|
+
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
5639
|
+
const invA256 = 256 - srcA256;
|
|
5640
|
+
const bSrc = brightness * srcA256;
|
|
5641
|
+
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5642
|
+
const idx = (ny + dy) * width + (nx + dx) << 2;
|
|
5643
|
+
data[idx] = data[idx] * invA256 + bSrc >> 8;
|
|
5644
|
+
data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
|
|
5645
|
+
data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
ctx.putImageData(imageData, 0, 0);
|
|
5649
|
+
} catch {
|
|
5650
|
+
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5651
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5652
|
+
const nx = noiseRng() * width;
|
|
5653
|
+
const ny = noiseRng() * height;
|
|
5654
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5655
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5656
|
+
ctx.globalAlpha = alpha;
|
|
5657
|
+
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5658
|
+
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5659
|
+
}
|
|
5048
5660
|
}
|
|
5049
5661
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
5050
5662
|
ctx.globalAlpha = 1;
|
|
5051
5663
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
5052
5664
|
const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
|
|
5665
|
+
// Tint vignette based on background: warm sepia for light, cool blue for dark
|
|
5666
|
+
const isLightBg = bgLum > 0.5;
|
|
5667
|
+
const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
|
|
5668
|
+
: `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
|
|
5053
5669
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
5054
5670
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
5055
|
-
vigGrad.addColorStop(1,
|
|
5671
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
5056
5672
|
ctx.fillStyle = vigGrad;
|
|
5057
5673
|
ctx.fillRect(0, 0, width, height);
|
|
5058
|
-
// ── 9. Organic connecting curves
|
|
5674
|
+
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5675
|
+
// Optimized: batch all curves into alpha-quantized groups to reduce
|
|
5676
|
+
// beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
|
|
5059
5677
|
if (shapePositions.length > 1) {
|
|
5060
5678
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5679
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5061
5680
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5681
|
+
// Collect curves into 3 alpha buckets
|
|
5682
|
+
const CURVE_ALPHA_BUCKETS = 3;
|
|
5683
|
+
const curveBuckets = [];
|
|
5684
|
+
const curveColors = [];
|
|
5685
|
+
const curveAlphas = new Array(CURVE_ALPHA_BUCKETS).fill(0);
|
|
5686
|
+
for(let b = 0; b < CURVE_ALPHA_BUCKETS; b++)curveBuckets.push([]);
|
|
5062
5687
|
for(let i = 0; i < numCurves; i++){
|
|
5063
5688
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
5064
5689
|
const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
|
|
5065
5690
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
5066
5691
|
const a = shapePositions[idxA];
|
|
5067
5692
|
const b = shapePositions[idxB];
|
|
5068
|
-
const mx = (a.x + b.x) / 2;
|
|
5069
|
-
const my = (a.y + b.y) / 2;
|
|
5070
5693
|
const dx = b.x - a.x;
|
|
5071
5694
|
const dy = b.y - a.y;
|
|
5072
5695
|
const dist = Math.hypot(dx, dy);
|
|
5696
|
+
// Skip connections between distant shapes
|
|
5697
|
+
if (dist > maxCurveDist) continue;
|
|
5698
|
+
const mx = (a.x + b.x) / 2;
|
|
5699
|
+
const my = (a.y + b.y) / 2;
|
|
5073
5700
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5074
5701
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5075
5702
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
5076
|
-
|
|
5077
|
-
|
|
5703
|
+
const curveAlpha = 0.06 + rng() * 0.1;
|
|
5704
|
+
const curveColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5705
|
+
const bi = Math.min(CURVE_ALPHA_BUCKETS - 1, Math.floor((curveAlpha - 0.06) / 0.1 * CURVE_ALPHA_BUCKETS));
|
|
5706
|
+
curveBuckets[bi].push({
|
|
5707
|
+
ax: a.x,
|
|
5708
|
+
ay: a.y,
|
|
5709
|
+
cpx: cpx,
|
|
5710
|
+
cpy: cpy,
|
|
5711
|
+
bx: b.x,
|
|
5712
|
+
by: b.y
|
|
5713
|
+
});
|
|
5714
|
+
curveAlphas[bi] = curveAlpha;
|
|
5715
|
+
if (!curveColors[bi]) curveColors[bi] = curveColor;
|
|
5716
|
+
}
|
|
5717
|
+
// Render batched curves
|
|
5718
|
+
for(let bi = 0; bi < CURVE_ALPHA_BUCKETS; bi++){
|
|
5719
|
+
const curves = curveBuckets[bi];
|
|
5720
|
+
if (curves.length === 0) continue;
|
|
5721
|
+
ctx.globalAlpha = curveAlphas[bi];
|
|
5722
|
+
ctx.strokeStyle = curveColors[bi];
|
|
5078
5723
|
ctx.beginPath();
|
|
5079
|
-
|
|
5080
|
-
|
|
5724
|
+
for(let j = 0; j < curves.length; j++){
|
|
5725
|
+
const c = curves[j];
|
|
5726
|
+
ctx.moveTo(c.ax, c.ay);
|
|
5727
|
+
ctx.quadraticCurveTo(c.cpx, c.cpy, c.bx, c.by);
|
|
5728
|
+
}
|
|
5081
5729
|
ctx.stroke();
|
|
5082
5730
|
}
|
|
5083
5731
|
}
|
|
@@ -5195,11 +5843,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5195
5843
|
}
|
|
5196
5844
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5197
5845
|
// Vine tendrils — organic curving lines along edges
|
|
5846
|
+
// Optimized: batch all tendrils into a single path
|
|
5198
5847
|
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5199
5848
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5200
5849
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5201
5850
|
ctx.lineCap = "round";
|
|
5202
5851
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5852
|
+
ctx.beginPath();
|
|
5853
|
+
const leafPositions = [];
|
|
5203
5854
|
for(let t = 0; t < tendrilCount; t++){
|
|
5204
5855
|
// Start from a random edge point
|
|
5205
5856
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -5217,7 +5868,6 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5217
5868
|
tx = width - borderPad;
|
|
5218
5869
|
ty = borderRng() * height;
|
|
5219
5870
|
}
|
|
5220
|
-
ctx.beginPath();
|
|
5221
5871
|
ctx.moveTo(tx, ty);
|
|
5222
5872
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5223
5873
|
for(let s = 0; s < segs; s++){
|
|
@@ -5231,14 +5881,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5231
5881
|
ty = cpy3;
|
|
5232
5882
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5233
5883
|
}
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5884
|
+
// Collect leaf positions for batch fill
|
|
5885
|
+
if (borderRng() < 0.6) leafPositions.push({
|
|
5886
|
+
x: tx,
|
|
5887
|
+
y: ty,
|
|
5888
|
+
r: borderPad * (0.15 + borderRng() * 0.2)
|
|
5889
|
+
});
|
|
5890
|
+
}
|
|
5891
|
+
ctx.stroke();
|
|
5892
|
+
// Batch all leaf dots into a single fill
|
|
5893
|
+
if (leafPositions.length > 0) {
|
|
5894
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5895
|
+
ctx.beginPath();
|
|
5896
|
+
for (const leaf of leafPositions){
|
|
5897
|
+
ctx.moveTo(leaf.x + leaf.r, leaf.y);
|
|
5898
|
+
ctx.arc(leaf.x, leaf.y, leaf.r, 0, Math.PI * 2);
|
|
5241
5899
|
}
|
|
5900
|
+
ctx.fill();
|
|
5242
5901
|
}
|
|
5243
5902
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5244
5903
|
// Star-studded arcs along edges
|
|
@@ -5253,8 +5912,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5253
5912
|
ctx.beginPath();
|
|
5254
5913
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5255
5914
|
ctx.stroke();
|
|
5256
|
-
// Scatter small stars along the border region
|
|
5915
|
+
// Scatter small stars along the border region — batched into single path
|
|
5257
5916
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5917
|
+
ctx.beginPath();
|
|
5258
5918
|
for(let s = 0; s < starCount; s++){
|
|
5259
5919
|
const edge = Math.floor(borderRng() * 4);
|
|
5260
5920
|
let sx, sy;
|
|
@@ -5273,7 +5933,6 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5273
5933
|
}
|
|
5274
5934
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5275
5935
|
// 4-point star
|
|
5276
|
-
ctx.beginPath();
|
|
5277
5936
|
for(let p = 0; p < 8; p++){
|
|
5278
5937
|
const a = p / 8 * Math.PI * 2;
|
|
5279
5938
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -5283,8 +5942,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5283
5942
|
else ctx.lineTo(px2, py2);
|
|
5284
5943
|
}
|
|
5285
5944
|
ctx.closePath();
|
|
5286
|
-
ctx.fill();
|
|
5287
5945
|
}
|
|
5946
|
+
ctx.fill();
|
|
5288
5947
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5289
5948
|
// Thin single rule — understated elegance
|
|
5290
5949
|
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
@@ -5295,13 +5954,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5295
5954
|
// Other archetypes: no border (intentional — not every image needs one)
|
|
5296
5955
|
ctx.restore();
|
|
5297
5956
|
}
|
|
5298
|
-
// ── 11. Signature mark —
|
|
5957
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
5299
5958
|
{
|
|
5300
5959
|
const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
|
|
5301
5960
|
const sigSize = Math.min(width, height) * 0.025;
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
const
|
|
5961
|
+
const sigMargin = sigSize * 2.5;
|
|
5962
|
+
// Find the corner with the lowest local density
|
|
5963
|
+
const cornerCandidates = [
|
|
5964
|
+
{
|
|
5965
|
+
x: sigMargin,
|
|
5966
|
+
y: sigMargin
|
|
5967
|
+
},
|
|
5968
|
+
{
|
|
5969
|
+
x: width - sigMargin,
|
|
5970
|
+
y: sigMargin
|
|
5971
|
+
},
|
|
5972
|
+
{
|
|
5973
|
+
x: sigMargin,
|
|
5974
|
+
y: height - sigMargin
|
|
5975
|
+
},
|
|
5976
|
+
{
|
|
5977
|
+
x: width - sigMargin,
|
|
5978
|
+
y: height - sigMargin
|
|
5979
|
+
}
|
|
5980
|
+
];
|
|
5981
|
+
let bestCorner = cornerCandidates[3]; // default: bottom-right
|
|
5982
|
+
let minDensity = Infinity;
|
|
5983
|
+
for (const corner of cornerCandidates){
|
|
5984
|
+
const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
|
|
5985
|
+
if (density < minDensity) {
|
|
5986
|
+
minDensity = density;
|
|
5987
|
+
bestCorner = corner;
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
const sigX = bestCorner.x;
|
|
5991
|
+
const sigY = bestCorner.y;
|
|
5305
5992
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
5306
5993
|
const sigColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
5307
5994
|
ctx.save();
|