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/main.js
CHANGED
|
@@ -531,13 +531,21 @@ class $d016ad53434219a1$export$ab958c550f521376 {
|
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
// ── Standalone color utilities ──────────────────────────────────────
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
534
|
+
// ── Cached hex→RGB parse — avoids repeated parseInt/substring on hot path ──
|
|
535
|
+
const $d016ad53434219a1$var$_rgbCache = new Map();
|
|
536
|
+
const $d016ad53434219a1$var$_RGB_CACHE_MAX = 512;
|
|
537
|
+
/** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. Cached. */ function $d016ad53434219a1$var$hexToRgb(hex) {
|
|
538
|
+
let cached = $d016ad53434219a1$var$_rgbCache.get(hex);
|
|
539
|
+
if (cached) return cached;
|
|
540
|
+
const c = hex.charAt(0) === "#" ? hex.substring(1) : hex;
|
|
541
|
+
cached = [
|
|
537
542
|
parseInt(c.substring(0, 2), 16),
|
|
538
543
|
parseInt(c.substring(2, 4), 16),
|
|
539
544
|
parseInt(c.substring(4, 6), 16)
|
|
540
545
|
];
|
|
546
|
+
if ($d016ad53434219a1$var$_rgbCache.size >= $d016ad53434219a1$var$_RGB_CACHE_MAX) $d016ad53434219a1$var$_rgbCache.clear();
|
|
547
|
+
$d016ad53434219a1$var$_rgbCache.set(hex, cached);
|
|
548
|
+
return cached;
|
|
541
549
|
}
|
|
542
550
|
/** Format [r, g, b] back to #RRGGBB. */ function $d016ad53434219a1$var$rgbToHex(r, g, b) {
|
|
543
551
|
const clamp = (v)=>Math.max(0, Math.min(255, Math.round(v)));
|
|
@@ -594,7 +602,9 @@ class $d016ad53434219a1$export$ab958c550f521376 {
|
|
|
594
602
|
}
|
|
595
603
|
function $d016ad53434219a1$export$f2121afcad3d553f(hex, alpha) {
|
|
596
604
|
const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex);
|
|
597
|
-
|
|
605
|
+
// Quantize alpha to 3 decimal places without toFixed overhead
|
|
606
|
+
const a = Math.round(alpha * 1000) / 1000;
|
|
607
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
598
608
|
}
|
|
599
609
|
function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
|
|
600
610
|
if (colors.length < 3) return {
|
|
@@ -603,15 +613,17 @@ function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
|
|
|
603
613
|
accent: colors[colors.length - 1] || "#888888",
|
|
604
614
|
all: colors
|
|
605
615
|
};
|
|
606
|
-
// Pick dominant as the color
|
|
616
|
+
// Pick dominant as the color with the highest chroma (saturation × distance from gray)
|
|
617
|
+
// This selects the most visually prominent color rather than the average
|
|
607
618
|
const hsls = colors.map((c)=>$d016ad53434219a1$var$hexToHsl(c));
|
|
608
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
609
619
|
let dominantIdx = 0;
|
|
610
|
-
let
|
|
620
|
+
let maxChroma = -1;
|
|
611
621
|
for(let i = 0; i < hsls.length; i++){
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
622
|
+
// Chroma approximation: saturation × how far lightness is from 50% (gray)
|
|
623
|
+
const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
|
|
624
|
+
const chroma = hsls[i][1] * lightnessVibrancy;
|
|
625
|
+
if (chroma > maxChroma) {
|
|
626
|
+
maxChroma = chroma;
|
|
615
627
|
dominantIdx = i;
|
|
616
628
|
}
|
|
617
629
|
}
|
|
@@ -672,12 +684,21 @@ function $d016ad53434219a1$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
|
672
684
|
const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
|
|
673
685
|
return $d016ad53434219a1$var$hslToHex($d016ad53434219a1$var$shiftHueToward(h, target, amount), s, l);
|
|
674
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Compute relative luminance of a hex color (0 = black, 1 = white).
|
|
689
|
+
* Uses the sRGB luminance formula from WCAG. Cached.
|
|
690
|
+
*/ const $d016ad53434219a1$var$_lumCache = new Map();
|
|
675
691
|
function $d016ad53434219a1$export$5c6e3c2b59b7fbbe(hex) {
|
|
692
|
+
let cached = $d016ad53434219a1$var$_lumCache.get(hex);
|
|
693
|
+
if (cached !== undefined) return cached;
|
|
676
694
|
const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex).map((c)=>{
|
|
677
695
|
const s = c / 255;
|
|
678
696
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
679
697
|
});
|
|
680
|
-
|
|
698
|
+
cached = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
699
|
+
if ($d016ad53434219a1$var$_lumCache.size >= 512) $d016ad53434219a1$var$_lumCache.clear();
|
|
700
|
+
$d016ad53434219a1$var$_lumCache.set(hex, cached);
|
|
701
|
+
return cached;
|
|
681
702
|
}
|
|
682
703
|
function $d016ad53434219a1$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
|
|
683
704
|
const fgLum = $d016ad53434219a1$export$5c6e3c2b59b7fbbe(fgHex);
|
|
@@ -1113,21 +1134,31 @@ const $4bf3d69be49ad55c$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
|
|
|
1113
1134
|
(0, $efc5b85ac9840d51$export$e46c5570db033611)(ctx, size, finalConfig);
|
|
1114
1135
|
const gridSize = 8;
|
|
1115
1136
|
const unit = size / gridSize;
|
|
1137
|
+
const radius = unit / 2;
|
|
1138
|
+
// Pre-compute the 8 star-point angle pairs (cos/sin) — avoids 648 trig calls
|
|
1139
|
+
const starPoints = [];
|
|
1140
|
+
for(let k = 0; k < 8; k++){
|
|
1141
|
+
const angle = Math.PI / 4 * k;
|
|
1142
|
+
const angle2 = angle + Math.PI / 4;
|
|
1143
|
+
starPoints.push({
|
|
1144
|
+
c1: Math.cos(angle) * radius,
|
|
1145
|
+
s1: Math.sin(angle) * radius,
|
|
1146
|
+
c2: Math.cos(angle2) * radius,
|
|
1147
|
+
s2: Math.sin(angle2) * radius
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1116
1150
|
ctx.beginPath();
|
|
1117
1151
|
// Create base grid
|
|
1118
|
-
for(let i = 0; i <= gridSize; i++)
|
|
1152
|
+
for(let i = 0; i <= gridSize; i++){
|
|
1119
1153
|
const x = (i - gridSize / 2) * unit;
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
const y2 = y + radius * Math.sin(angle + Math.PI / 4);
|
|
1129
|
-
ctx.moveTo(x1, y1);
|
|
1130
|
-
ctx.lineTo(x2, y2);
|
|
1154
|
+
for(let j = 0; j <= gridSize; j++){
|
|
1155
|
+
const y = (j - gridSize / 2) * unit;
|
|
1156
|
+
// Draw star pattern at each intersection using pre-computed offsets
|
|
1157
|
+
for(let k = 0; k < 8; k++){
|
|
1158
|
+
const sp = starPoints[k];
|
|
1159
|
+
ctx.moveTo(x + sp.c1, y + sp.s1);
|
|
1160
|
+
ctx.lineTo(x + sp.c2, y + sp.s2);
|
|
1161
|
+
}
|
|
1131
1162
|
}
|
|
1132
1163
|
}
|
|
1133
1164
|
ctx.stroke();
|
|
@@ -1447,20 +1478,23 @@ const $dd5df256f00f6199$export$eeae7765f05012e2 = (ctx, size)=>{
|
|
|
1447
1478
|
const $dd5df256f00f6199$export$3355220a8108efc3 = (ctx, size)=>{
|
|
1448
1479
|
const outerRadius = size / 2;
|
|
1449
1480
|
const innerRadius = size / 4;
|
|
1450
|
-
|
|
1481
|
+
// Adaptive step count: fewer segments for small shapes where detail isn't visible.
|
|
1482
|
+
// 36×36 = 1296 segments at full size; at size < 60 we drop to 16×16 = 256.
|
|
1483
|
+
const steps = size < 60 ? 16 : size < 150 ? 24 : 36;
|
|
1484
|
+
const TWO_PI = Math.PI * 2;
|
|
1485
|
+
const angleStep = TWO_PI / steps;
|
|
1451
1486
|
ctx.beginPath();
|
|
1452
1487
|
for(let i = 0; i < steps; i++){
|
|
1453
|
-
const angle1 = i
|
|
1454
|
-
|
|
1488
|
+
const angle1 = i * angleStep;
|
|
1489
|
+
const cosA = Math.cos(angle1);
|
|
1490
|
+
const sinA = Math.sin(angle1);
|
|
1455
1491
|
for(let j = 0; j < steps; j++){
|
|
1456
|
-
const phi1 = j
|
|
1457
|
-
const phi2 =
|
|
1458
|
-
const
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
ctx.moveTo(x1, y1);
|
|
1463
|
-
ctx.lineTo(x2, y2);
|
|
1492
|
+
const phi1 = j * angleStep;
|
|
1493
|
+
const phi2 = phi1 + angleStep;
|
|
1494
|
+
const r1 = outerRadius + innerRadius * Math.cos(phi1);
|
|
1495
|
+
const r2 = outerRadius + innerRadius * Math.cos(phi2);
|
|
1496
|
+
ctx.moveTo(r1 * cosA, r1 * sinA);
|
|
1497
|
+
ctx.lineTo(r2 * cosA, r2 * sinA);
|
|
1464
1498
|
}
|
|
1465
1499
|
}
|
|
1466
1500
|
};
|
|
@@ -2023,6 +2057,43 @@ const $c3de8257a8baa3b0$var$RENDER_STYLES = [
|
|
|
2023
2057
|
function $c3de8257a8baa3b0$export$9fd4e64b2acd410e(rng) {
|
|
2024
2058
|
return $c3de8257a8baa3b0$var$RENDER_STYLES[Math.floor(rng() * $c3de8257a8baa3b0$var$RENDER_STYLES.length)];
|
|
2025
2059
|
}
|
|
2060
|
+
const $c3de8257a8baa3b0$export$2f738f61a8c15e07 = {
|
|
2061
|
+
"fill-and-stroke": 1,
|
|
2062
|
+
"fill-only": 0.5,
|
|
2063
|
+
"stroke-only": 1,
|
|
2064
|
+
"double-stroke": 1.5,
|
|
2065
|
+
"dashed": 1,
|
|
2066
|
+
"watercolor": 7,
|
|
2067
|
+
"hatched": 3,
|
|
2068
|
+
"incomplete": 1,
|
|
2069
|
+
"stipple": 90,
|
|
2070
|
+
"stencil": 2,
|
|
2071
|
+
"noise-grain": 400,
|
|
2072
|
+
"wood-grain": 10,
|
|
2073
|
+
"marble-vein": 4,
|
|
2074
|
+
"fabric-weave": 6,
|
|
2075
|
+
"hand-drawn": 5
|
|
2076
|
+
};
|
|
2077
|
+
function $c3de8257a8baa3b0$export$909ab0580e273f19(style) {
|
|
2078
|
+
switch(style){
|
|
2079
|
+
case "noise-grain":
|
|
2080
|
+
return "hatched";
|
|
2081
|
+
case "stipple":
|
|
2082
|
+
return "dashed";
|
|
2083
|
+
case "wood-grain":
|
|
2084
|
+
return "hatched";
|
|
2085
|
+
case "watercolor":
|
|
2086
|
+
return "fill-and-stroke";
|
|
2087
|
+
case "fabric-weave":
|
|
2088
|
+
return "hatched";
|
|
2089
|
+
case "hand-drawn":
|
|
2090
|
+
return "fill-and-stroke";
|
|
2091
|
+
case "marble-vein":
|
|
2092
|
+
return "stroke-only";
|
|
2093
|
+
default:
|
|
2094
|
+
return style;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2026
2097
|
function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
2027
2098
|
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
|
|
2028
2099
|
ctx.save();
|
|
@@ -2142,6 +2213,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2142
2213
|
case "hatched":
|
|
2143
2214
|
{
|
|
2144
2215
|
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
2216
|
+
// Optimized: batch all parallel lines into a single path per pass
|
|
2145
2217
|
const savedAlphaH = ctx.globalAlpha;
|
|
2146
2218
|
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
2147
2219
|
ctx.fill();
|
|
@@ -2153,28 +2225,28 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2153
2225
|
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
2154
2226
|
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
2155
2227
|
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
2156
|
-
// Draw parallel lines across the bounding box
|
|
2228
|
+
// Draw parallel lines across the bounding box — batched into single path
|
|
2157
2229
|
const extent = size * 0.8;
|
|
2158
2230
|
const cos = Math.cos(hatchAngle);
|
|
2159
2231
|
const sin = Math.sin(hatchAngle);
|
|
2232
|
+
ctx.beginPath();
|
|
2160
2233
|
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
2161
|
-
ctx.beginPath();
|
|
2162
2234
|
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
2163
2235
|
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
2164
|
-
ctx.stroke();
|
|
2165
2236
|
}
|
|
2237
|
+
ctx.stroke();
|
|
2166
2238
|
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
2167
2239
|
if (!rng || rng() < 0.5) {
|
|
2168
2240
|
const crossAngle = hatchAngle + Math.PI / 2;
|
|
2169
2241
|
const cos2 = Math.cos(crossAngle);
|
|
2170
2242
|
const sin2 = Math.sin(crossAngle);
|
|
2171
2243
|
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
2244
|
+
ctx.beginPath();
|
|
2172
2245
|
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
2173
|
-
ctx.beginPath();
|
|
2174
2246
|
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
2175
2247
|
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
2176
|
-
ctx.stroke();
|
|
2177
2248
|
}
|
|
2249
|
+
ctx.stroke();
|
|
2178
2250
|
}
|
|
2179
2251
|
ctx.restore();
|
|
2180
2252
|
ctx.globalAlpha = savedAlphaH;
|
|
@@ -2212,6 +2284,8 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2212
2284
|
case "stipple":
|
|
2213
2285
|
{
|
|
2214
2286
|
// Dot-fill texture — clip to shape, then scatter dots
|
|
2287
|
+
// Optimized: use fillRect instead of arc for dots (much cheaper to render),
|
|
2288
|
+
// and cap total dot count to avoid O(size²) blowup on large shapes.
|
|
2215
2289
|
const savedAlphaS = ctx.globalAlpha;
|
|
2216
2290
|
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
2217
2291
|
ctx.fill(); // ghost fill
|
|
@@ -2219,16 +2293,20 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2219
2293
|
ctx.save();
|
|
2220
2294
|
ctx.clip();
|
|
2221
2295
|
const dotSpacing = Math.max(2, size * 0.03);
|
|
2222
|
-
const
|
|
2296
|
+
const extentS = size * 0.55;
|
|
2297
|
+
// Cap total dots: beyond ~900 (30×30 grid) the visual density plateaus
|
|
2298
|
+
const maxDotsPerAxis = Math.min(Math.ceil(extentS * 2 / dotSpacing), 30);
|
|
2299
|
+
const actualSpacing = extentS * 2 / maxDotsPerAxis;
|
|
2223
2300
|
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
2224
|
-
for(let
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2301
|
+
for(let xi = 0; xi < maxDotsPerAxis; xi++){
|
|
2302
|
+
const dx = -extentS + xi * actualSpacing;
|
|
2303
|
+
for(let yi = 0; yi < maxDotsPerAxis; yi++){
|
|
2304
|
+
const dy = -extentS + yi * actualSpacing;
|
|
2305
|
+
const jx = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2306
|
+
const jy = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2307
|
+
const dotD = rng ? actualSpacing * (0.3 + rng() * 0.4) : actualSpacing * 0.4;
|
|
2308
|
+
ctx.fillRect(dx + jx - dotD * 0.5, dy + jy - dotD * 0.5, dotD, dotD);
|
|
2309
|
+
}
|
|
2232
2310
|
}
|
|
2233
2311
|
ctx.restore();
|
|
2234
2312
|
ctx.globalAlpha = savedAlphaS;
|
|
@@ -2261,6 +2339,9 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2261
2339
|
case "noise-grain":
|
|
2262
2340
|
{
|
|
2263
2341
|
// Procedural noise grain texture clipped to shape boundary
|
|
2342
|
+
// Optimized: cap grid to max 40×40 = 1600 dots (was unbounded at O(size²)),
|
|
2343
|
+
// quantize alpha into buckets to minimize globalAlpha state changes,
|
|
2344
|
+
// and batch dots by brightness (black/white) × alpha bucket
|
|
2264
2345
|
const savedAlphaN = ctx.globalAlpha;
|
|
2265
2346
|
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
2266
2347
|
ctx.fill(); // base tint
|
|
@@ -2269,17 +2350,47 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2269
2350
|
ctx.clip();
|
|
2270
2351
|
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
2271
2352
|
const extentN = size * 0.55;
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
const
|
|
2276
|
-
const
|
|
2277
|
-
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2353
|
+
if (rng) {
|
|
2354
|
+
// Cap grid to max 40 dots per axis — beyond this the grain is
|
|
2355
|
+
// visually indistinguishable but cost scales quadratically.
|
|
2356
|
+
const maxGrainPerAxis = Math.min(Math.ceil(extentN * 2 / grainSpacing), 40);
|
|
2357
|
+
const actualGrainSpacing = extentN * 2 / maxGrainPerAxis;
|
|
2358
|
+
// 4 alpha buckets: 0.2, 0.3, 0.4, 0.5 — covers the 0.15-0.50 range
|
|
2359
|
+
const BUCKETS = 4;
|
|
2360
|
+
const bucketMin = 0.15;
|
|
2361
|
+
const bucketRange = 0.35;
|
|
2362
|
+
// [black_bucket0, black_bucket1, ..., white_bucket0, ...]
|
|
2363
|
+
const buckets = [];
|
|
2364
|
+
for(let i = 0; i < BUCKETS * 2; i++)buckets.push([]);
|
|
2365
|
+
for(let xi = 0; xi < maxGrainPerAxis; xi++){
|
|
2366
|
+
const gx = -extentN + xi * actualGrainSpacing;
|
|
2367
|
+
for(let yi = 0; yi < maxGrainPerAxis; yi++){
|
|
2368
|
+
const gy = -extentN + yi * actualGrainSpacing;
|
|
2369
|
+
const jx = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2370
|
+
const jy = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2371
|
+
const isWhite = rng() > 0.5;
|
|
2372
|
+
const dotAlpha = bucketMin + rng() * bucketRange;
|
|
2373
|
+
const dotSize = actualGrainSpacing * (0.3 + rng() * 0.5);
|
|
2374
|
+
const bucketIdx = Math.min(BUCKETS - 1, Math.floor((dotAlpha - bucketMin) / bucketRange * BUCKETS));
|
|
2375
|
+
const offset = isWhite ? BUCKETS : 0;
|
|
2376
|
+
buckets[offset + bucketIdx].push({
|
|
2377
|
+
x: gx + jx,
|
|
2378
|
+
y: gy + jy,
|
|
2379
|
+
s: dotSize
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
// Render each bucket: 2 colors × 4 alpha levels = 8 state changes total
|
|
2384
|
+
for(let color = 0; color < 2; color++){
|
|
2385
|
+
ctx.fillStyle = color === 0 ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
2386
|
+
for(let b = 0; b < BUCKETS; b++){
|
|
2387
|
+
const dots = buckets[color * BUCKETS + b];
|
|
2388
|
+
if (dots.length === 0) continue;
|
|
2389
|
+
const alpha = bucketMin + (b + 0.5) / BUCKETS * bucketRange;
|
|
2390
|
+
ctx.globalAlpha = savedAlphaN * alpha;
|
|
2391
|
+
for(let i = 0; i < dots.length; i++)ctx.fillRect(dots[i].x, dots[i].y, dots[i].s, dots[i].s);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2283
2394
|
}
|
|
2284
2395
|
ctx.restore();
|
|
2285
2396
|
ctx.fillStyle = fillColor;
|
|
@@ -2292,6 +2403,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2292
2403
|
case "wood-grain":
|
|
2293
2404
|
{
|
|
2294
2405
|
// Parallel wavy lines simulating wood grain, clipped to shape
|
|
2406
|
+
// Optimized: batch all grain lines into a single path, increased step from 2 to 4
|
|
2295
2407
|
const savedAlphaW = ctx.globalAlpha;
|
|
2296
2408
|
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
2297
2409
|
ctx.fill(); // base tint
|
|
@@ -2307,17 +2419,19 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2307
2419
|
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
2308
2420
|
const cosG = Math.cos(grainAngle);
|
|
2309
2421
|
const sinG = Math.sin(grainAngle);
|
|
2422
|
+
const waveCoeff = waveFreq * Math.PI;
|
|
2423
|
+
const invExtentW = 1 / extentW;
|
|
2424
|
+
// Batch all grain lines into a single path
|
|
2425
|
+
ctx.beginPath();
|
|
2310
2426
|
for(let d = -extentW; d <= extentW; d += grainLineSpacing){
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
const
|
|
2315
|
-
|
|
2316
|
-
if (t === -extentW) ctx.moveTo(px, py);
|
|
2317
|
-
else ctx.lineTo(px, py);
|
|
2427
|
+
const firstWave = Math.sin(-extentW * invExtentW * waveCoeff) * waveAmp;
|
|
2428
|
+
ctx.moveTo(-extentW * cosG - (d + firstWave) * sinG, -extentW * sinG + (d + firstWave) * cosG);
|
|
2429
|
+
for(let t = -extentW + 4; t <= extentW; t += 4){
|
|
2430
|
+
const wave = Math.sin(t * invExtentW * waveCoeff) * waveAmp;
|
|
2431
|
+
ctx.lineTo(t * cosG - (d + wave) * sinG, t * sinG + (d + wave) * cosG);
|
|
2318
2432
|
}
|
|
2319
|
-
ctx.stroke();
|
|
2320
2433
|
}
|
|
2434
|
+
ctx.stroke();
|
|
2321
2435
|
ctx.restore();
|
|
2322
2436
|
ctx.globalAlpha = savedAlphaW;
|
|
2323
2437
|
ctx.globalAlpha *= 0.35;
|
|
@@ -2379,6 +2493,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2379
2493
|
case "fabric-weave":
|
|
2380
2494
|
{
|
|
2381
2495
|
// Interlocking horizontal/vertical threads clipped to shape
|
|
2496
|
+
// Optimized: batch all horizontal threads into one path, all vertical into another
|
|
2382
2497
|
const savedAlphaF = ctx.globalAlpha;
|
|
2383
2498
|
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
2384
2499
|
ctx.fill(); // ghost base
|
|
@@ -2388,26 +2503,24 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2388
2503
|
const threadSpacing = Math.max(2, size * 0.04);
|
|
2389
2504
|
const extentF = size * 0.55;
|
|
2390
2505
|
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
2506
|
+
// Horizontal threads — batched
|
|
2391
2507
|
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
2392
|
-
|
|
2508
|
+
ctx.beginPath();
|
|
2393
2509
|
for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2394
|
-
ctx.beginPath();
|
|
2395
2510
|
ctx.moveTo(-extentF, y);
|
|
2396
2511
|
ctx.lineTo(extentF, y);
|
|
2397
|
-
ctx.stroke();
|
|
2398
2512
|
}
|
|
2399
|
-
|
|
2513
|
+
ctx.stroke();
|
|
2514
|
+
// Vertical threads (offset by half spacing for weave effect) — batched
|
|
2400
2515
|
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
2401
2516
|
ctx.strokeStyle = fillColor;
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
ctx.lineTo(x, y + threadSpacing);
|
|
2408
|
-
}
|
|
2409
|
-
ctx.stroke();
|
|
2517
|
+
ctx.beginPath();
|
|
2518
|
+
for(let x = -extentF; x <= extentF; x += threadSpacing * 2)for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2519
|
+
// Over-under: draw segment, skip segment
|
|
2520
|
+
ctx.moveTo(x, y);
|
|
2521
|
+
ctx.lineTo(x, y + threadSpacing);
|
|
2410
2522
|
}
|
|
2523
|
+
ctx.stroke();
|
|
2411
2524
|
ctx.strokeStyle = strokeColor;
|
|
2412
2525
|
ctx.restore();
|
|
2413
2526
|
ctx.globalAlpha = savedAlphaF;
|
|
@@ -2473,14 +2586,17 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2473
2586
|
ctx.translate(x, y);
|
|
2474
2587
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2475
2588
|
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2476
|
-
|
|
2589
|
+
// Skip shadow entirely for small shapes (< 20px) — the blur is expensive
|
|
2590
|
+
// and visually imperceptible at that scale.
|
|
2591
|
+
const useShadow = size >= 20;
|
|
2592
|
+
if (useShadow && lightAngle !== undefined) {
|
|
2477
2593
|
const shadowDist = size * 0.035;
|
|
2478
2594
|
const shadowBlurR = size * 0.06;
|
|
2479
2595
|
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2480
2596
|
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2481
2597
|
ctx.shadowBlur = shadowBlurR;
|
|
2482
2598
|
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2483
|
-
} else if (glowRadius > 0) {
|
|
2599
|
+
} else if (useShadow && glowRadius > 0) {
|
|
2484
2600
|
// Glow / shadow effect (legacy path)
|
|
2485
2601
|
ctx.shadowBlur = glowRadius;
|
|
2486
2602
|
ctx.shadowColor = glowColor || fillColor;
|
|
@@ -2504,17 +2620,24 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2504
2620
|
$c3de8257a8baa3b0$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2505
2621
|
}
|
|
2506
2622
|
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2623
|
+
// Only reset if we actually set shadow (avoids unnecessary state changes)
|
|
2624
|
+
if (useShadow && (lightAngle !== undefined || glowRadius > 0)) {
|
|
2625
|
+
ctx.shadowBlur = 0;
|
|
2626
|
+
ctx.shadowOffsetX = 0;
|
|
2627
|
+
ctx.shadowOffsetY = 0;
|
|
2628
|
+
ctx.shadowColor = "transparent";
|
|
2629
|
+
}
|
|
2630
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2631
|
+
// Skip for small shapes (< 30px) — gradient creation + composite op
|
|
2632
|
+
// switch is expensive and the highlight is invisible at small sizes.
|
|
2633
|
+
if (lightAngle !== undefined && size > 30 && rng) {
|
|
2513
2634
|
const hlRadius = size * 0.35;
|
|
2514
2635
|
const hlDist = size * 0.15;
|
|
2515
2636
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2516
2637
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2517
2638
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2639
|
+
// Use a simple white highlight — the per-shape hex parse was expensive
|
|
2640
|
+
// and the visual difference from tinted highlights is negligible.
|
|
2518
2641
|
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2519
2642
|
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2520
2643
|
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
@@ -3579,6 +3702,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3579
3702
|
"watercolor",
|
|
3580
3703
|
"fill-only"
|
|
3581
3704
|
],
|
|
3705
|
+
preferredCompositions: [
|
|
3706
|
+
"clustered",
|
|
3707
|
+
"flow-field",
|
|
3708
|
+
"radial"
|
|
3709
|
+
],
|
|
3582
3710
|
flowLineMultiplier: 2.5,
|
|
3583
3711
|
heroShape: false,
|
|
3584
3712
|
glowMultiplier: 0.5,
|
|
@@ -3600,6 +3728,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3600
3728
|
"stroke-only",
|
|
3601
3729
|
"incomplete"
|
|
3602
3730
|
],
|
|
3731
|
+
preferredCompositions: [
|
|
3732
|
+
"golden-spiral",
|
|
3733
|
+
"grid-subdivision"
|
|
3734
|
+
],
|
|
3603
3735
|
flowLineMultiplier: 0.3,
|
|
3604
3736
|
heroShape: true,
|
|
3605
3737
|
glowMultiplier: 0,
|
|
@@ -3621,6 +3753,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3621
3753
|
"fill-only",
|
|
3622
3754
|
"incomplete"
|
|
3623
3755
|
],
|
|
3756
|
+
preferredCompositions: [
|
|
3757
|
+
"flow-field",
|
|
3758
|
+
"golden-spiral",
|
|
3759
|
+
"spiral"
|
|
3760
|
+
],
|
|
3624
3761
|
flowLineMultiplier: 4,
|
|
3625
3762
|
heroShape: false,
|
|
3626
3763
|
glowMultiplier: 0.3,
|
|
@@ -3643,6 +3780,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3643
3780
|
"double-stroke",
|
|
3644
3781
|
"hatched"
|
|
3645
3782
|
],
|
|
3783
|
+
preferredCompositions: [
|
|
3784
|
+
"grid-subdivision",
|
|
3785
|
+
"radial"
|
|
3786
|
+
],
|
|
3646
3787
|
flowLineMultiplier: 0,
|
|
3647
3788
|
heroShape: false,
|
|
3648
3789
|
glowMultiplier: 0,
|
|
@@ -3664,6 +3805,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3664
3805
|
"incomplete",
|
|
3665
3806
|
"fill-only"
|
|
3666
3807
|
],
|
|
3808
|
+
preferredCompositions: [
|
|
3809
|
+
"golden-spiral",
|
|
3810
|
+
"radial",
|
|
3811
|
+
"spiral"
|
|
3812
|
+
],
|
|
3667
3813
|
flowLineMultiplier: 1.5,
|
|
3668
3814
|
heroShape: true,
|
|
3669
3815
|
glowMultiplier: 2,
|
|
@@ -3684,6 +3830,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3684
3830
|
"fill-and-stroke",
|
|
3685
3831
|
"double-stroke"
|
|
3686
3832
|
],
|
|
3833
|
+
preferredCompositions: [
|
|
3834
|
+
"grid-subdivision",
|
|
3835
|
+
"golden-spiral"
|
|
3836
|
+
],
|
|
3687
3837
|
flowLineMultiplier: 0,
|
|
3688
3838
|
heroShape: true,
|
|
3689
3839
|
glowMultiplier: 0,
|
|
@@ -3705,6 +3855,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3705
3855
|
"double-stroke",
|
|
3706
3856
|
"dashed"
|
|
3707
3857
|
],
|
|
3858
|
+
preferredCompositions: [
|
|
3859
|
+
"radial",
|
|
3860
|
+
"spiral",
|
|
3861
|
+
"clustered"
|
|
3862
|
+
],
|
|
3708
3863
|
flowLineMultiplier: 2,
|
|
3709
3864
|
heroShape: true,
|
|
3710
3865
|
glowMultiplier: 3,
|
|
@@ -3727,6 +3882,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3727
3882
|
"stroke-only",
|
|
3728
3883
|
"dashed"
|
|
3729
3884
|
],
|
|
3885
|
+
preferredCompositions: [
|
|
3886
|
+
"flow-field",
|
|
3887
|
+
"grid-subdivision",
|
|
3888
|
+
"clustered"
|
|
3889
|
+
],
|
|
3730
3890
|
flowLineMultiplier: 1.5,
|
|
3731
3891
|
heroShape: false,
|
|
3732
3892
|
glowMultiplier: 0,
|
|
@@ -3748,6 +3908,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3748
3908
|
"watercolor",
|
|
3749
3909
|
"fill-and-stroke"
|
|
3750
3910
|
],
|
|
3911
|
+
preferredCompositions: [
|
|
3912
|
+
"radial",
|
|
3913
|
+
"spiral",
|
|
3914
|
+
"golden-spiral"
|
|
3915
|
+
],
|
|
3751
3916
|
flowLineMultiplier: 3,
|
|
3752
3917
|
heroShape: true,
|
|
3753
3918
|
glowMultiplier: 2.5,
|
|
@@ -3769,6 +3934,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3769
3934
|
"fill-only",
|
|
3770
3935
|
"incomplete"
|
|
3771
3936
|
],
|
|
3937
|
+
preferredCompositions: [
|
|
3938
|
+
"golden-spiral",
|
|
3939
|
+
"flow-field",
|
|
3940
|
+
"radial"
|
|
3941
|
+
],
|
|
3772
3942
|
flowLineMultiplier: 0.5,
|
|
3773
3943
|
heroShape: false,
|
|
3774
3944
|
glowMultiplier: 0.3,
|
|
@@ -3790,6 +3960,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3790
3960
|
"stroke-only",
|
|
3791
3961
|
"dashed"
|
|
3792
3962
|
],
|
|
3963
|
+
preferredCompositions: [
|
|
3964
|
+
"grid-subdivision",
|
|
3965
|
+
"radial"
|
|
3966
|
+
],
|
|
3793
3967
|
flowLineMultiplier: 0,
|
|
3794
3968
|
heroShape: false,
|
|
3795
3969
|
glowMultiplier: 0,
|
|
@@ -3811,6 +3985,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3811
3985
|
"fill-only",
|
|
3812
3986
|
"double-stroke"
|
|
3813
3987
|
],
|
|
3988
|
+
preferredCompositions: [
|
|
3989
|
+
"grid-subdivision",
|
|
3990
|
+
"clustered"
|
|
3991
|
+
],
|
|
3814
3992
|
flowLineMultiplier: 0,
|
|
3815
3993
|
heroShape: true,
|
|
3816
3994
|
glowMultiplier: 0,
|
|
@@ -3832,6 +4010,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3832
4010
|
"watercolor",
|
|
3833
4011
|
"fill-only"
|
|
3834
4012
|
],
|
|
4013
|
+
preferredCompositions: [
|
|
4014
|
+
"radial",
|
|
4015
|
+
"golden-spiral",
|
|
4016
|
+
"flow-field"
|
|
4017
|
+
],
|
|
3835
4018
|
flowLineMultiplier: 1,
|
|
3836
4019
|
heroShape: true,
|
|
3837
4020
|
glowMultiplier: 1,
|
|
@@ -3853,6 +4036,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3853
4036
|
"stroke-only",
|
|
3854
4037
|
"fill-only"
|
|
3855
4038
|
],
|
|
4039
|
+
preferredCompositions: [
|
|
4040
|
+
"clustered",
|
|
4041
|
+
"grid-subdivision",
|
|
4042
|
+
"radial"
|
|
4043
|
+
],
|
|
3856
4044
|
flowLineMultiplier: 0,
|
|
3857
4045
|
heroShape: false,
|
|
3858
4046
|
glowMultiplier: 0.3,
|
|
@@ -3874,6 +4062,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3874
4062
|
"fill-only",
|
|
3875
4063
|
"incomplete"
|
|
3876
4064
|
],
|
|
4065
|
+
preferredCompositions: [
|
|
4066
|
+
"flow-field",
|
|
4067
|
+
"golden-spiral",
|
|
4068
|
+
"spiral"
|
|
4069
|
+
],
|
|
3877
4070
|
flowLineMultiplier: 3,
|
|
3878
4071
|
heroShape: true,
|
|
3879
4072
|
glowMultiplier: 0.2,
|
|
@@ -3895,6 +4088,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3895
4088
|
"fill-only",
|
|
3896
4089
|
"hatched"
|
|
3897
4090
|
],
|
|
4091
|
+
preferredCompositions: [
|
|
4092
|
+
"radial",
|
|
4093
|
+
"clustered",
|
|
4094
|
+
"flow-field"
|
|
4095
|
+
],
|
|
3898
4096
|
flowLineMultiplier: 0,
|
|
3899
4097
|
heroShape: false,
|
|
3900
4098
|
glowMultiplier: 0,
|
|
@@ -3917,6 +4115,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3917
4115
|
"stroke-only",
|
|
3918
4116
|
"incomplete"
|
|
3919
4117
|
],
|
|
4118
|
+
preferredCompositions: [
|
|
4119
|
+
"spiral",
|
|
4120
|
+
"radial",
|
|
4121
|
+
"golden-spiral"
|
|
4122
|
+
],
|
|
3920
4123
|
flowLineMultiplier: 2,
|
|
3921
4124
|
heroShape: true,
|
|
3922
4125
|
glowMultiplier: 2.5,
|
|
@@ -3940,6 +4143,12 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3940
4143
|
...b.preferredStyles
|
|
3941
4144
|
])
|
|
3942
4145
|
];
|
|
4146
|
+
const mergedCompositions = [
|
|
4147
|
+
...new Set([
|
|
4148
|
+
...a.preferredCompositions,
|
|
4149
|
+
...b.preferredCompositions
|
|
4150
|
+
])
|
|
4151
|
+
];
|
|
3943
4152
|
return {
|
|
3944
4153
|
name: `${a.name}+${b.name}`,
|
|
3945
4154
|
gridSize: Math.round($f89bc858f7202849$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3951,6 +4160,7 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3951
4160
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3952
4161
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3953
4162
|
preferredStyles: mergedStyles,
|
|
4163
|
+
preferredCompositions: mergedCompositions,
|
|
3954
4164
|
flowLineMultiplier: $f89bc858f7202849$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3955
4165
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3956
4166
|
glowMultiplier: $f89bc858f7202849$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3972,6 +4182,46 @@ function $f89bc858f7202849$export$f1142fd7da4d6590(rng) {
|
|
|
3972
4182
|
}
|
|
3973
4183
|
|
|
3974
4184
|
|
|
4185
|
+
// ── Render style cost weights (normalized: fill-and-stroke = 1) ─────
|
|
4186
|
+
// Based on benchmark measurements. Used by the complexity budget to
|
|
4187
|
+
// cap total rendering work and downgrade expensive styles when needed.
|
|
4188
|
+
const $4f72c5a314eddf25$var$RENDER_STYLE_COST = {
|
|
4189
|
+
"fill-and-stroke": 1,
|
|
4190
|
+
"fill-only": 0.5,
|
|
4191
|
+
"stroke-only": 1,
|
|
4192
|
+
"double-stroke": 1.5,
|
|
4193
|
+
"dashed": 1,
|
|
4194
|
+
"watercolor": 7,
|
|
4195
|
+
"hatched": 3,
|
|
4196
|
+
"incomplete": 1,
|
|
4197
|
+
"stipple": 90,
|
|
4198
|
+
"stencil": 2,
|
|
4199
|
+
"noise-grain": 400,
|
|
4200
|
+
"wood-grain": 10,
|
|
4201
|
+
"marble-vein": 4,
|
|
4202
|
+
"fabric-weave": 6,
|
|
4203
|
+
"hand-drawn": 5
|
|
4204
|
+
};
|
|
4205
|
+
function $4f72c5a314eddf25$var$downgradeRenderStyle(style) {
|
|
4206
|
+
switch(style){
|
|
4207
|
+
case "noise-grain":
|
|
4208
|
+
return "hatched";
|
|
4209
|
+
case "stipple":
|
|
4210
|
+
return "dashed";
|
|
4211
|
+
case "wood-grain":
|
|
4212
|
+
return "hatched";
|
|
4213
|
+
case "watercolor":
|
|
4214
|
+
return "fill-and-stroke";
|
|
4215
|
+
case "fabric-weave":
|
|
4216
|
+
return "hatched";
|
|
4217
|
+
case "hand-drawn":
|
|
4218
|
+
return "fill-and-stroke";
|
|
4219
|
+
case "marble-vein":
|
|
4220
|
+
return "stroke-only";
|
|
4221
|
+
default:
|
|
4222
|
+
return style;
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
3975
4225
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
3976
4226
|
const $4f72c5a314eddf25$var$SACRED_SHAPES = [
|
|
3977
4227
|
"mandala",
|
|
@@ -3984,7 +4234,8 @@ const $4f72c5a314eddf25$var$SACRED_SHAPES = [
|
|
|
3984
4234
|
"torus",
|
|
3985
4235
|
"eggOfLife"
|
|
3986
4236
|
];
|
|
3987
|
-
|
|
4237
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4238
|
+
const $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES = [
|
|
3988
4239
|
"radial",
|
|
3989
4240
|
"flow-field",
|
|
3990
4241
|
"spiral",
|
|
@@ -4086,7 +4337,69 @@ function $4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones) {
|
|
|
4086
4337
|
}
|
|
4087
4338
|
return false;
|
|
4088
4339
|
}
|
|
4089
|
-
// ──
|
|
4340
|
+
// ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
|
|
4341
|
+
class $4f72c5a314eddf25$var$SpatialGrid {
|
|
4342
|
+
cells;
|
|
4343
|
+
cellSize;
|
|
4344
|
+
constructor(cellSize){
|
|
4345
|
+
this.cells = new Map();
|
|
4346
|
+
this.cellSize = cellSize;
|
|
4347
|
+
}
|
|
4348
|
+
key(cx, cy) {
|
|
4349
|
+
return `${cx},${cy}`;
|
|
4350
|
+
}
|
|
4351
|
+
insert(item) {
|
|
4352
|
+
const cx = Math.floor(item.x / this.cellSize);
|
|
4353
|
+
const cy = Math.floor(item.y / this.cellSize);
|
|
4354
|
+
const k = this.key(cx, cy);
|
|
4355
|
+
const cell = this.cells.get(k);
|
|
4356
|
+
if (cell) cell.push(item);
|
|
4357
|
+
else this.cells.set(k, [
|
|
4358
|
+
item
|
|
4359
|
+
]);
|
|
4360
|
+
}
|
|
4361
|
+
/** Count items within radius of (x, y) */ countNear(x, y, radius) {
|
|
4362
|
+
const r2 = radius * radius;
|
|
4363
|
+
const minCx = Math.floor((x - radius) / this.cellSize);
|
|
4364
|
+
const maxCx = Math.floor((x + radius) / this.cellSize);
|
|
4365
|
+
const minCy = Math.floor((y - radius) / this.cellSize);
|
|
4366
|
+
const maxCy = Math.floor((y + radius) / this.cellSize);
|
|
4367
|
+
let count = 0;
|
|
4368
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4369
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4370
|
+
if (!cell) continue;
|
|
4371
|
+
for (const p of cell){
|
|
4372
|
+
const dx = x - p.x;
|
|
4373
|
+
const dy = y - p.y;
|
|
4374
|
+
if (dx * dx + dy * dy < r2) count++;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
return count;
|
|
4378
|
+
}
|
|
4379
|
+
/** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
|
|
4380
|
+
const minCx = Math.floor((x - searchRadius) / this.cellSize);
|
|
4381
|
+
const maxCx = Math.floor((x + searchRadius) / this.cellSize);
|
|
4382
|
+
const minCy = Math.floor((y - searchRadius) / this.cellSize);
|
|
4383
|
+
const maxCy = Math.floor((y + searchRadius) / this.cellSize);
|
|
4384
|
+
let nearest = null;
|
|
4385
|
+
let bestDist2 = Infinity;
|
|
4386
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4387
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4388
|
+
if (!cell) continue;
|
|
4389
|
+
for (const p of cell){
|
|
4390
|
+
const dx = x - p.x;
|
|
4391
|
+
const dy = y - p.y;
|
|
4392
|
+
const d2 = dx * dx + dy * dy;
|
|
4393
|
+
if (d2 > 0 && d2 < bestDist2) {
|
|
4394
|
+
bestDist2 = d2;
|
|
4395
|
+
nearest = p;
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
return nearest;
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
// ── Helper: density check (legacy wrapper) ──────────────────────────
|
|
4090
4403
|
function $4f72c5a314eddf25$var$localDensity(x, y, positions, radius) {
|
|
4091
4404
|
let count = 0;
|
|
4092
4405
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4385,42 +4698,43 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4385
4698
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4386
4699
|
const patternColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4387
4700
|
if (bgPatternRoll < 0.2) {
|
|
4388
|
-
// Dot grid
|
|
4701
|
+
// Dot grid — batched into a single path
|
|
4389
4702
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4390
4703
|
const dotR = dotSpacing * 0.08;
|
|
4391
4704
|
ctx.globalAlpha = patternOpacity;
|
|
4392
4705
|
ctx.fillStyle = patternColor;
|
|
4706
|
+
ctx.beginPath();
|
|
4393
4707
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4394
|
-
ctx.
|
|
4708
|
+
ctx.moveTo(px + dotR, py);
|
|
4395
4709
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4396
|
-
ctx.fill();
|
|
4397
4710
|
}
|
|
4711
|
+
ctx.fill();
|
|
4398
4712
|
} else if (bgPatternRoll < 0.4) {
|
|
4399
|
-
// Diagonal lines
|
|
4713
|
+
// Diagonal lines — batched into a single path
|
|
4400
4714
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4401
4715
|
ctx.globalAlpha = patternOpacity;
|
|
4402
4716
|
ctx.strokeStyle = patternColor;
|
|
4403
4717
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4404
4718
|
const diag = Math.hypot(width, height);
|
|
4719
|
+
ctx.beginPath();
|
|
4405
4720
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4406
|
-
ctx.beginPath();
|
|
4407
4721
|
ctx.moveTo(d, 0);
|
|
4408
4722
|
ctx.lineTo(d + height, height);
|
|
4409
|
-
ctx.stroke();
|
|
4410
4723
|
}
|
|
4724
|
+
ctx.stroke();
|
|
4411
4725
|
} else {
|
|
4412
|
-
// Tessellation — hexagonal grid
|
|
4726
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4413
4727
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4414
4728
|
const tessH = tessSize * Math.sqrt(3);
|
|
4415
4729
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4416
4730
|
ctx.strokeStyle = patternColor;
|
|
4417
4731
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4732
|
+
ctx.beginPath();
|
|
4418
4733
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4419
4734
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4420
4735
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4421
4736
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4422
4737
|
const hy = row * tessH;
|
|
4423
|
-
ctx.beginPath();
|
|
4424
4738
|
for(let s = 0; s < 6; s++){
|
|
4425
4739
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4426
4740
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4429,18 +4743,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4429
4743
|
else ctx.lineTo(vx, vy);
|
|
4430
4744
|
}
|
|
4431
4745
|
ctx.closePath();
|
|
4432
|
-
ctx.stroke();
|
|
4433
4746
|
}
|
|
4434
4747
|
}
|
|
4748
|
+
ctx.stroke();
|
|
4435
4749
|
}
|
|
4436
4750
|
ctx.restore();
|
|
4437
4751
|
}
|
|
4438
4752
|
ctx.globalCompositeOperation = "source-over";
|
|
4439
|
-
// ── 2. Composition mode
|
|
4440
|
-
const compositionMode = $4f72c5a314eddf25$var$
|
|
4753
|
+
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4754
|
+
const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES.length)];
|
|
4441
4755
|
const symRoll = rng();
|
|
4442
4756
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4443
|
-
// ── 3. Focal points + void zones
|
|
4757
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4444
4758
|
const THIRDS_POINTS = [
|
|
4445
4759
|
{
|
|
4446
4760
|
x: 1 / 3,
|
|
@@ -4473,9 +4787,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4473
4787
|
y: height * (0.2 + rng() * 0.6),
|
|
4474
4788
|
strength: 0.3 + rng() * 0.4
|
|
4475
4789
|
});
|
|
4476
|
-
|
|
4790
|
+
// Archetype-aware void zones: dense archetypes get fewer/no voids,
|
|
4791
|
+
// minimal archetypes get golden-ratio positioned voids
|
|
4792
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4793
|
+
const isMinimalArchetype = archetype.gridSize <= 3;
|
|
4794
|
+
const isDenseArchetype = archetype.gridSize >= 8;
|
|
4795
|
+
const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
|
|
4477
4796
|
const voidZones = [];
|
|
4478
|
-
for(let v = 0; v < numVoids; v++)
|
|
4797
|
+
for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
|
|
4798
|
+
// Place voids at golden-ratio positions for intentional negative space
|
|
4799
|
+
const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
|
|
4800
|
+
const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
|
|
4801
|
+
voidZones.push({
|
|
4802
|
+
x: width * (gx + (rng() - 0.5) * 0.05),
|
|
4803
|
+
y: height * (gy + (rng() - 0.5) * 0.05),
|
|
4804
|
+
radius: Math.min(width, height) * (0.08 + rng() * 0.08)
|
|
4805
|
+
});
|
|
4806
|
+
} else voidZones.push({
|
|
4479
4807
|
x: width * (0.15 + rng() * 0.7),
|
|
4480
4808
|
y: height * (0.15 + rng() * 0.7),
|
|
4481
4809
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4505,19 +4833,20 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4505
4833
|
ctx.beginPath();
|
|
4506
4834
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
4507
4835
|
ctx.stroke();
|
|
4508
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
4836
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
4509
4837
|
if (rng() < 0.5) {
|
|
4510
4838
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
4511
4839
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
4512
4840
|
ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
4841
|
+
ctx.beginPath();
|
|
4513
4842
|
for(let d = 0; d < dotCount; d++){
|
|
4514
4843
|
const angle = rng() * Math.PI * 2;
|
|
4515
4844
|
const dist = rng() * zone.radius * 0.7;
|
|
4516
4845
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
4517
|
-
ctx.
|
|
4846
|
+
ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
|
|
4518
4847
|
ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
|
|
4519
|
-
ctx.fill();
|
|
4520
4848
|
}
|
|
4849
|
+
ctx.fill();
|
|
4521
4850
|
}
|
|
4522
4851
|
// ~30% chance: thin concentric ring inside
|
|
4523
4852
|
if (rng() < 0.3) {
|
|
@@ -4552,6 +4881,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4552
4881
|
}
|
|
4553
4882
|
// Track all placed shapes for density checks and connecting curves
|
|
4554
4883
|
const shapePositions = [];
|
|
4884
|
+
// Spatial grid for O(1) density and nearest-neighbor lookups
|
|
4885
|
+
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4886
|
+
const spatialGrid = new $4f72c5a314eddf25$var$SpatialGrid(densityCheckRadius);
|
|
4555
4887
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4556
4888
|
let heroCenter = null;
|
|
4557
4889
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4597,10 +4929,35 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4597
4929
|
size: heroSize,
|
|
4598
4930
|
shape: heroShape
|
|
4599
4931
|
});
|
|
4932
|
+
spatialGrid.insert({
|
|
4933
|
+
x: heroFocal.x,
|
|
4934
|
+
y: heroFocal.y,
|
|
4935
|
+
size: heroSize,
|
|
4936
|
+
shape: heroShape
|
|
4937
|
+
});
|
|
4600
4938
|
}
|
|
4601
4939
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4602
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4603
4940
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4941
|
+
// ── Complexity budget — caps total rendering work ──────────────
|
|
4942
|
+
// Budget scales with pixel area so larger canvases get proportionally
|
|
4943
|
+
// more headroom. The multiplier extras (glazing, echoes, nesting,
|
|
4944
|
+
// constellations, rhythm) are gated behind the budget; when it runs
|
|
4945
|
+
// low they are skipped. When it's exhausted, expensive render styles
|
|
4946
|
+
// are downgraded to cheaper alternatives.
|
|
4947
|
+
//
|
|
4948
|
+
// RNG values are always consumed even when skipping, so the
|
|
4949
|
+
// deterministic sequence for shapes that *do* render is preserved.
|
|
4950
|
+
const pixelArea = width * height;
|
|
4951
|
+
const BUDGET_PER_MEGAPIXEL = 6000; // cost units per 1M pixels
|
|
4952
|
+
let complexityBudget = pixelArea / 1000000 * BUDGET_PER_MEGAPIXEL;
|
|
4953
|
+
const totalBudget = complexityBudget;
|
|
4954
|
+
const budgetForExtras = complexityBudget * 0.25; // reserve 25% for multiplier extras
|
|
4955
|
+
let extrasSpent = 0;
|
|
4956
|
+
// Hard cap on clip-heavy render styles (stipple, noise-grain).
|
|
4957
|
+
// These generate O(size²) fillRect calls per shape and dominate
|
|
4958
|
+
// worst-case render time. Cap scales with pixel area.
|
|
4959
|
+
const MAX_CLIP_HEAVY_SHAPES = Math.max(4, Math.floor(8 * (pixelArea / 1000000)));
|
|
4960
|
+
let clipHeavyCount = 0;
|
|
4604
4961
|
for(let layer = 0; layer < layers; layer++){
|
|
4605
4962
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
4606
4963
|
const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
|
|
@@ -4638,7 +4995,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4638
4995
|
if ($4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones)) {
|
|
4639
4996
|
if (rng() < 0.85) continue;
|
|
4640
4997
|
}
|
|
4641
|
-
if (
|
|
4998
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4642
4999
|
if (rng() < 0.6) continue;
|
|
4643
5000
|
}
|
|
4644
5001
|
// Power distribution for size — archetype controls the curve
|
|
@@ -4689,7 +5046,26 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4689
5046
|
const shapeRenderStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
|
|
4690
5047
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
4691
5048
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
4692
|
-
|
|
5049
|
+
let finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
|
|
5050
|
+
// Budget check: downgrade expensive styles proportionally —
|
|
5051
|
+
// the more expensive the style, the earlier it gets downgraded.
|
|
5052
|
+
// noise-grain (400) downgrades when budget < 20% remaining,
|
|
5053
|
+
// stipple (90) when < 82%, wood-grain (10) when < 98%.
|
|
5054
|
+
let styleCost = $4f72c5a314eddf25$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5055
|
+
if (styleCost > 3) {
|
|
5056
|
+
const downgradeThreshold = Math.min(0.85, styleCost / 500);
|
|
5057
|
+
if (complexityBudget < totalBudget * (1 - downgradeThreshold)) {
|
|
5058
|
+
finalRenderStyle = $4f72c5a314eddf25$var$downgradeRenderStyle(finalRenderStyle);
|
|
5059
|
+
styleCost = $4f72c5a314eddf25$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
// Hard cap: clip-heavy styles (stipple, noise-grain) are limited
|
|
5063
|
+
// to MAX_CLIP_HEAVY_SHAPES total across the entire render.
|
|
5064
|
+
if ((finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
5065
|
+
finalRenderStyle = $4f72c5a314eddf25$var$downgradeRenderStyle(finalRenderStyle);
|
|
5066
|
+
styleCost = $4f72c5a314eddf25$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5067
|
+
}
|
|
5068
|
+
if (finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") clipHeavyCount++;
|
|
4693
5069
|
// Consistent light direction — subtle shadow offset
|
|
4694
5070
|
const shadowDist = hasGlow ? 0 : size * 0.02;
|
|
4695
5071
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
@@ -4698,17 +5074,11 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4698
5074
|
let finalX = x;
|
|
4699
5075
|
let finalY = y;
|
|
4700
5076
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4701
|
-
//
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
for (const sp of shapePositions){
|
|
4705
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4706
|
-
if (d < nearestDist && d > 0) {
|
|
4707
|
-
nearestDist = d;
|
|
4708
|
-
nearestPos = sp;
|
|
4709
|
-
}
|
|
4710
|
-
}
|
|
5077
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
5078
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
5079
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4711
5080
|
if (nearestPos) {
|
|
5081
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4712
5082
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4713
5083
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4714
5084
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4750,30 +5120,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4750
5120
|
lightAngle: lightAngle,
|
|
4751
5121
|
scaleFactor: scaleFactor
|
|
4752
5122
|
};
|
|
4753
|
-
if (shouldMirror)
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
5123
|
+
if (shouldMirror) {
|
|
5124
|
+
(0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
5125
|
+
...shapeConfig,
|
|
5126
|
+
mirrorAxis: mirrorAxis,
|
|
5127
|
+
mirrorGap: size * (0.1 + rng() * 0.3)
|
|
5128
|
+
});
|
|
5129
|
+
complexityBudget -= styleCost * 2; // mirrored = 2 shapes
|
|
5130
|
+
} else {
|
|
5131
|
+
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
|
|
5132
|
+
complexityBudget -= styleCost;
|
|
5133
|
+
}
|
|
5134
|
+
// ── Extras budget gate — skip multiplier sections when over budget ──
|
|
5135
|
+
const extrasAllowed = extrasSpent < budgetForExtras;
|
|
4759
5136
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
4760
5137
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
4761
5138
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
5139
|
+
if (extrasAllowed) {
|
|
5140
|
+
for(let g = 0; g < glazePasses; g++){
|
|
5141
|
+
const glazeScale = 1 - (g + 1) * 0.12;
|
|
5142
|
+
const glazeAlpha = 0.08 + g * 0.04;
|
|
5143
|
+
ctx.globalAlpha = glazeAlpha;
|
|
5144
|
+
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
|
|
5145
|
+
fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
|
|
5146
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
5147
|
+
strokeWidth: 0,
|
|
5148
|
+
size: size * glazeScale,
|
|
5149
|
+
rotation: rotation,
|
|
5150
|
+
proportionType: "GOLDEN_RATIO",
|
|
5151
|
+
renderStyle: "fill-only",
|
|
5152
|
+
rng: rng
|
|
5153
|
+
});
|
|
5154
|
+
}
|
|
5155
|
+
extrasSpent += glazePasses;
|
|
4776
5156
|
}
|
|
5157
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
4777
5158
|
}
|
|
4778
5159
|
shapePositions.push({
|
|
4779
5160
|
x: finalX,
|
|
@@ -4781,35 +5162,51 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4781
5162
|
size: size,
|
|
4782
5163
|
shape: shape
|
|
4783
5164
|
});
|
|
5165
|
+
spatialGrid.insert({
|
|
5166
|
+
x: finalX,
|
|
5167
|
+
y: finalY,
|
|
5168
|
+
size: size,
|
|
5169
|
+
shape: shape
|
|
5170
|
+
});
|
|
4784
5171
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4785
5172
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4786
5173
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
4787
5174
|
const echoAngle = rng() * Math.PI * 2;
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
5175
|
+
if (extrasAllowed) {
|
|
5176
|
+
for(let e = 0; e < echoCount; e++){
|
|
5177
|
+
const echoScale = 0.3 - e * 0.08;
|
|
5178
|
+
const echoDist = size * (0.6 + e * 0.4);
|
|
5179
|
+
const echoX = finalX + Math.cos(echoAngle) * echoDist;
|
|
5180
|
+
const echoY = finalY + Math.sin(echoAngle) * echoDist;
|
|
5181
|
+
const echoSize = size * Math.max(0.1, echoScale);
|
|
5182
|
+
if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
|
|
5183
|
+
ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
|
|
5184
|
+
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
|
|
5185
|
+
fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
|
|
5186
|
+
strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.4),
|
|
5187
|
+
strokeWidth: strokeWidth * 0.6,
|
|
5188
|
+
size: echoSize,
|
|
5189
|
+
rotation: rotation + (e + 1) * 15,
|
|
5190
|
+
proportionType: "GOLDEN_RATIO",
|
|
5191
|
+
renderStyle: finalRenderStyle,
|
|
5192
|
+
rng: rng
|
|
5193
|
+
});
|
|
5194
|
+
shapePositions.push({
|
|
5195
|
+
x: echoX,
|
|
5196
|
+
y: echoY,
|
|
5197
|
+
size: echoSize,
|
|
5198
|
+
shape: shape
|
|
5199
|
+
});
|
|
5200
|
+
spatialGrid.insert({
|
|
5201
|
+
x: echoX,
|
|
5202
|
+
y: echoY,
|
|
5203
|
+
size: echoSize,
|
|
5204
|
+
shape: shape
|
|
5205
|
+
});
|
|
5206
|
+
}
|
|
5207
|
+
extrasSpent += echoCount * styleCost;
|
|
4812
5208
|
}
|
|
5209
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
4813
5210
|
}
|
|
4814
5211
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
4815
5212
|
// Focal depth: shapes near focal points get more detail
|
|
@@ -4817,7 +5214,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4817
5214
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
4818
5215
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
4819
5216
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
4820
|
-
for(let n = 0; n < innerCount; n++){
|
|
5217
|
+
if (extrasAllowed) for(let n = 0; n < innerCount; n++){
|
|
4821
5218
|
// Pick inner shape from palette affinities
|
|
4822
5219
|
const innerSizeFraction = size * 0.25 / adjustedMaxSize;
|
|
4823
5220
|
const innerShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
|
|
@@ -4826,6 +5223,10 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4826
5223
|
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
4827
5224
|
const innerRot = rng() * 360;
|
|
4828
5225
|
const innerFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
|
|
5226
|
+
let innerStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng);
|
|
5227
|
+
// Apply clip-heavy cap to nested shapes too
|
|
5228
|
+
if ((innerStyle === "stipple" || innerStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) innerStyle = $4f72c5a314eddf25$var$downgradeRenderStyle(innerStyle);
|
|
5229
|
+
if (innerStyle === "stipple" || innerStyle === "noise-grain") clipHeavyCount++;
|
|
4829
5230
|
ctx.globalAlpha = layerOpacity * 0.7;
|
|
4830
5231
|
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
|
|
4831
5232
|
fillColor: innerFill,
|
|
@@ -4834,9 +5235,21 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4834
5235
|
size: innerSize,
|
|
4835
5236
|
rotation: innerRot,
|
|
4836
5237
|
proportionType: "GOLDEN_RATIO",
|
|
4837
|
-
renderStyle:
|
|
5238
|
+
renderStyle: innerStyle,
|
|
4838
5239
|
rng: rng
|
|
4839
5240
|
});
|
|
5241
|
+
extrasSpent += $4f72c5a314eddf25$var$RENDER_STYLE_COST[innerStyle] ?? 1;
|
|
5242
|
+
}
|
|
5243
|
+
else // Drain RNG to keep determinism — each nested shape consumes ~8 rng calls
|
|
5244
|
+
for(let n = 0; n < innerCount; n++){
|
|
5245
|
+
rng();
|
|
5246
|
+
rng();
|
|
5247
|
+
rng();
|
|
5248
|
+
rng();
|
|
5249
|
+
rng();
|
|
5250
|
+
rng();
|
|
5251
|
+
rng();
|
|
5252
|
+
rng();
|
|
4840
5253
|
}
|
|
4841
5254
|
}
|
|
4842
5255
|
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
@@ -4845,41 +5258,113 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4845
5258
|
const constellation = $4f72c5a314eddf25$var$CONSTELLATIONS[Math.floor(rng() * $4f72c5a314eddf25$var$CONSTELLATIONS.length)];
|
|
4846
5259
|
const members = constellation.build(rng, size);
|
|
4847
5260
|
const groupRotation = rng() * Math.PI * 2;
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
5261
|
+
if (extrasAllowed) {
|
|
5262
|
+
const cosR = Math.cos(groupRotation);
|
|
5263
|
+
const sinR = Math.sin(groupRotation);
|
|
5264
|
+
for (const member of members){
|
|
5265
|
+
// Rotate the group offset by the group rotation
|
|
5266
|
+
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
5267
|
+
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
5268
|
+
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
5269
|
+
const memberFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
|
|
5270
|
+
const memberStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
|
|
5271
|
+
ctx.globalAlpha = layerOpacity * 0.6;
|
|
5272
|
+
// Use the member's shape if available, otherwise fall back to palette
|
|
5273
|
+
const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
|
|
5274
|
+
let memberStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng);
|
|
5275
|
+
// Apply clip-heavy cap to constellation members too
|
|
5276
|
+
if ((memberStyle === "stipple" || memberStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) memberStyle = $4f72c5a314eddf25$var$downgradeRenderStyle(memberStyle);
|
|
5277
|
+
if (memberStyle === "stipple" || memberStyle === "noise-grain") clipHeavyCount++;
|
|
5278
|
+
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
|
|
5279
|
+
fillColor: memberFill,
|
|
5280
|
+
strokeColor: memberStroke,
|
|
5281
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5282
|
+
size: member.size,
|
|
5283
|
+
rotation: member.rotation + groupRotation * 180 / Math.PI,
|
|
5284
|
+
proportionType: "GOLDEN_RATIO",
|
|
5285
|
+
renderStyle: memberStyle,
|
|
5286
|
+
rng: rng
|
|
5287
|
+
});
|
|
5288
|
+
shapePositions.push({
|
|
5289
|
+
x: mx,
|
|
5290
|
+
y: my,
|
|
5291
|
+
size: member.size,
|
|
5292
|
+
shape: memberShape
|
|
5293
|
+
});
|
|
5294
|
+
spatialGrid.insert({
|
|
5295
|
+
x: mx,
|
|
5296
|
+
y: my,
|
|
5297
|
+
size: member.size,
|
|
5298
|
+
shape: memberShape
|
|
5299
|
+
});
|
|
5300
|
+
extrasSpent += $4f72c5a314eddf25$var$RENDER_STYLE_COST[memberStyle] ?? 1;
|
|
5301
|
+
}
|
|
5302
|
+
} else // Drain RNG — each member consumes ~6 rng calls for colors/style
|
|
5303
|
+
for(let m = 0; m < members.length; m++){
|
|
5304
|
+
rng();
|
|
5305
|
+
rng();
|
|
5306
|
+
rng();
|
|
5307
|
+
rng();
|
|
5308
|
+
rng();
|
|
5309
|
+
rng();
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
5313
|
+
// ~12% of medium-large shapes spawn a rhythmic sequence
|
|
5314
|
+
if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
|
|
5315
|
+
const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
|
|
5316
|
+
const rhythmAngle = rng() * Math.PI * 2;
|
|
5317
|
+
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5318
|
+
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5319
|
+
const rhythmShape = shape; // same shape for visual rhythm
|
|
5320
|
+
if (extrasAllowed) {
|
|
5321
|
+
let rhythmSize = size * 0.6;
|
|
5322
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5323
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5324
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5325
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5326
|
+
if ($4f72c5a314eddf25$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5327
|
+
rhythmSize *= rhythmDecay;
|
|
5328
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5329
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5330
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5331
|
+
const rhythmFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5332
|
+
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5333
|
+
fillColor: rhythmFill,
|
|
5334
|
+
strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5335
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5336
|
+
size: rhythmSize,
|
|
5337
|
+
rotation: rotation + (r + 1) * 12,
|
|
5338
|
+
proportionType: "GOLDEN_RATIO",
|
|
5339
|
+
renderStyle: finalRenderStyle,
|
|
5340
|
+
rng: rng
|
|
5341
|
+
});
|
|
5342
|
+
shapePositions.push({
|
|
5343
|
+
x: rx,
|
|
5344
|
+
y: ry,
|
|
5345
|
+
size: rhythmSize,
|
|
5346
|
+
shape: rhythmShape
|
|
5347
|
+
});
|
|
5348
|
+
spatialGrid.insert({
|
|
5349
|
+
x: rx,
|
|
5350
|
+
y: ry,
|
|
5351
|
+
size: rhythmSize,
|
|
5352
|
+
shape: rhythmShape
|
|
5353
|
+
});
|
|
5354
|
+
}
|
|
5355
|
+
extrasSpent += rhythmCount * styleCost;
|
|
5356
|
+
} else // Drain RNG — each rhythm step consumes ~3 rng calls for colors
|
|
5357
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5358
|
+
rng();
|
|
5359
|
+
rng();
|
|
5360
|
+
rng();
|
|
4876
5361
|
}
|
|
4877
5362
|
}
|
|
4878
5363
|
}
|
|
4879
5364
|
}
|
|
4880
5365
|
// Reset blend mode for post-processing passes
|
|
4881
5366
|
ctx.globalCompositeOperation = "source-over";
|
|
4882
|
-
// ──
|
|
5367
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
4883
5368
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
4884
5369
|
// with a tinted background wash, creating a "peek through" effect.
|
|
4885
5370
|
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
@@ -4938,14 +5423,26 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4938
5423
|
}
|
|
4939
5424
|
}
|
|
4940
5425
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
5426
|
+
// Optimized: collect all segments into width-quantized buckets, then
|
|
5427
|
+
// render each bucket as a single batched path. This reduces
|
|
5428
|
+
// beginPath/stroke calls from O(segments) to O(buckets).
|
|
4941
5429
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
4942
5430
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
5431
|
+
// Width buckets — 6 buckets cover the taper×pressure range
|
|
5432
|
+
const FLOW_WIDTH_BUCKETS = 6;
|
|
5433
|
+
const flowBuckets = [];
|
|
5434
|
+
for(let b = 0; b < FLOW_WIDTH_BUCKETS; b++)flowBuckets.push([]);
|
|
5435
|
+
// Track the representative width for each bucket
|
|
5436
|
+
const flowBucketWidths = new Array(FLOW_WIDTH_BUCKETS);
|
|
5437
|
+
// Pre-compute max possible width for bucket assignment
|
|
5438
|
+
let globalMaxFlowWidth = 0;
|
|
4943
5439
|
for(let i = 0; i < numFlowLines; i++){
|
|
4944
5440
|
let fx = rng() * width;
|
|
4945
5441
|
let fy = rng() * height;
|
|
4946
5442
|
const steps = 30 + Math.floor(rng() * 40);
|
|
4947
5443
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
4948
5444
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
5445
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
4949
5446
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
4950
5447
|
const lineColorStart = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
4951
5448
|
const lineColorEnd = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
@@ -4960,20 +5457,29 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4960
5457
|
fx += Math.cos(angle) * stepLen;
|
|
4961
5458
|
fy += Math.sin(angle) * stepLen;
|
|
4962
5459
|
if (fx < 0 || fx > width || fy < 0 || fy > height) break;
|
|
5460
|
+
// Skip segments that pass through void zones
|
|
5461
|
+
if ($4f72c5a314eddf25$var$isInVoidZone(fx, fy, voidZones)) {
|
|
5462
|
+
prevX = fx;
|
|
5463
|
+
prevY = fy;
|
|
5464
|
+
continue;
|
|
5465
|
+
}
|
|
4963
5466
|
const t = s / steps;
|
|
4964
|
-
// Taper + pressure
|
|
4965
5467
|
const taper = 1 - t * 0.8;
|
|
4966
5468
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
4967
|
-
|
|
4968
|
-
|
|
5469
|
+
const segWidth = startWidth * taper * pressure;
|
|
5470
|
+
const segAlpha = lineAlpha * taper;
|
|
4969
5471
|
const lineColor = t < 0.5 ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $d016ad53434219a1$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5472
|
+
// Quantize width into bucket
|
|
5473
|
+
const bucketIdx = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(segWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5474
|
+
flowBuckets[bucketIdx].push({
|
|
5475
|
+
x1: prevX,
|
|
5476
|
+
y1: prevY,
|
|
5477
|
+
x2: fx,
|
|
5478
|
+
y2: fy,
|
|
5479
|
+
color: lineColor,
|
|
5480
|
+
alpha: segAlpha
|
|
5481
|
+
});
|
|
5482
|
+
flowBucketWidths[bucketIdx] = segWidth;
|
|
4977
5483
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
4978
5484
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
4979
5485
|
const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
|
|
@@ -4989,12 +5495,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4989
5495
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
4990
5496
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
4991
5497
|
const bTaper = 1 - bs / branchSteps * 0.9;
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
5498
|
+
const bSegWidth = branchWidth * bTaper;
|
|
5499
|
+
const bAlpha = lineAlpha * taper * bTaper * 0.6;
|
|
5500
|
+
const bBucket = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(bSegWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5501
|
+
flowBuckets[bBucket].push({
|
|
5502
|
+
x1: bPrevX,
|
|
5503
|
+
y1: bPrevY,
|
|
5504
|
+
x2: bx,
|
|
5505
|
+
y2: by,
|
|
5506
|
+
color: lineColor,
|
|
5507
|
+
alpha: bAlpha
|
|
5508
|
+
});
|
|
5509
|
+
flowBucketWidths[bBucket] = bSegWidth;
|
|
4998
5510
|
bPrevX = bx;
|
|
4999
5511
|
bPrevY = by;
|
|
5000
5512
|
}
|
|
@@ -5003,7 +5515,40 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5003
5515
|
prevY = fy;
|
|
5004
5516
|
}
|
|
5005
5517
|
}
|
|
5518
|
+
// Render flow line buckets — one batched path per width bucket
|
|
5519
|
+
// Within each bucket, further sub-batch by quantized alpha (4 levels)
|
|
5520
|
+
ctx.lineCap = "round";
|
|
5521
|
+
const FLOW_ALPHA_BUCKETS = 4;
|
|
5522
|
+
for(let wb = 0; wb < FLOW_WIDTH_BUCKETS; wb++){
|
|
5523
|
+
const segs = flowBuckets[wb];
|
|
5524
|
+
if (segs.length === 0) continue;
|
|
5525
|
+
ctx.lineWidth = flowBucketWidths[wb];
|
|
5526
|
+
// Sub-bucket by alpha
|
|
5527
|
+
const alphaSubs = [];
|
|
5528
|
+
for(let a = 0; a < FLOW_ALPHA_BUCKETS; a++)alphaSubs.push([]);
|
|
5529
|
+
let maxAlpha = 0;
|
|
5530
|
+
for(let j = 0; j < segs.length; j++)if (segs[j].alpha > maxAlpha) maxAlpha = segs[j].alpha;
|
|
5531
|
+
for(let j = 0; j < segs.length; j++){
|
|
5532
|
+
const ai = Math.min(FLOW_ALPHA_BUCKETS - 1, Math.floor(segs[j].alpha / (maxAlpha || 1) * FLOW_ALPHA_BUCKETS));
|
|
5533
|
+
alphaSubs[ai].push(segs[j]);
|
|
5534
|
+
}
|
|
5535
|
+
for(let ai = 0; ai < FLOW_ALPHA_BUCKETS; ai++){
|
|
5536
|
+
const sub = alphaSubs[ai];
|
|
5537
|
+
if (sub.length === 0) continue;
|
|
5538
|
+
// Use the median segment's alpha and color as representative
|
|
5539
|
+
const rep = sub[Math.floor(sub.length / 2)];
|
|
5540
|
+
ctx.globalAlpha = rep.alpha;
|
|
5541
|
+
ctx.strokeStyle = rep.color;
|
|
5542
|
+
ctx.beginPath();
|
|
5543
|
+
for(let j = 0; j < sub.length; j++){
|
|
5544
|
+
ctx.moveTo(sub[j].x1, sub[j].y1);
|
|
5545
|
+
ctx.lineTo(sub[j].x2, sub[j].y2);
|
|
5546
|
+
}
|
|
5547
|
+
ctx.stroke();
|
|
5548
|
+
}
|
|
5549
|
+
}
|
|
5006
5550
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
5551
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
5007
5552
|
const energyArchetypes = [
|
|
5008
5553
|
"dense-chaotic",
|
|
5009
5554
|
"cosmic",
|
|
@@ -5014,8 +5559,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5014
5559
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
5015
5560
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
5016
5561
|
ctx.lineCap = "round";
|
|
5562
|
+
// Collect all energy segments with their computed state
|
|
5563
|
+
const ENERGY_ALPHA_BUCKETS = 3;
|
|
5564
|
+
const energyBuckets = [];
|
|
5565
|
+
for(let b = 0; b < ENERGY_ALPHA_BUCKETS; b++)energyBuckets.push([]);
|
|
5566
|
+
const energyAlphas = new Array(ENERGY_ALPHA_BUCKETS).fill(0);
|
|
5017
5567
|
for(let e = 0; e < energyCount; e++){
|
|
5018
|
-
// Pick a random shape to radiate from
|
|
5019
5568
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5020
5569
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
5021
5570
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -5027,14 +5576,37 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5027
5576
|
const sy = source.y + Math.sin(angle) * startDist;
|
|
5028
5577
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
5029
5578
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5579
|
+
const eAlpha = 0.04 + rng() * 0.06;
|
|
5580
|
+
const eColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5581
|
+
const eLw = (0.5 + rng() * 1.5) * scaleFactor;
|
|
5582
|
+
// Quantize alpha into bucket
|
|
5583
|
+
const bi = Math.min(ENERGY_ALPHA_BUCKETS - 1, Math.floor((eAlpha - 0.04) / 0.06 * ENERGY_ALPHA_BUCKETS));
|
|
5584
|
+
energyBuckets[bi].push({
|
|
5585
|
+
x1: sx,
|
|
5586
|
+
y1: sy,
|
|
5587
|
+
x2: ex,
|
|
5588
|
+
y2: ey,
|
|
5589
|
+
color: eColor,
|
|
5590
|
+
lw: eLw
|
|
5591
|
+
});
|
|
5592
|
+
energyAlphas[bi] = eAlpha;
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
// Render batched energy lines
|
|
5596
|
+
for(let bi = 0; bi < ENERGY_ALPHA_BUCKETS; bi++){
|
|
5597
|
+
const segs = energyBuckets[bi];
|
|
5598
|
+
if (segs.length === 0) continue;
|
|
5599
|
+
ctx.globalAlpha = energyAlphas[bi];
|
|
5600
|
+
// Use median segment's color and width as representative
|
|
5601
|
+
const rep = segs[Math.floor(segs.length / 2)];
|
|
5602
|
+
ctx.strokeStyle = rep.color;
|
|
5603
|
+
ctx.lineWidth = rep.lw;
|
|
5604
|
+
ctx.beginPath();
|
|
5605
|
+
for(let j = 0; j < segs.length; j++){
|
|
5606
|
+
ctx.moveTo(segs[j].x1, segs[j].y1);
|
|
5607
|
+
ctx.lineTo(segs[j].x2, segs[j].y2);
|
|
5037
5608
|
}
|
|
5609
|
+
ctx.stroke();
|
|
5038
5610
|
}
|
|
5039
5611
|
}
|
|
5040
5612
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
@@ -5057,50 +5629,128 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5057
5629
|
}
|
|
5058
5630
|
ctx.restore();
|
|
5059
5631
|
}
|
|
5060
|
-
// ── 7. Noise texture overlay
|
|
5632
|
+
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
5633
|
+
// Optimized: cap density at large sizes (diminishing returns above ~2K dots),
|
|
5634
|
+
// skip inner pixelScale loop when scale=1, use Uint32Array for faster writes.
|
|
5061
5635
|
const noiseRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 777));
|
|
5062
|
-
const
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
const
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5636
|
+
const rawNoiseDensity = Math.floor(width * height / 800);
|
|
5637
|
+
// Cap at 2500 dots — beyond this the visual effect is indistinguishable
|
|
5638
|
+
// but getImageData/putImageData cost scales with canvas size
|
|
5639
|
+
const noiseDensity = Math.min(rawNoiseDensity, 2500);
|
|
5640
|
+
try {
|
|
5641
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5642
|
+
const data = imageData.data;
|
|
5643
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5644
|
+
if (pixelScale === 1) // Fast path — no inner loop, direct pixel write
|
|
5645
|
+
// Pre-compute alpha blend as integer math (avoid float multiply per channel)
|
|
5646
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5647
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5648
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5649
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5650
|
+
// srcA in range [0.01, 0.04] — multiply by 256 for fixed-point
|
|
5651
|
+
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
5652
|
+
const invA256 = 256 - srcA256;
|
|
5653
|
+
const bSrc = brightness * srcA256; // pre-multiply brightness × alpha
|
|
5654
|
+
const idx = ny * width + nx << 2;
|
|
5655
|
+
data[idx] = data[idx] * invA256 + bSrc >> 8;
|
|
5656
|
+
data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
|
|
5657
|
+
data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
|
|
5658
|
+
}
|
|
5659
|
+
else for(let i = 0; i < noiseDensity; i++){
|
|
5660
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5661
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5662
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5663
|
+
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
5664
|
+
const invA256 = 256 - srcA256;
|
|
5665
|
+
const bSrc = brightness * srcA256;
|
|
5666
|
+
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5667
|
+
const idx = (ny + dy) * width + (nx + dx) << 2;
|
|
5668
|
+
data[idx] = data[idx] * invA256 + bSrc >> 8;
|
|
5669
|
+
data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
|
|
5670
|
+
data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
ctx.putImageData(imageData, 0, 0);
|
|
5674
|
+
} catch {
|
|
5675
|
+
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5676
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5677
|
+
const nx = noiseRng() * width;
|
|
5678
|
+
const ny = noiseRng() * height;
|
|
5679
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5680
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5681
|
+
ctx.globalAlpha = alpha;
|
|
5682
|
+
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5683
|
+
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5684
|
+
}
|
|
5071
5685
|
}
|
|
5072
5686
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
5073
5687
|
ctx.globalAlpha = 1;
|
|
5074
5688
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
5075
5689
|
const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
|
|
5690
|
+
// Tint vignette based on background: warm sepia for light, cool blue for dark
|
|
5691
|
+
const isLightBg = bgLum > 0.5;
|
|
5692
|
+
const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
|
|
5693
|
+
: `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
|
|
5076
5694
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
5077
5695
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
5078
|
-
vigGrad.addColorStop(1,
|
|
5696
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
5079
5697
|
ctx.fillStyle = vigGrad;
|
|
5080
5698
|
ctx.fillRect(0, 0, width, height);
|
|
5081
|
-
// ── 9. Organic connecting curves
|
|
5699
|
+
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5700
|
+
// Optimized: batch all curves into alpha-quantized groups to reduce
|
|
5701
|
+
// beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
|
|
5082
5702
|
if (shapePositions.length > 1) {
|
|
5083
5703
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5704
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5084
5705
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5706
|
+
// Collect curves into 3 alpha buckets
|
|
5707
|
+
const CURVE_ALPHA_BUCKETS = 3;
|
|
5708
|
+
const curveBuckets = [];
|
|
5709
|
+
const curveColors = [];
|
|
5710
|
+
const curveAlphas = new Array(CURVE_ALPHA_BUCKETS).fill(0);
|
|
5711
|
+
for(let b = 0; b < CURVE_ALPHA_BUCKETS; b++)curveBuckets.push([]);
|
|
5085
5712
|
for(let i = 0; i < numCurves; i++){
|
|
5086
5713
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
5087
5714
|
const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
|
|
5088
5715
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
5089
5716
|
const a = shapePositions[idxA];
|
|
5090
5717
|
const b = shapePositions[idxB];
|
|
5091
|
-
const mx = (a.x + b.x) / 2;
|
|
5092
|
-
const my = (a.y + b.y) / 2;
|
|
5093
5718
|
const dx = b.x - a.x;
|
|
5094
5719
|
const dy = b.y - a.y;
|
|
5095
5720
|
const dist = Math.hypot(dx, dy);
|
|
5721
|
+
// Skip connections between distant shapes
|
|
5722
|
+
if (dist > maxCurveDist) continue;
|
|
5723
|
+
const mx = (a.x + b.x) / 2;
|
|
5724
|
+
const my = (a.y + b.y) / 2;
|
|
5096
5725
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5097
5726
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5098
5727
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
5099
|
-
|
|
5100
|
-
|
|
5728
|
+
const curveAlpha = 0.06 + rng() * 0.1;
|
|
5729
|
+
const curveColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5730
|
+
const bi = Math.min(CURVE_ALPHA_BUCKETS - 1, Math.floor((curveAlpha - 0.06) / 0.1 * CURVE_ALPHA_BUCKETS));
|
|
5731
|
+
curveBuckets[bi].push({
|
|
5732
|
+
ax: a.x,
|
|
5733
|
+
ay: a.y,
|
|
5734
|
+
cpx: cpx,
|
|
5735
|
+
cpy: cpy,
|
|
5736
|
+
bx: b.x,
|
|
5737
|
+
by: b.y
|
|
5738
|
+
});
|
|
5739
|
+
curveAlphas[bi] = curveAlpha;
|
|
5740
|
+
if (!curveColors[bi]) curveColors[bi] = curveColor;
|
|
5741
|
+
}
|
|
5742
|
+
// Render batched curves
|
|
5743
|
+
for(let bi = 0; bi < CURVE_ALPHA_BUCKETS; bi++){
|
|
5744
|
+
const curves = curveBuckets[bi];
|
|
5745
|
+
if (curves.length === 0) continue;
|
|
5746
|
+
ctx.globalAlpha = curveAlphas[bi];
|
|
5747
|
+
ctx.strokeStyle = curveColors[bi];
|
|
5101
5748
|
ctx.beginPath();
|
|
5102
|
-
|
|
5103
|
-
|
|
5749
|
+
for(let j = 0; j < curves.length; j++){
|
|
5750
|
+
const c = curves[j];
|
|
5751
|
+
ctx.moveTo(c.ax, c.ay);
|
|
5752
|
+
ctx.quadraticCurveTo(c.cpx, c.cpy, c.bx, c.by);
|
|
5753
|
+
}
|
|
5104
5754
|
ctx.stroke();
|
|
5105
5755
|
}
|
|
5106
5756
|
}
|
|
@@ -5218,11 +5868,14 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5218
5868
|
}
|
|
5219
5869
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5220
5870
|
// Vine tendrils — organic curving lines along edges
|
|
5871
|
+
// Optimized: batch all tendrils into a single path
|
|
5221
5872
|
ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5222
5873
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5223
5874
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5224
5875
|
ctx.lineCap = "round";
|
|
5225
5876
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5877
|
+
ctx.beginPath();
|
|
5878
|
+
const leafPositions = [];
|
|
5226
5879
|
for(let t = 0; t < tendrilCount; t++){
|
|
5227
5880
|
// Start from a random edge point
|
|
5228
5881
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -5240,7 +5893,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5240
5893
|
tx = width - borderPad;
|
|
5241
5894
|
ty = borderRng() * height;
|
|
5242
5895
|
}
|
|
5243
|
-
ctx.beginPath();
|
|
5244
5896
|
ctx.moveTo(tx, ty);
|
|
5245
5897
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5246
5898
|
for(let s = 0; s < segs; s++){
|
|
@@ -5254,14 +5906,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5254
5906
|
ty = cpy3;
|
|
5255
5907
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5256
5908
|
}
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5909
|
+
// Collect leaf positions for batch fill
|
|
5910
|
+
if (borderRng() < 0.6) leafPositions.push({
|
|
5911
|
+
x: tx,
|
|
5912
|
+
y: ty,
|
|
5913
|
+
r: borderPad * (0.15 + borderRng() * 0.2)
|
|
5914
|
+
});
|
|
5915
|
+
}
|
|
5916
|
+
ctx.stroke();
|
|
5917
|
+
// Batch all leaf dots into a single fill
|
|
5918
|
+
if (leafPositions.length > 0) {
|
|
5919
|
+
ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5920
|
+
ctx.beginPath();
|
|
5921
|
+
for (const leaf of leafPositions){
|
|
5922
|
+
ctx.moveTo(leaf.x + leaf.r, leaf.y);
|
|
5923
|
+
ctx.arc(leaf.x, leaf.y, leaf.r, 0, Math.PI * 2);
|
|
5264
5924
|
}
|
|
5925
|
+
ctx.fill();
|
|
5265
5926
|
}
|
|
5266
5927
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5267
5928
|
// Star-studded arcs along edges
|
|
@@ -5276,8 +5937,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5276
5937
|
ctx.beginPath();
|
|
5277
5938
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5278
5939
|
ctx.stroke();
|
|
5279
|
-
// Scatter small stars along the border region
|
|
5940
|
+
// Scatter small stars along the border region — batched into single path
|
|
5280
5941
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5942
|
+
ctx.beginPath();
|
|
5281
5943
|
for(let s = 0; s < starCount; s++){
|
|
5282
5944
|
const edge = Math.floor(borderRng() * 4);
|
|
5283
5945
|
let sx, sy;
|
|
@@ -5296,7 +5958,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5296
5958
|
}
|
|
5297
5959
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5298
5960
|
// 4-point star
|
|
5299
|
-
ctx.beginPath();
|
|
5300
5961
|
for(let p = 0; p < 8; p++){
|
|
5301
5962
|
const a = p / 8 * Math.PI * 2;
|
|
5302
5963
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -5306,8 +5967,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5306
5967
|
else ctx.lineTo(px2, py2);
|
|
5307
5968
|
}
|
|
5308
5969
|
ctx.closePath();
|
|
5309
|
-
ctx.fill();
|
|
5310
5970
|
}
|
|
5971
|
+
ctx.fill();
|
|
5311
5972
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5312
5973
|
// Thin single rule — understated elegance
|
|
5313
5974
|
ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
@@ -5318,13 +5979,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5318
5979
|
// Other archetypes: no border (intentional — not every image needs one)
|
|
5319
5980
|
ctx.restore();
|
|
5320
5981
|
}
|
|
5321
|
-
// ── 11. Signature mark —
|
|
5982
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
5322
5983
|
{
|
|
5323
5984
|
const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));
|
|
5324
5985
|
const sigSize = Math.min(width, height) * 0.025;
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
const
|
|
5986
|
+
const sigMargin = sigSize * 2.5;
|
|
5987
|
+
// Find the corner with the lowest local density
|
|
5988
|
+
const cornerCandidates = [
|
|
5989
|
+
{
|
|
5990
|
+
x: sigMargin,
|
|
5991
|
+
y: sigMargin
|
|
5992
|
+
},
|
|
5993
|
+
{
|
|
5994
|
+
x: width - sigMargin,
|
|
5995
|
+
y: sigMargin
|
|
5996
|
+
},
|
|
5997
|
+
{
|
|
5998
|
+
x: sigMargin,
|
|
5999
|
+
y: height - sigMargin
|
|
6000
|
+
},
|
|
6001
|
+
{
|
|
6002
|
+
x: width - sigMargin,
|
|
6003
|
+
y: height - sigMargin
|
|
6004
|
+
}
|
|
6005
|
+
];
|
|
6006
|
+
let bestCorner = cornerCandidates[3]; // default: bottom-right
|
|
6007
|
+
let minDensity = Infinity;
|
|
6008
|
+
for (const corner of cornerCandidates){
|
|
6009
|
+
const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
|
|
6010
|
+
if (density < minDensity) {
|
|
6011
|
+
minDensity = density;
|
|
6012
|
+
bestCorner = corner;
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
const sigX = bestCorner.x;
|
|
6016
|
+
const sigY = bestCorner.y;
|
|
5328
6017
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
5329
6018
|
const sigColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
5330
6019
|
ctx.save();
|