git-hash-art 0.11.0 → 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/ALGORITHM.md +76 -24
- package/CHANGELOG.md +9 -1
- package/dist/browser.js +648 -265
- package/dist/browser.js.map +1 -1
- package/dist/main.js +648 -265
- package/dist/main.js.map +1 -1
- package/dist/module.js +648 -265
- 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/canvas/colors.ts +23 -6
- package/src/lib/canvas/draw.ts +149 -62
- package/src/lib/canvas/shapes/complex.ts +19 -10
- package/src/lib/canvas/shapes/sacred.ts +16 -17
- package/src/lib/render.ts +460 -190
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 {
|
|
@@ -674,12 +684,21 @@ function $d016ad53434219a1$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
|
674
684
|
const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
|
|
675
685
|
return $d016ad53434219a1$var$hslToHex($d016ad53434219a1$var$shiftHueToward(h, target, amount), s, l);
|
|
676
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();
|
|
677
691
|
function $d016ad53434219a1$export$5c6e3c2b59b7fbbe(hex) {
|
|
692
|
+
let cached = $d016ad53434219a1$var$_lumCache.get(hex);
|
|
693
|
+
if (cached !== undefined) return cached;
|
|
678
694
|
const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex).map((c)=>{
|
|
679
695
|
const s = c / 255;
|
|
680
696
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
681
697
|
});
|
|
682
|
-
|
|
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;
|
|
683
702
|
}
|
|
684
703
|
function $d016ad53434219a1$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
|
|
685
704
|
const fgLum = $d016ad53434219a1$export$5c6e3c2b59b7fbbe(fgHex);
|
|
@@ -1115,21 +1134,31 @@ const $4bf3d69be49ad55c$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
|
|
|
1115
1134
|
(0, $efc5b85ac9840d51$export$e46c5570db033611)(ctx, size, finalConfig);
|
|
1116
1135
|
const gridSize = 8;
|
|
1117
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
|
+
}
|
|
1118
1150
|
ctx.beginPath();
|
|
1119
1151
|
// Create base grid
|
|
1120
|
-
for(let i = 0; i <= gridSize; i++)
|
|
1152
|
+
for(let i = 0; i <= gridSize; i++){
|
|
1121
1153
|
const x = (i - gridSize / 2) * unit;
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const y2 = y + radius * Math.sin(angle + Math.PI / 4);
|
|
1131
|
-
ctx.moveTo(x1, y1);
|
|
1132
|
-
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
|
+
}
|
|
1133
1162
|
}
|
|
1134
1163
|
}
|
|
1135
1164
|
ctx.stroke();
|
|
@@ -1449,20 +1478,23 @@ const $dd5df256f00f6199$export$eeae7765f05012e2 = (ctx, size)=>{
|
|
|
1449
1478
|
const $dd5df256f00f6199$export$3355220a8108efc3 = (ctx, size)=>{
|
|
1450
1479
|
const outerRadius = size / 2;
|
|
1451
1480
|
const innerRadius = size / 4;
|
|
1452
|
-
|
|
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;
|
|
1453
1486
|
ctx.beginPath();
|
|
1454
1487
|
for(let i = 0; i < steps; i++){
|
|
1455
|
-
const angle1 = i
|
|
1456
|
-
|
|
1488
|
+
const angle1 = i * angleStep;
|
|
1489
|
+
const cosA = Math.cos(angle1);
|
|
1490
|
+
const sinA = Math.sin(angle1);
|
|
1457
1491
|
for(let j = 0; j < steps; j++){
|
|
1458
|
-
const phi1 = j
|
|
1459
|
-
const phi2 =
|
|
1460
|
-
const
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
ctx.moveTo(x1, y1);
|
|
1465
|
-
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);
|
|
1466
1498
|
}
|
|
1467
1499
|
}
|
|
1468
1500
|
};
|
|
@@ -2025,6 +2057,43 @@ const $c3de8257a8baa3b0$var$RENDER_STYLES = [
|
|
|
2025
2057
|
function $c3de8257a8baa3b0$export$9fd4e64b2acd410e(rng) {
|
|
2026
2058
|
return $c3de8257a8baa3b0$var$RENDER_STYLES[Math.floor(rng() * $c3de8257a8baa3b0$var$RENDER_STYLES.length)];
|
|
2027
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
|
+
}
|
|
2028
2097
|
function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
2029
2098
|
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
|
|
2030
2099
|
ctx.save();
|
|
@@ -2144,6 +2213,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2144
2213
|
case "hatched":
|
|
2145
2214
|
{
|
|
2146
2215
|
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
2216
|
+
// Optimized: batch all parallel lines into a single path per pass
|
|
2147
2217
|
const savedAlphaH = ctx.globalAlpha;
|
|
2148
2218
|
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
2149
2219
|
ctx.fill();
|
|
@@ -2155,28 +2225,28 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2155
2225
|
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
2156
2226
|
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
2157
2227
|
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
2158
|
-
// Draw parallel lines across the bounding box
|
|
2228
|
+
// Draw parallel lines across the bounding box — batched into single path
|
|
2159
2229
|
const extent = size * 0.8;
|
|
2160
2230
|
const cos = Math.cos(hatchAngle);
|
|
2161
2231
|
const sin = Math.sin(hatchAngle);
|
|
2232
|
+
ctx.beginPath();
|
|
2162
2233
|
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
2163
|
-
ctx.beginPath();
|
|
2164
2234
|
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
2165
2235
|
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
2166
|
-
ctx.stroke();
|
|
2167
2236
|
}
|
|
2237
|
+
ctx.stroke();
|
|
2168
2238
|
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
2169
2239
|
if (!rng || rng() < 0.5) {
|
|
2170
2240
|
const crossAngle = hatchAngle + Math.PI / 2;
|
|
2171
2241
|
const cos2 = Math.cos(crossAngle);
|
|
2172
2242
|
const sin2 = Math.sin(crossAngle);
|
|
2173
2243
|
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
2244
|
+
ctx.beginPath();
|
|
2174
2245
|
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
2175
|
-
ctx.beginPath();
|
|
2176
2246
|
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
2177
2247
|
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
2178
|
-
ctx.stroke();
|
|
2179
2248
|
}
|
|
2249
|
+
ctx.stroke();
|
|
2180
2250
|
}
|
|
2181
2251
|
ctx.restore();
|
|
2182
2252
|
ctx.globalAlpha = savedAlphaH;
|
|
@@ -2214,6 +2284,8 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2214
2284
|
case "stipple":
|
|
2215
2285
|
{
|
|
2216
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.
|
|
2217
2289
|
const savedAlphaS = ctx.globalAlpha;
|
|
2218
2290
|
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
2219
2291
|
ctx.fill(); // ghost fill
|
|
@@ -2221,16 +2293,20 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2221
2293
|
ctx.save();
|
|
2222
2294
|
ctx.clip();
|
|
2223
2295
|
const dotSpacing = Math.max(2, size * 0.03);
|
|
2224
|
-
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;
|
|
2225
2300
|
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
2226
|
-
for(let
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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
|
+
}
|
|
2234
2310
|
}
|
|
2235
2311
|
ctx.restore();
|
|
2236
2312
|
ctx.globalAlpha = savedAlphaS;
|
|
@@ -2263,6 +2339,9 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2263
2339
|
case "noise-grain":
|
|
2264
2340
|
{
|
|
2265
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
|
|
2266
2345
|
const savedAlphaN = ctx.globalAlpha;
|
|
2267
2346
|
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
2268
2347
|
ctx.fill(); // base tint
|
|
@@ -2271,17 +2350,47 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2271
2350
|
ctx.clip();
|
|
2272
2351
|
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
2273
2352
|
const extentN = size * 0.55;
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
const
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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
|
+
}
|
|
2285
2394
|
}
|
|
2286
2395
|
ctx.restore();
|
|
2287
2396
|
ctx.fillStyle = fillColor;
|
|
@@ -2294,6 +2403,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2294
2403
|
case "wood-grain":
|
|
2295
2404
|
{
|
|
2296
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
|
|
2297
2407
|
const savedAlphaW = ctx.globalAlpha;
|
|
2298
2408
|
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
2299
2409
|
ctx.fill(); // base tint
|
|
@@ -2309,17 +2419,19 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2309
2419
|
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
2310
2420
|
const cosG = Math.cos(grainAngle);
|
|
2311
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();
|
|
2312
2426
|
for(let d = -extentW; d <= extentW; d += grainLineSpacing){
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
const
|
|
2317
|
-
|
|
2318
|
-
if (t === -extentW) ctx.moveTo(px, py);
|
|
2319
|
-
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);
|
|
2320
2432
|
}
|
|
2321
|
-
ctx.stroke();
|
|
2322
2433
|
}
|
|
2434
|
+
ctx.stroke();
|
|
2323
2435
|
ctx.restore();
|
|
2324
2436
|
ctx.globalAlpha = savedAlphaW;
|
|
2325
2437
|
ctx.globalAlpha *= 0.35;
|
|
@@ -2381,6 +2493,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2381
2493
|
case "fabric-weave":
|
|
2382
2494
|
{
|
|
2383
2495
|
// Interlocking horizontal/vertical threads clipped to shape
|
|
2496
|
+
// Optimized: batch all horizontal threads into one path, all vertical into another
|
|
2384
2497
|
const savedAlphaF = ctx.globalAlpha;
|
|
2385
2498
|
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
2386
2499
|
ctx.fill(); // ghost base
|
|
@@ -2390,26 +2503,24 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2390
2503
|
const threadSpacing = Math.max(2, size * 0.04);
|
|
2391
2504
|
const extentF = size * 0.55;
|
|
2392
2505
|
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
2506
|
+
// Horizontal threads — batched
|
|
2393
2507
|
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
2394
|
-
|
|
2508
|
+
ctx.beginPath();
|
|
2395
2509
|
for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2396
|
-
ctx.beginPath();
|
|
2397
2510
|
ctx.moveTo(-extentF, y);
|
|
2398
2511
|
ctx.lineTo(extentF, y);
|
|
2399
|
-
ctx.stroke();
|
|
2400
2512
|
}
|
|
2401
|
-
|
|
2513
|
+
ctx.stroke();
|
|
2514
|
+
// Vertical threads (offset by half spacing for weave effect) — batched
|
|
2402
2515
|
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
2403
2516
|
ctx.strokeStyle = fillColor;
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
ctx.lineTo(x, y + threadSpacing);
|
|
2410
|
-
}
|
|
2411
|
-
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);
|
|
2412
2522
|
}
|
|
2523
|
+
ctx.stroke();
|
|
2413
2524
|
ctx.strokeStyle = strokeColor;
|
|
2414
2525
|
ctx.restore();
|
|
2415
2526
|
ctx.globalAlpha = savedAlphaF;
|
|
@@ -2475,14 +2586,17 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2475
2586
|
ctx.translate(x, y);
|
|
2476
2587
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2477
2588
|
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2478
|
-
|
|
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) {
|
|
2479
2593
|
const shadowDist = size * 0.035;
|
|
2480
2594
|
const shadowBlurR = size * 0.06;
|
|
2481
2595
|
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2482
2596
|
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2483
2597
|
ctx.shadowBlur = shadowBlurR;
|
|
2484
2598
|
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2485
|
-
} else if (glowRadius > 0) {
|
|
2599
|
+
} else if (useShadow && glowRadius > 0) {
|
|
2486
2600
|
// Glow / shadow effect (legacy path)
|
|
2487
2601
|
ctx.shadowBlur = glowRadius;
|
|
2488
2602
|
ctx.shadowColor = glowColor || fillColor;
|
|
@@ -2506,30 +2620,27 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2506
2620
|
$c3de8257a8baa3b0$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2507
2621
|
}
|
|
2508
2622
|
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
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
|
+
}
|
|
2513
2630
|
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2514
|
-
|
|
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) {
|
|
2515
2634
|
const hlRadius = size * 0.35;
|
|
2516
2635
|
const hlDist = size * 0.15;
|
|
2517
2636
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2518
2637
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2519
2638
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2520
|
-
//
|
|
2521
|
-
//
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2526
|
-
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2527
|
-
// Blend toward white but keep a hint of the fill's warmth
|
|
2528
|
-
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2529
|
-
}
|
|
2530
|
-
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2531
|
-
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2532
|
-
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2639
|
+
// Use a simple white highlight — the per-shape hex parse was expensive
|
|
2640
|
+
// and the visual difference from tinted highlights is negligible.
|
|
2641
|
+
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2642
|
+
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2643
|
+
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
2533
2644
|
const savedOp = ctx.globalCompositeOperation;
|
|
2534
2645
|
ctx.globalCompositeOperation = "soft-light";
|
|
2535
2646
|
ctx.fillStyle = hlGrad;
|
|
@@ -4071,6 +4182,46 @@ function $f89bc858f7202849$export$f1142fd7da4d6590(rng) {
|
|
|
4071
4182
|
}
|
|
4072
4183
|
|
|
4073
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
|
+
}
|
|
4074
4225
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
4075
4226
|
const $4f72c5a314eddf25$var$SACRED_SHAPES = [
|
|
4076
4227
|
"mandala",
|
|
@@ -4682,19 +4833,20 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4682
4833
|
ctx.beginPath();
|
|
4683
4834
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
4684
4835
|
ctx.stroke();
|
|
4685
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
4836
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
4686
4837
|
if (rng() < 0.5) {
|
|
4687
4838
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
4688
4839
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
4689
4840
|
ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
4841
|
+
ctx.beginPath();
|
|
4690
4842
|
for(let d = 0; d < dotCount; d++){
|
|
4691
4843
|
const angle = rng() * Math.PI * 2;
|
|
4692
4844
|
const dist = rng() * zone.radius * 0.7;
|
|
4693
4845
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
4694
|
-
ctx.
|
|
4846
|
+
ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
|
|
4695
4847
|
ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
|
|
4696
|
-
ctx.fill();
|
|
4697
4848
|
}
|
|
4849
|
+
ctx.fill();
|
|
4698
4850
|
}
|
|
4699
4851
|
// ~30% chance: thin concentric ring inside
|
|
4700
4852
|
if (rng() < 0.3) {
|
|
@@ -4786,6 +4938,26 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4786
4938
|
}
|
|
4787
4939
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4788
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;
|
|
4789
4961
|
for(let layer = 0; layer < layers; layer++){
|
|
4790
4962
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
4791
4963
|
const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
|
|
@@ -4874,7 +5046,26 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4874
5046
|
const shapeRenderStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
|
|
4875
5047
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
4876
5048
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
4877
|
-
|
|
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++;
|
|
4878
5069
|
// Consistent light direction — subtle shadow offset
|
|
4879
5070
|
const shadowDist = hasGlow ? 0 : size * 0.02;
|
|
4880
5071
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
@@ -4929,30 +5120,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4929
5120
|
lightAngle: lightAngle,
|
|
4930
5121
|
scaleFactor: scaleFactor
|
|
4931
5122
|
};
|
|
4932
|
-
if (shouldMirror)
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
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;
|
|
4938
5136
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
4939
5137
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
4940
5138
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
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;
|
|
4955
5156
|
}
|
|
5157
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
4956
5158
|
}
|
|
4957
5159
|
shapePositions.push({
|
|
4958
5160
|
x: finalX,
|
|
@@ -4970,37 +5172,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4970
5172
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4971
5173
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
4972
5174
|
const echoAngle = rng() * Math.PI * 2;
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
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;
|
|
5003
5208
|
}
|
|
5209
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
5004
5210
|
}
|
|
5005
5211
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
5006
5212
|
// Focal depth: shapes near focal points get more detail
|
|
@@ -5008,7 +5214,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5008
5214
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
5009
5215
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
5010
5216
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
5011
|
-
for(let n = 0; n < innerCount; n++){
|
|
5217
|
+
if (extrasAllowed) for(let n = 0; n < innerCount; n++){
|
|
5012
5218
|
// Pick inner shape from palette affinities
|
|
5013
5219
|
const innerSizeFraction = size * 0.25 / adjustedMaxSize;
|
|
5014
5220
|
const innerShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
|
|
@@ -5017,6 +5223,10 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5017
5223
|
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
5018
5224
|
const innerRot = rng() * 360;
|
|
5019
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++;
|
|
5020
5230
|
ctx.globalAlpha = layerOpacity * 0.7;
|
|
5021
5231
|
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
|
|
5022
5232
|
fillColor: innerFill,
|
|
@@ -5025,9 +5235,21 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5025
5235
|
size: innerSize,
|
|
5026
5236
|
rotation: innerRot,
|
|
5027
5237
|
proportionType: "GOLDEN_RATIO",
|
|
5028
|
-
renderStyle:
|
|
5238
|
+
renderStyle: innerStyle,
|
|
5029
5239
|
rng: rng
|
|
5030
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();
|
|
5031
5253
|
}
|
|
5032
5254
|
}
|
|
5033
5255
|
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
@@ -5036,40 +5258,55 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5036
5258
|
const constellation = $4f72c5a314eddf25$var$CONSTELLATIONS[Math.floor(rng() * $4f72c5a314eddf25$var$CONSTELLATIONS.length)];
|
|
5037
5259
|
const members = constellation.build(rng, size);
|
|
5038
5260
|
const groupRotation = rng() * Math.PI * 2;
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
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();
|
|
5073
5310
|
}
|
|
5074
5311
|
}
|
|
5075
5312
|
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
@@ -5080,39 +5317,47 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5080
5317
|
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5081
5318
|
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5082
5319
|
const rhythmShape = shape; // same shape for visual rhythm
|
|
5083
|
-
|
|
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
|
|
5084
5357
|
for(let r = 0; r < rhythmCount; r++){
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
if ($4f72c5a314eddf25$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5089
|
-
rhythmSize *= rhythmDecay;
|
|
5090
|
-
if (rhythmSize < adjustedMinSize) break;
|
|
5091
|
-
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5092
|
-
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5093
|
-
const rhythmFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5094
|
-
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5095
|
-
fillColor: rhythmFill,
|
|
5096
|
-
strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5097
|
-
strokeWidth: strokeWidth * 0.7,
|
|
5098
|
-
size: rhythmSize,
|
|
5099
|
-
rotation: rotation + (r + 1) * 12,
|
|
5100
|
-
proportionType: "GOLDEN_RATIO",
|
|
5101
|
-
renderStyle: finalRenderStyle,
|
|
5102
|
-
rng: rng
|
|
5103
|
-
});
|
|
5104
|
-
shapePositions.push({
|
|
5105
|
-
x: rx,
|
|
5106
|
-
y: ry,
|
|
5107
|
-
size: rhythmSize,
|
|
5108
|
-
shape: rhythmShape
|
|
5109
|
-
});
|
|
5110
|
-
spatialGrid.insert({
|
|
5111
|
-
x: rx,
|
|
5112
|
-
y: ry,
|
|
5113
|
-
size: rhythmSize,
|
|
5114
|
-
shape: rhythmShape
|
|
5115
|
-
});
|
|
5358
|
+
rng();
|
|
5359
|
+
rng();
|
|
5360
|
+
rng();
|
|
5116
5361
|
}
|
|
5117
5362
|
}
|
|
5118
5363
|
}
|
|
@@ -5178,14 +5423,26 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5178
5423
|
}
|
|
5179
5424
|
}
|
|
5180
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).
|
|
5181
5429
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
5182
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;
|
|
5183
5439
|
for(let i = 0; i < numFlowLines; i++){
|
|
5184
5440
|
let fx = rng() * width;
|
|
5185
5441
|
let fy = rng() * height;
|
|
5186
5442
|
const steps = 30 + Math.floor(rng() * 40);
|
|
5187
5443
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
5188
5444
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
5445
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
5189
5446
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
5190
5447
|
const lineColorStart = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
5191
5448
|
const lineColorEnd = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
@@ -5207,19 +5464,22 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5207
5464
|
continue;
|
|
5208
5465
|
}
|
|
5209
5466
|
const t = s / steps;
|
|
5210
|
-
// Taper + pressure
|
|
5211
5467
|
const taper = 1 - t * 0.8;
|
|
5212
5468
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
5213
|
-
|
|
5214
|
-
|
|
5469
|
+
const segWidth = startWidth * taper * pressure;
|
|
5470
|
+
const segAlpha = lineAlpha * taper;
|
|
5215
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);
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
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;
|
|
5223
5483
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
5224
5484
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
5225
5485
|
const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
|
|
@@ -5235,12 +5495,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5235
5495
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
5236
5496
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
5237
5497
|
const bTaper = 1 - bs / branchSteps * 0.9;
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
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;
|
|
5244
5510
|
bPrevX = bx;
|
|
5245
5511
|
bPrevY = by;
|
|
5246
5512
|
}
|
|
@@ -5249,7 +5515,40 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5249
5515
|
prevY = fy;
|
|
5250
5516
|
}
|
|
5251
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
|
+
}
|
|
5252
5550
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
5551
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
5253
5552
|
const energyArchetypes = [
|
|
5254
5553
|
"dense-chaotic",
|
|
5255
5554
|
"cosmic",
|
|
@@ -5260,8 +5559,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5260
5559
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
5261
5560
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
5262
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);
|
|
5263
5567
|
for(let e = 0; e < energyCount; e++){
|
|
5264
|
-
// Pick a random shape to radiate from
|
|
5265
5568
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5266
5569
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
5267
5570
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -5273,15 +5576,38 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5273
5576
|
const sy = source.y + Math.sin(angle) * startDist;
|
|
5274
5577
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
5275
5578
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
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;
|
|
5283
5593
|
}
|
|
5284
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);
|
|
5608
|
+
}
|
|
5609
|
+
ctx.stroke();
|
|
5610
|
+
}
|
|
5285
5611
|
}
|
|
5286
5612
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
5287
5613
|
if (symmetryMode !== "none") {
|
|
@@ -5304,27 +5630,44 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5304
5630
|
ctx.restore();
|
|
5305
5631
|
}
|
|
5306
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.
|
|
5307
5635
|
const noiseRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 777));
|
|
5308
|
-
const
|
|
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);
|
|
5309
5640
|
try {
|
|
5310
5641
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5311
5642
|
const data = imageData.data;
|
|
5312
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)
|
|
5313
5646
|
for(let i = 0; i < noiseDensity; i++){
|
|
5314
5647
|
const nx = Math.floor(noiseRng() * width);
|
|
5315
5648
|
const ny = Math.floor(noiseRng() * height);
|
|
5316
5649
|
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5317
|
-
|
|
5318
|
-
|
|
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;
|
|
5319
5666
|
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5320
|
-
const idx = (
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5325
|
-
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5326
|
-
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5327
|
-
// Keep existing alpha
|
|
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;
|
|
5328
5671
|
}
|
|
5329
5672
|
}
|
|
5330
5673
|
ctx.putImageData(imageData, 0, 0);
|
|
@@ -5354,10 +5697,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5354
5697
|
ctx.fillStyle = vigGrad;
|
|
5355
5698
|
ctx.fillRect(0, 0, width, height);
|
|
5356
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).
|
|
5357
5702
|
if (shapePositions.length > 1) {
|
|
5358
5703
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5359
5704
|
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5360
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([]);
|
|
5361
5712
|
for(let i = 0; i < numCurves; i++){
|
|
5362
5713
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
5363
5714
|
const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
|
|
@@ -5374,11 +5725,32 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5374
5725
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5375
5726
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5376
5727
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
5377
|
-
|
|
5378
|
-
|
|
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];
|
|
5379
5748
|
ctx.beginPath();
|
|
5380
|
-
|
|
5381
|
-
|
|
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
|
+
}
|
|
5382
5754
|
ctx.stroke();
|
|
5383
5755
|
}
|
|
5384
5756
|
}
|
|
@@ -5496,11 +5868,14 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5496
5868
|
}
|
|
5497
5869
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5498
5870
|
// Vine tendrils — organic curving lines along edges
|
|
5871
|
+
// Optimized: batch all tendrils into a single path
|
|
5499
5872
|
ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5500
5873
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5501
5874
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5502
5875
|
ctx.lineCap = "round";
|
|
5503
5876
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5877
|
+
ctx.beginPath();
|
|
5878
|
+
const leafPositions = [];
|
|
5504
5879
|
for(let t = 0; t < tendrilCount; t++){
|
|
5505
5880
|
// Start from a random edge point
|
|
5506
5881
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -5518,7 +5893,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5518
5893
|
tx = width - borderPad;
|
|
5519
5894
|
ty = borderRng() * height;
|
|
5520
5895
|
}
|
|
5521
|
-
ctx.beginPath();
|
|
5522
5896
|
ctx.moveTo(tx, ty);
|
|
5523
5897
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5524
5898
|
for(let s = 0; s < segs; s++){
|
|
@@ -5532,14 +5906,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5532
5906
|
ty = cpy3;
|
|
5533
5907
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5534
5908
|
}
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
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);
|
|
5542
5924
|
}
|
|
5925
|
+
ctx.fill();
|
|
5543
5926
|
}
|
|
5544
5927
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5545
5928
|
// Star-studded arcs along edges
|
|
@@ -5554,8 +5937,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5554
5937
|
ctx.beginPath();
|
|
5555
5938
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5556
5939
|
ctx.stroke();
|
|
5557
|
-
// Scatter small stars along the border region
|
|
5940
|
+
// Scatter small stars along the border region — batched into single path
|
|
5558
5941
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5942
|
+
ctx.beginPath();
|
|
5559
5943
|
for(let s = 0; s < starCount; s++){
|
|
5560
5944
|
const edge = Math.floor(borderRng() * 4);
|
|
5561
5945
|
let sx, sy;
|
|
@@ -5574,7 +5958,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5574
5958
|
}
|
|
5575
5959
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5576
5960
|
// 4-point star
|
|
5577
|
-
ctx.beginPath();
|
|
5578
5961
|
for(let p = 0; p < 8; p++){
|
|
5579
5962
|
const a = p / 8 * Math.PI * 2;
|
|
5580
5963
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -5584,8 +5967,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5584
5967
|
else ctx.lineTo(px2, py2);
|
|
5585
5968
|
}
|
|
5586
5969
|
ctx.closePath();
|
|
5587
|
-
ctx.fill();
|
|
5588
5970
|
}
|
|
5971
|
+
ctx.fill();
|
|
5589
5972
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5590
5973
|
// Thin single rule — understated elegance
|
|
5591
5974
|
ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|