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/browser.js
CHANGED
|
@@ -508,13 +508,21 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
// ── Standalone color utilities ──────────────────────────────────────
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
511
|
+
// ── Cached hex→RGB parse — avoids repeated parseInt/substring on hot path ──
|
|
512
|
+
const $b5a262d09b87e373$var$_rgbCache = new Map();
|
|
513
|
+
const $b5a262d09b87e373$var$_RGB_CACHE_MAX = 512;
|
|
514
|
+
/** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. Cached. */ function $b5a262d09b87e373$var$hexToRgb(hex) {
|
|
515
|
+
let cached = $b5a262d09b87e373$var$_rgbCache.get(hex);
|
|
516
|
+
if (cached) return cached;
|
|
517
|
+
const c = hex.charAt(0) === "#" ? hex.substring(1) : hex;
|
|
518
|
+
cached = [
|
|
514
519
|
parseInt(c.substring(0, 2), 16),
|
|
515
520
|
parseInt(c.substring(2, 4), 16),
|
|
516
521
|
parseInt(c.substring(4, 6), 16)
|
|
517
522
|
];
|
|
523
|
+
if ($b5a262d09b87e373$var$_rgbCache.size >= $b5a262d09b87e373$var$_RGB_CACHE_MAX) $b5a262d09b87e373$var$_rgbCache.clear();
|
|
524
|
+
$b5a262d09b87e373$var$_rgbCache.set(hex, cached);
|
|
525
|
+
return cached;
|
|
518
526
|
}
|
|
519
527
|
/** Format [r, g, b] back to #RRGGBB. */ function $b5a262d09b87e373$var$rgbToHex(r, g, b) {
|
|
520
528
|
const clamp = (v)=>Math.max(0, Math.min(255, Math.round(v)));
|
|
@@ -571,7 +579,9 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
571
579
|
}
|
|
572
580
|
function $b5a262d09b87e373$export$f2121afcad3d553f(hex, alpha) {
|
|
573
581
|
const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex);
|
|
574
|
-
|
|
582
|
+
// Quantize alpha to 3 decimal places without toFixed overhead
|
|
583
|
+
const a = Math.round(alpha * 1000) / 1000;
|
|
584
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
575
585
|
}
|
|
576
586
|
function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
|
|
577
587
|
if (colors.length < 3) return {
|
|
@@ -651,12 +661,21 @@ function $b5a262d09b87e373$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
|
651
661
|
const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
|
|
652
662
|
return $b5a262d09b87e373$var$hslToHex($b5a262d09b87e373$var$shiftHueToward(h, target, amount), s, l);
|
|
653
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Compute relative luminance of a hex color (0 = black, 1 = white).
|
|
666
|
+
* Uses the sRGB luminance formula from WCAG. Cached.
|
|
667
|
+
*/ const $b5a262d09b87e373$var$_lumCache = new Map();
|
|
654
668
|
function $b5a262d09b87e373$export$5c6e3c2b59b7fbbe(hex) {
|
|
669
|
+
let cached = $b5a262d09b87e373$var$_lumCache.get(hex);
|
|
670
|
+
if (cached !== undefined) return cached;
|
|
655
671
|
const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex).map((c)=>{
|
|
656
672
|
const s = c / 255;
|
|
657
673
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
658
674
|
});
|
|
659
|
-
|
|
675
|
+
cached = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
676
|
+
if ($b5a262d09b87e373$var$_lumCache.size >= 512) $b5a262d09b87e373$var$_lumCache.clear();
|
|
677
|
+
$b5a262d09b87e373$var$_lumCache.set(hex, cached);
|
|
678
|
+
return cached;
|
|
660
679
|
}
|
|
661
680
|
function $b5a262d09b87e373$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
|
|
662
681
|
const fgLum = $b5a262d09b87e373$export$5c6e3c2b59b7fbbe(fgHex);
|
|
@@ -1092,21 +1111,31 @@ const $f0f1a7293548e501$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
|
|
|
1092
1111
|
(0, $ce2c52df8af02e62$export$e46c5570db033611)(ctx, size, finalConfig);
|
|
1093
1112
|
const gridSize = 8;
|
|
1094
1113
|
const unit = size / gridSize;
|
|
1114
|
+
const radius = unit / 2;
|
|
1115
|
+
// Pre-compute the 8 star-point angle pairs (cos/sin) — avoids 648 trig calls
|
|
1116
|
+
const starPoints = [];
|
|
1117
|
+
for(let k = 0; k < 8; k++){
|
|
1118
|
+
const angle = Math.PI / 4 * k;
|
|
1119
|
+
const angle2 = angle + Math.PI / 4;
|
|
1120
|
+
starPoints.push({
|
|
1121
|
+
c1: Math.cos(angle) * radius,
|
|
1122
|
+
s1: Math.sin(angle) * radius,
|
|
1123
|
+
c2: Math.cos(angle2) * radius,
|
|
1124
|
+
s2: Math.sin(angle2) * radius
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1095
1127
|
ctx.beginPath();
|
|
1096
1128
|
// Create base grid
|
|
1097
|
-
for(let i = 0; i <= gridSize; i++)
|
|
1129
|
+
for(let i = 0; i <= gridSize; i++){
|
|
1098
1130
|
const x = (i - gridSize / 2) * unit;
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const y2 = y + radius * Math.sin(angle + Math.PI / 4);
|
|
1108
|
-
ctx.moveTo(x1, y1);
|
|
1109
|
-
ctx.lineTo(x2, y2);
|
|
1131
|
+
for(let j = 0; j <= gridSize; j++){
|
|
1132
|
+
const y = (j - gridSize / 2) * unit;
|
|
1133
|
+
// Draw star pattern at each intersection using pre-computed offsets
|
|
1134
|
+
for(let k = 0; k < 8; k++){
|
|
1135
|
+
const sp = starPoints[k];
|
|
1136
|
+
ctx.moveTo(x + sp.c1, y + sp.s1);
|
|
1137
|
+
ctx.lineTo(x + sp.c2, y + sp.s2);
|
|
1138
|
+
}
|
|
1110
1139
|
}
|
|
1111
1140
|
}
|
|
1112
1141
|
ctx.stroke();
|
|
@@ -1426,20 +1455,23 @@ const $77711f013715e6da$export$eeae7765f05012e2 = (ctx, size)=>{
|
|
|
1426
1455
|
const $77711f013715e6da$export$3355220a8108efc3 = (ctx, size)=>{
|
|
1427
1456
|
const outerRadius = size / 2;
|
|
1428
1457
|
const innerRadius = size / 4;
|
|
1429
|
-
|
|
1458
|
+
// Adaptive step count: fewer segments for small shapes where detail isn't visible.
|
|
1459
|
+
// 36×36 = 1296 segments at full size; at size < 60 we drop to 16×16 = 256.
|
|
1460
|
+
const steps = size < 60 ? 16 : size < 150 ? 24 : 36;
|
|
1461
|
+
const TWO_PI = Math.PI * 2;
|
|
1462
|
+
const angleStep = TWO_PI / steps;
|
|
1430
1463
|
ctx.beginPath();
|
|
1431
1464
|
for(let i = 0; i < steps; i++){
|
|
1432
|
-
const angle1 = i
|
|
1433
|
-
|
|
1465
|
+
const angle1 = i * angleStep;
|
|
1466
|
+
const cosA = Math.cos(angle1);
|
|
1467
|
+
const sinA = Math.sin(angle1);
|
|
1434
1468
|
for(let j = 0; j < steps; j++){
|
|
1435
|
-
const phi1 = j
|
|
1436
|
-
const phi2 =
|
|
1437
|
-
const
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
ctx.moveTo(x1, y1);
|
|
1442
|
-
ctx.lineTo(x2, y2);
|
|
1469
|
+
const phi1 = j * angleStep;
|
|
1470
|
+
const phi2 = phi1 + angleStep;
|
|
1471
|
+
const r1 = outerRadius + innerRadius * Math.cos(phi1);
|
|
1472
|
+
const r2 = outerRadius + innerRadius * Math.cos(phi2);
|
|
1473
|
+
ctx.moveTo(r1 * cosA, r1 * sinA);
|
|
1474
|
+
ctx.lineTo(r2 * cosA, r2 * sinA);
|
|
1443
1475
|
}
|
|
1444
1476
|
}
|
|
1445
1477
|
};
|
|
@@ -2002,6 +2034,43 @@ const $e0f99502ff383dd8$var$RENDER_STYLES = [
|
|
|
2002
2034
|
function $e0f99502ff383dd8$export$9fd4e64b2acd410e(rng) {
|
|
2003
2035
|
return $e0f99502ff383dd8$var$RENDER_STYLES[Math.floor(rng() * $e0f99502ff383dd8$var$RENDER_STYLES.length)];
|
|
2004
2036
|
}
|
|
2037
|
+
const $e0f99502ff383dd8$export$2f738f61a8c15e07 = {
|
|
2038
|
+
"fill-and-stroke": 1,
|
|
2039
|
+
"fill-only": 0.5,
|
|
2040
|
+
"stroke-only": 1,
|
|
2041
|
+
"double-stroke": 1.5,
|
|
2042
|
+
"dashed": 1,
|
|
2043
|
+
"watercolor": 7,
|
|
2044
|
+
"hatched": 3,
|
|
2045
|
+
"incomplete": 1,
|
|
2046
|
+
"stipple": 90,
|
|
2047
|
+
"stencil": 2,
|
|
2048
|
+
"noise-grain": 400,
|
|
2049
|
+
"wood-grain": 10,
|
|
2050
|
+
"marble-vein": 4,
|
|
2051
|
+
"fabric-weave": 6,
|
|
2052
|
+
"hand-drawn": 5
|
|
2053
|
+
};
|
|
2054
|
+
function $e0f99502ff383dd8$export$909ab0580e273f19(style) {
|
|
2055
|
+
switch(style){
|
|
2056
|
+
case "noise-grain":
|
|
2057
|
+
return "hatched";
|
|
2058
|
+
case "stipple":
|
|
2059
|
+
return "dashed";
|
|
2060
|
+
case "wood-grain":
|
|
2061
|
+
return "hatched";
|
|
2062
|
+
case "watercolor":
|
|
2063
|
+
return "fill-and-stroke";
|
|
2064
|
+
case "fabric-weave":
|
|
2065
|
+
return "hatched";
|
|
2066
|
+
case "hand-drawn":
|
|
2067
|
+
return "fill-and-stroke";
|
|
2068
|
+
case "marble-vein":
|
|
2069
|
+
return "stroke-only";
|
|
2070
|
+
default:
|
|
2071
|
+
return style;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2005
2074
|
function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
2006
2075
|
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
|
|
2007
2076
|
ctx.save();
|
|
@@ -2121,6 +2190,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2121
2190
|
case "hatched":
|
|
2122
2191
|
{
|
|
2123
2192
|
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
2193
|
+
// Optimized: batch all parallel lines into a single path per pass
|
|
2124
2194
|
const savedAlphaH = ctx.globalAlpha;
|
|
2125
2195
|
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
2126
2196
|
ctx.fill();
|
|
@@ -2132,28 +2202,28 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2132
2202
|
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
2133
2203
|
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
2134
2204
|
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
2135
|
-
// Draw parallel lines across the bounding box
|
|
2205
|
+
// Draw parallel lines across the bounding box — batched into single path
|
|
2136
2206
|
const extent = size * 0.8;
|
|
2137
2207
|
const cos = Math.cos(hatchAngle);
|
|
2138
2208
|
const sin = Math.sin(hatchAngle);
|
|
2209
|
+
ctx.beginPath();
|
|
2139
2210
|
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
2140
|
-
ctx.beginPath();
|
|
2141
2211
|
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
2142
2212
|
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
2143
|
-
ctx.stroke();
|
|
2144
2213
|
}
|
|
2214
|
+
ctx.stroke();
|
|
2145
2215
|
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
2146
2216
|
if (!rng || rng() < 0.5) {
|
|
2147
2217
|
const crossAngle = hatchAngle + Math.PI / 2;
|
|
2148
2218
|
const cos2 = Math.cos(crossAngle);
|
|
2149
2219
|
const sin2 = Math.sin(crossAngle);
|
|
2150
2220
|
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
2221
|
+
ctx.beginPath();
|
|
2151
2222
|
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
2152
|
-
ctx.beginPath();
|
|
2153
2223
|
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
2154
2224
|
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
2155
|
-
ctx.stroke();
|
|
2156
2225
|
}
|
|
2226
|
+
ctx.stroke();
|
|
2157
2227
|
}
|
|
2158
2228
|
ctx.restore();
|
|
2159
2229
|
ctx.globalAlpha = savedAlphaH;
|
|
@@ -2191,6 +2261,8 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2191
2261
|
case "stipple":
|
|
2192
2262
|
{
|
|
2193
2263
|
// Dot-fill texture — clip to shape, then scatter dots
|
|
2264
|
+
// Optimized: use fillRect instead of arc for dots (much cheaper to render),
|
|
2265
|
+
// and cap total dot count to avoid O(size²) blowup on large shapes.
|
|
2194
2266
|
const savedAlphaS = ctx.globalAlpha;
|
|
2195
2267
|
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
2196
2268
|
ctx.fill(); // ghost fill
|
|
@@ -2198,16 +2270,20 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2198
2270
|
ctx.save();
|
|
2199
2271
|
ctx.clip();
|
|
2200
2272
|
const dotSpacing = Math.max(2, size * 0.03);
|
|
2201
|
-
const
|
|
2273
|
+
const extentS = size * 0.55;
|
|
2274
|
+
// Cap total dots: beyond ~900 (30×30 grid) the visual density plateaus
|
|
2275
|
+
const maxDotsPerAxis = Math.min(Math.ceil(extentS * 2 / dotSpacing), 30);
|
|
2276
|
+
const actualSpacing = extentS * 2 / maxDotsPerAxis;
|
|
2202
2277
|
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
2203
|
-
for(let
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2278
|
+
for(let xi = 0; xi < maxDotsPerAxis; xi++){
|
|
2279
|
+
const dx = -extentS + xi * actualSpacing;
|
|
2280
|
+
for(let yi = 0; yi < maxDotsPerAxis; yi++){
|
|
2281
|
+
const dy = -extentS + yi * actualSpacing;
|
|
2282
|
+
const jx = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2283
|
+
const jy = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2284
|
+
const dotD = rng ? actualSpacing * (0.3 + rng() * 0.4) : actualSpacing * 0.4;
|
|
2285
|
+
ctx.fillRect(dx + jx - dotD * 0.5, dy + jy - dotD * 0.5, dotD, dotD);
|
|
2286
|
+
}
|
|
2211
2287
|
}
|
|
2212
2288
|
ctx.restore();
|
|
2213
2289
|
ctx.globalAlpha = savedAlphaS;
|
|
@@ -2240,6 +2316,9 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2240
2316
|
case "noise-grain":
|
|
2241
2317
|
{
|
|
2242
2318
|
// Procedural noise grain texture clipped to shape boundary
|
|
2319
|
+
// Optimized: cap grid to max 40×40 = 1600 dots (was unbounded at O(size²)),
|
|
2320
|
+
// quantize alpha into buckets to minimize globalAlpha state changes,
|
|
2321
|
+
// and batch dots by brightness (black/white) × alpha bucket
|
|
2243
2322
|
const savedAlphaN = ctx.globalAlpha;
|
|
2244
2323
|
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
2245
2324
|
ctx.fill(); // base tint
|
|
@@ -2248,17 +2327,47 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2248
2327
|
ctx.clip();
|
|
2249
2328
|
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
2250
2329
|
const extentN = size * 0.55;
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
const
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2330
|
+
if (rng) {
|
|
2331
|
+
// Cap grid to max 40 dots per axis — beyond this the grain is
|
|
2332
|
+
// visually indistinguishable but cost scales quadratically.
|
|
2333
|
+
const maxGrainPerAxis = Math.min(Math.ceil(extentN * 2 / grainSpacing), 40);
|
|
2334
|
+
const actualGrainSpacing = extentN * 2 / maxGrainPerAxis;
|
|
2335
|
+
// 4 alpha buckets: 0.2, 0.3, 0.4, 0.5 — covers the 0.15-0.50 range
|
|
2336
|
+
const BUCKETS = 4;
|
|
2337
|
+
const bucketMin = 0.15;
|
|
2338
|
+
const bucketRange = 0.35;
|
|
2339
|
+
// [black_bucket0, black_bucket1, ..., white_bucket0, ...]
|
|
2340
|
+
const buckets = [];
|
|
2341
|
+
for(let i = 0; i < BUCKETS * 2; i++)buckets.push([]);
|
|
2342
|
+
for(let xi = 0; xi < maxGrainPerAxis; xi++){
|
|
2343
|
+
const gx = -extentN + xi * actualGrainSpacing;
|
|
2344
|
+
for(let yi = 0; yi < maxGrainPerAxis; yi++){
|
|
2345
|
+
const gy = -extentN + yi * actualGrainSpacing;
|
|
2346
|
+
const jx = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2347
|
+
const jy = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2348
|
+
const isWhite = rng() > 0.5;
|
|
2349
|
+
const dotAlpha = bucketMin + rng() * bucketRange;
|
|
2350
|
+
const dotSize = actualGrainSpacing * (0.3 + rng() * 0.5);
|
|
2351
|
+
const bucketIdx = Math.min(BUCKETS - 1, Math.floor((dotAlpha - bucketMin) / bucketRange * BUCKETS));
|
|
2352
|
+
const offset = isWhite ? BUCKETS : 0;
|
|
2353
|
+
buckets[offset + bucketIdx].push({
|
|
2354
|
+
x: gx + jx,
|
|
2355
|
+
y: gy + jy,
|
|
2356
|
+
s: dotSize
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
// Render each bucket: 2 colors × 4 alpha levels = 8 state changes total
|
|
2361
|
+
for(let color = 0; color < 2; color++){
|
|
2362
|
+
ctx.fillStyle = color === 0 ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
2363
|
+
for(let b = 0; b < BUCKETS; b++){
|
|
2364
|
+
const dots = buckets[color * BUCKETS + b];
|
|
2365
|
+
if (dots.length === 0) continue;
|
|
2366
|
+
const alpha = bucketMin + (b + 0.5) / BUCKETS * bucketRange;
|
|
2367
|
+
ctx.globalAlpha = savedAlphaN * alpha;
|
|
2368
|
+
for(let i = 0; i < dots.length; i++)ctx.fillRect(dots[i].x, dots[i].y, dots[i].s, dots[i].s);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2262
2371
|
}
|
|
2263
2372
|
ctx.restore();
|
|
2264
2373
|
ctx.fillStyle = fillColor;
|
|
@@ -2271,6 +2380,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2271
2380
|
case "wood-grain":
|
|
2272
2381
|
{
|
|
2273
2382
|
// Parallel wavy lines simulating wood grain, clipped to shape
|
|
2383
|
+
// Optimized: batch all grain lines into a single path, increased step from 2 to 4
|
|
2274
2384
|
const savedAlphaW = ctx.globalAlpha;
|
|
2275
2385
|
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
2276
2386
|
ctx.fill(); // base tint
|
|
@@ -2286,17 +2396,19 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2286
2396
|
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
2287
2397
|
const cosG = Math.cos(grainAngle);
|
|
2288
2398
|
const sinG = Math.sin(grainAngle);
|
|
2399
|
+
const waveCoeff = waveFreq * Math.PI;
|
|
2400
|
+
const invExtentW = 1 / extentW;
|
|
2401
|
+
// Batch all grain lines into a single path
|
|
2402
|
+
ctx.beginPath();
|
|
2289
2403
|
for(let d = -extentW; d <= extentW; d += grainLineSpacing){
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
if (t === -extentW) ctx.moveTo(px, py);
|
|
2296
|
-
else ctx.lineTo(px, py);
|
|
2404
|
+
const firstWave = Math.sin(-extentW * invExtentW * waveCoeff) * waveAmp;
|
|
2405
|
+
ctx.moveTo(-extentW * cosG - (d + firstWave) * sinG, -extentW * sinG + (d + firstWave) * cosG);
|
|
2406
|
+
for(let t = -extentW + 4; t <= extentW; t += 4){
|
|
2407
|
+
const wave = Math.sin(t * invExtentW * waveCoeff) * waveAmp;
|
|
2408
|
+
ctx.lineTo(t * cosG - (d + wave) * sinG, t * sinG + (d + wave) * cosG);
|
|
2297
2409
|
}
|
|
2298
|
-
ctx.stroke();
|
|
2299
2410
|
}
|
|
2411
|
+
ctx.stroke();
|
|
2300
2412
|
ctx.restore();
|
|
2301
2413
|
ctx.globalAlpha = savedAlphaW;
|
|
2302
2414
|
ctx.globalAlpha *= 0.35;
|
|
@@ -2358,6 +2470,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2358
2470
|
case "fabric-weave":
|
|
2359
2471
|
{
|
|
2360
2472
|
// Interlocking horizontal/vertical threads clipped to shape
|
|
2473
|
+
// Optimized: batch all horizontal threads into one path, all vertical into another
|
|
2361
2474
|
const savedAlphaF = ctx.globalAlpha;
|
|
2362
2475
|
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
2363
2476
|
ctx.fill(); // ghost base
|
|
@@ -2367,26 +2480,24 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2367
2480
|
const threadSpacing = Math.max(2, size * 0.04);
|
|
2368
2481
|
const extentF = size * 0.55;
|
|
2369
2482
|
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
2483
|
+
// Horizontal threads — batched
|
|
2370
2484
|
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
2371
|
-
|
|
2485
|
+
ctx.beginPath();
|
|
2372
2486
|
for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2373
|
-
ctx.beginPath();
|
|
2374
2487
|
ctx.moveTo(-extentF, y);
|
|
2375
2488
|
ctx.lineTo(extentF, y);
|
|
2376
|
-
ctx.stroke();
|
|
2377
2489
|
}
|
|
2378
|
-
|
|
2490
|
+
ctx.stroke();
|
|
2491
|
+
// Vertical threads (offset by half spacing for weave effect) — batched
|
|
2379
2492
|
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
2380
2493
|
ctx.strokeStyle = fillColor;
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
ctx.lineTo(x, y + threadSpacing);
|
|
2387
|
-
}
|
|
2388
|
-
ctx.stroke();
|
|
2494
|
+
ctx.beginPath();
|
|
2495
|
+
for(let x = -extentF; x <= extentF; x += threadSpacing * 2)for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2496
|
+
// Over-under: draw segment, skip segment
|
|
2497
|
+
ctx.moveTo(x, y);
|
|
2498
|
+
ctx.lineTo(x, y + threadSpacing);
|
|
2389
2499
|
}
|
|
2500
|
+
ctx.stroke();
|
|
2390
2501
|
ctx.strokeStyle = strokeColor;
|
|
2391
2502
|
ctx.restore();
|
|
2392
2503
|
ctx.globalAlpha = savedAlphaF;
|
|
@@ -2452,14 +2563,17 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2452
2563
|
ctx.translate(x, y);
|
|
2453
2564
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2454
2565
|
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2455
|
-
|
|
2566
|
+
// Skip shadow entirely for small shapes (< 20px) — the blur is expensive
|
|
2567
|
+
// and visually imperceptible at that scale.
|
|
2568
|
+
const useShadow = size >= 20;
|
|
2569
|
+
if (useShadow && lightAngle !== undefined) {
|
|
2456
2570
|
const shadowDist = size * 0.035;
|
|
2457
2571
|
const shadowBlurR = size * 0.06;
|
|
2458
2572
|
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2459
2573
|
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2460
2574
|
ctx.shadowBlur = shadowBlurR;
|
|
2461
2575
|
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2462
|
-
} else if (glowRadius > 0) {
|
|
2576
|
+
} else if (useShadow && glowRadius > 0) {
|
|
2463
2577
|
// Glow / shadow effect (legacy path)
|
|
2464
2578
|
ctx.shadowBlur = glowRadius;
|
|
2465
2579
|
ctx.shadowColor = glowColor || fillColor;
|
|
@@ -2483,30 +2597,27 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2483
2597
|
$e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2484
2598
|
}
|
|
2485
2599
|
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2600
|
+
// Only reset if we actually set shadow (avoids unnecessary state changes)
|
|
2601
|
+
if (useShadow && (lightAngle !== undefined || glowRadius > 0)) {
|
|
2602
|
+
ctx.shadowBlur = 0;
|
|
2603
|
+
ctx.shadowOffsetX = 0;
|
|
2604
|
+
ctx.shadowOffsetY = 0;
|
|
2605
|
+
ctx.shadowColor = "transparent";
|
|
2606
|
+
}
|
|
2490
2607
|
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2491
|
-
|
|
2608
|
+
// Skip for small shapes (< 30px) — gradient creation + composite op
|
|
2609
|
+
// switch is expensive and the highlight is invisible at small sizes.
|
|
2610
|
+
if (lightAngle !== undefined && size > 30 && rng) {
|
|
2492
2611
|
const hlRadius = size * 0.35;
|
|
2493
2612
|
const hlDist = size * 0.15;
|
|
2494
2613
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2495
2614
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2496
2615
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2497
|
-
//
|
|
2498
|
-
//
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2503
|
-
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2504
|
-
// Blend toward white but keep a hint of the fill's warmth
|
|
2505
|
-
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2506
|
-
}
|
|
2507
|
-
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2508
|
-
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2509
|
-
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2616
|
+
// Use a simple white highlight — the per-shape hex parse was expensive
|
|
2617
|
+
// and the visual difference from tinted highlights is negligible.
|
|
2618
|
+
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2619
|
+
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2620
|
+
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
2510
2621
|
const savedOp = ctx.globalCompositeOperation;
|
|
2511
2622
|
ctx.globalCompositeOperation = "soft-light";
|
|
2512
2623
|
ctx.fillStyle = hlGrad;
|
|
@@ -4048,6 +4159,46 @@ function $68a238ccd77f2bcd$export$f1142fd7da4d6590(rng) {
|
|
|
4048
4159
|
}
|
|
4049
4160
|
|
|
4050
4161
|
|
|
4162
|
+
// ── Render style cost weights (normalized: fill-and-stroke = 1) ─────
|
|
4163
|
+
// Based on benchmark measurements. Used by the complexity budget to
|
|
4164
|
+
// cap total rendering work and downgrade expensive styles when needed.
|
|
4165
|
+
const $1f63dc64b5593c73$var$RENDER_STYLE_COST = {
|
|
4166
|
+
"fill-and-stroke": 1,
|
|
4167
|
+
"fill-only": 0.5,
|
|
4168
|
+
"stroke-only": 1,
|
|
4169
|
+
"double-stroke": 1.5,
|
|
4170
|
+
"dashed": 1,
|
|
4171
|
+
"watercolor": 7,
|
|
4172
|
+
"hatched": 3,
|
|
4173
|
+
"incomplete": 1,
|
|
4174
|
+
"stipple": 90,
|
|
4175
|
+
"stencil": 2,
|
|
4176
|
+
"noise-grain": 400,
|
|
4177
|
+
"wood-grain": 10,
|
|
4178
|
+
"marble-vein": 4,
|
|
4179
|
+
"fabric-weave": 6,
|
|
4180
|
+
"hand-drawn": 5
|
|
4181
|
+
};
|
|
4182
|
+
function $1f63dc64b5593c73$var$downgradeRenderStyle(style) {
|
|
4183
|
+
switch(style){
|
|
4184
|
+
case "noise-grain":
|
|
4185
|
+
return "hatched";
|
|
4186
|
+
case "stipple":
|
|
4187
|
+
return "dashed";
|
|
4188
|
+
case "wood-grain":
|
|
4189
|
+
return "hatched";
|
|
4190
|
+
case "watercolor":
|
|
4191
|
+
return "fill-and-stroke";
|
|
4192
|
+
case "fabric-weave":
|
|
4193
|
+
return "hatched";
|
|
4194
|
+
case "hand-drawn":
|
|
4195
|
+
return "fill-and-stroke";
|
|
4196
|
+
case "marble-vein":
|
|
4197
|
+
return "stroke-only";
|
|
4198
|
+
default:
|
|
4199
|
+
return style;
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4051
4202
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
4052
4203
|
const $1f63dc64b5593c73$var$SACRED_SHAPES = [
|
|
4053
4204
|
"mandala",
|
|
@@ -4657,19 +4808,20 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4657
4808
|
ctx.beginPath();
|
|
4658
4809
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
4659
4810
|
ctx.stroke();
|
|
4660
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
4811
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
4661
4812
|
if (rng() < 0.5) {
|
|
4662
4813
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
4663
4814
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
4664
4815
|
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
4816
|
+
ctx.beginPath();
|
|
4665
4817
|
for(let d = 0; d < dotCount; d++){
|
|
4666
4818
|
const angle = rng() * Math.PI * 2;
|
|
4667
4819
|
const dist = rng() * zone.radius * 0.7;
|
|
4668
4820
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
4669
|
-
ctx.
|
|
4821
|
+
ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
|
|
4670
4822
|
ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
|
|
4671
|
-
ctx.fill();
|
|
4672
4823
|
}
|
|
4824
|
+
ctx.fill();
|
|
4673
4825
|
}
|
|
4674
4826
|
// ~30% chance: thin concentric ring inside
|
|
4675
4827
|
if (rng() < 0.3) {
|
|
@@ -4761,6 +4913,26 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4761
4913
|
}
|
|
4762
4914
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4763
4915
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4916
|
+
// ── Complexity budget — caps total rendering work ──────────────
|
|
4917
|
+
// Budget scales with pixel area so larger canvases get proportionally
|
|
4918
|
+
// more headroom. The multiplier extras (glazing, echoes, nesting,
|
|
4919
|
+
// constellations, rhythm) are gated behind the budget; when it runs
|
|
4920
|
+
// low they are skipped. When it's exhausted, expensive render styles
|
|
4921
|
+
// are downgraded to cheaper alternatives.
|
|
4922
|
+
//
|
|
4923
|
+
// RNG values are always consumed even when skipping, so the
|
|
4924
|
+
// deterministic sequence for shapes that *do* render is preserved.
|
|
4925
|
+
const pixelArea = width * height;
|
|
4926
|
+
const BUDGET_PER_MEGAPIXEL = 6000; // cost units per 1M pixels
|
|
4927
|
+
let complexityBudget = pixelArea / 1000000 * BUDGET_PER_MEGAPIXEL;
|
|
4928
|
+
const totalBudget = complexityBudget;
|
|
4929
|
+
const budgetForExtras = complexityBudget * 0.25; // reserve 25% for multiplier extras
|
|
4930
|
+
let extrasSpent = 0;
|
|
4931
|
+
// Hard cap on clip-heavy render styles (stipple, noise-grain).
|
|
4932
|
+
// These generate O(size²) fillRect calls per shape and dominate
|
|
4933
|
+
// worst-case render time. Cap scales with pixel area.
|
|
4934
|
+
const MAX_CLIP_HEAVY_SHAPES = Math.max(4, Math.floor(8 * (pixelArea / 1000000)));
|
|
4935
|
+
let clipHeavyCount = 0;
|
|
4764
4936
|
for(let layer = 0; layer < layers; layer++){
|
|
4765
4937
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
4766
4938
|
const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
|
|
@@ -4849,7 +5021,26 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4849
5021
|
const shapeRenderStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
|
|
4850
5022
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
4851
5023
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
4852
|
-
|
|
5024
|
+
let finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
|
|
5025
|
+
// Budget check: downgrade expensive styles proportionally —
|
|
5026
|
+
// the more expensive the style, the earlier it gets downgraded.
|
|
5027
|
+
// noise-grain (400) downgrades when budget < 20% remaining,
|
|
5028
|
+
// stipple (90) when < 82%, wood-grain (10) when < 98%.
|
|
5029
|
+
let styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5030
|
+
if (styleCost > 3) {
|
|
5031
|
+
const downgradeThreshold = Math.min(0.85, styleCost / 500);
|
|
5032
|
+
if (complexityBudget < totalBudget * (1 - downgradeThreshold)) {
|
|
5033
|
+
finalRenderStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(finalRenderStyle);
|
|
5034
|
+
styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
// Hard cap: clip-heavy styles (stipple, noise-grain) are limited
|
|
5038
|
+
// to MAX_CLIP_HEAVY_SHAPES total across the entire render.
|
|
5039
|
+
if ((finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
5040
|
+
finalRenderStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(finalRenderStyle);
|
|
5041
|
+
styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5042
|
+
}
|
|
5043
|
+
if (finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") clipHeavyCount++;
|
|
4853
5044
|
// Consistent light direction — subtle shadow offset
|
|
4854
5045
|
const shadowDist = hasGlow ? 0 : size * 0.02;
|
|
4855
5046
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
@@ -4904,30 +5095,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4904
5095
|
lightAngle: lightAngle,
|
|
4905
5096
|
scaleFactor: scaleFactor
|
|
4906
5097
|
};
|
|
4907
|
-
if (shouldMirror)
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
5098
|
+
if (shouldMirror) {
|
|
5099
|
+
(0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
5100
|
+
...shapeConfig,
|
|
5101
|
+
mirrorAxis: mirrorAxis,
|
|
5102
|
+
mirrorGap: size * (0.1 + rng() * 0.3)
|
|
5103
|
+
});
|
|
5104
|
+
complexityBudget -= styleCost * 2; // mirrored = 2 shapes
|
|
5105
|
+
} else {
|
|
5106
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
|
|
5107
|
+
complexityBudget -= styleCost;
|
|
5108
|
+
}
|
|
5109
|
+
// ── Extras budget gate — skip multiplier sections when over budget ──
|
|
5110
|
+
const extrasAllowed = extrasSpent < budgetForExtras;
|
|
4913
5111
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
4914
5112
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
4915
5113
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
5114
|
+
if (extrasAllowed) {
|
|
5115
|
+
for(let g = 0; g < glazePasses; g++){
|
|
5116
|
+
const glazeScale = 1 - (g + 1) * 0.12;
|
|
5117
|
+
const glazeAlpha = 0.08 + g * 0.04;
|
|
5118
|
+
ctx.globalAlpha = glazeAlpha;
|
|
5119
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
|
|
5120
|
+
fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
|
|
5121
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
5122
|
+
strokeWidth: 0,
|
|
5123
|
+
size: size * glazeScale,
|
|
5124
|
+
rotation: rotation,
|
|
5125
|
+
proportionType: "GOLDEN_RATIO",
|
|
5126
|
+
renderStyle: "fill-only",
|
|
5127
|
+
rng: rng
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
extrasSpent += glazePasses;
|
|
4930
5131
|
}
|
|
5132
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
4931
5133
|
}
|
|
4932
5134
|
shapePositions.push({
|
|
4933
5135
|
x: finalX,
|
|
@@ -4945,37 +5147,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4945
5147
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4946
5148
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
4947
5149
|
const echoAngle = rng() * Math.PI * 2;
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
5150
|
+
if (extrasAllowed) {
|
|
5151
|
+
for(let e = 0; e < echoCount; e++){
|
|
5152
|
+
const echoScale = 0.3 - e * 0.08;
|
|
5153
|
+
const echoDist = size * (0.6 + e * 0.4);
|
|
5154
|
+
const echoX = finalX + Math.cos(echoAngle) * echoDist;
|
|
5155
|
+
const echoY = finalY + Math.sin(echoAngle) * echoDist;
|
|
5156
|
+
const echoSize = size * Math.max(0.1, echoScale);
|
|
5157
|
+
if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
|
|
5158
|
+
ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
|
|
5159
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
|
|
5160
|
+
fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
|
|
5161
|
+
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.4),
|
|
5162
|
+
strokeWidth: strokeWidth * 0.6,
|
|
5163
|
+
size: echoSize,
|
|
5164
|
+
rotation: rotation + (e + 1) * 15,
|
|
5165
|
+
proportionType: "GOLDEN_RATIO",
|
|
5166
|
+
renderStyle: finalRenderStyle,
|
|
5167
|
+
rng: rng
|
|
5168
|
+
});
|
|
5169
|
+
shapePositions.push({
|
|
5170
|
+
x: echoX,
|
|
5171
|
+
y: echoY,
|
|
5172
|
+
size: echoSize,
|
|
5173
|
+
shape: shape
|
|
5174
|
+
});
|
|
5175
|
+
spatialGrid.insert({
|
|
5176
|
+
x: echoX,
|
|
5177
|
+
y: echoY,
|
|
5178
|
+
size: echoSize,
|
|
5179
|
+
shape: shape
|
|
5180
|
+
});
|
|
5181
|
+
}
|
|
5182
|
+
extrasSpent += echoCount * styleCost;
|
|
4978
5183
|
}
|
|
5184
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
4979
5185
|
}
|
|
4980
5186
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
4981
5187
|
// Focal depth: shapes near focal points get more detail
|
|
@@ -4983,7 +5189,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4983
5189
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
4984
5190
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
4985
5191
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
4986
|
-
for(let n = 0; n < innerCount; n++){
|
|
5192
|
+
if (extrasAllowed) for(let n = 0; n < innerCount; n++){
|
|
4987
5193
|
// Pick inner shape from palette affinities
|
|
4988
5194
|
const innerSizeFraction = size * 0.25 / adjustedMaxSize;
|
|
4989
5195
|
const innerShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
|
|
@@ -4992,6 +5198,10 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4992
5198
|
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
4993
5199
|
const innerRot = rng() * 360;
|
|
4994
5200
|
const innerFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
|
|
5201
|
+
let innerStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng);
|
|
5202
|
+
// Apply clip-heavy cap to nested shapes too
|
|
5203
|
+
if ((innerStyle === "stipple" || innerStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) innerStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(innerStyle);
|
|
5204
|
+
if (innerStyle === "stipple" || innerStyle === "noise-grain") clipHeavyCount++;
|
|
4995
5205
|
ctx.globalAlpha = layerOpacity * 0.7;
|
|
4996
5206
|
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
|
|
4997
5207
|
fillColor: innerFill,
|
|
@@ -5000,9 +5210,21 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5000
5210
|
size: innerSize,
|
|
5001
5211
|
rotation: innerRot,
|
|
5002
5212
|
proportionType: "GOLDEN_RATIO",
|
|
5003
|
-
renderStyle:
|
|
5213
|
+
renderStyle: innerStyle,
|
|
5004
5214
|
rng: rng
|
|
5005
5215
|
});
|
|
5216
|
+
extrasSpent += $1f63dc64b5593c73$var$RENDER_STYLE_COST[innerStyle] ?? 1;
|
|
5217
|
+
}
|
|
5218
|
+
else // Drain RNG to keep determinism — each nested shape consumes ~8 rng calls
|
|
5219
|
+
for(let n = 0; n < innerCount; n++){
|
|
5220
|
+
rng();
|
|
5221
|
+
rng();
|
|
5222
|
+
rng();
|
|
5223
|
+
rng();
|
|
5224
|
+
rng();
|
|
5225
|
+
rng();
|
|
5226
|
+
rng();
|
|
5227
|
+
rng();
|
|
5006
5228
|
}
|
|
5007
5229
|
}
|
|
5008
5230
|
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
@@ -5011,40 +5233,55 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5011
5233
|
const constellation = $1f63dc64b5593c73$var$CONSTELLATIONS[Math.floor(rng() * $1f63dc64b5593c73$var$CONSTELLATIONS.length)];
|
|
5012
5234
|
const members = constellation.build(rng, size);
|
|
5013
5235
|
const groupRotation = rng() * Math.PI * 2;
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5236
|
+
if (extrasAllowed) {
|
|
5237
|
+
const cosR = Math.cos(groupRotation);
|
|
5238
|
+
const sinR = Math.sin(groupRotation);
|
|
5239
|
+
for (const member of members){
|
|
5240
|
+
// Rotate the group offset by the group rotation
|
|
5241
|
+
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
5242
|
+
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
5243
|
+
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
5244
|
+
const memberFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
|
|
5245
|
+
const memberStroke = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
|
|
5246
|
+
ctx.globalAlpha = layerOpacity * 0.6;
|
|
5247
|
+
// Use the member's shape if available, otherwise fall back to palette
|
|
5248
|
+
const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
|
|
5249
|
+
let memberStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng);
|
|
5250
|
+
// Apply clip-heavy cap to constellation members too
|
|
5251
|
+
if ((memberStyle === "stipple" || memberStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) memberStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(memberStyle);
|
|
5252
|
+
if (memberStyle === "stipple" || memberStyle === "noise-grain") clipHeavyCount++;
|
|
5253
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
|
|
5254
|
+
fillColor: memberFill,
|
|
5255
|
+
strokeColor: memberStroke,
|
|
5256
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5257
|
+
size: member.size,
|
|
5258
|
+
rotation: member.rotation + groupRotation * 180 / Math.PI,
|
|
5259
|
+
proportionType: "GOLDEN_RATIO",
|
|
5260
|
+
renderStyle: memberStyle,
|
|
5261
|
+
rng: rng
|
|
5262
|
+
});
|
|
5263
|
+
shapePositions.push({
|
|
5264
|
+
x: mx,
|
|
5265
|
+
y: my,
|
|
5266
|
+
size: member.size,
|
|
5267
|
+
shape: memberShape
|
|
5268
|
+
});
|
|
5269
|
+
spatialGrid.insert({
|
|
5270
|
+
x: mx,
|
|
5271
|
+
y: my,
|
|
5272
|
+
size: member.size,
|
|
5273
|
+
shape: memberShape
|
|
5274
|
+
});
|
|
5275
|
+
extrasSpent += $1f63dc64b5593c73$var$RENDER_STYLE_COST[memberStyle] ?? 1;
|
|
5276
|
+
}
|
|
5277
|
+
} else // Drain RNG — each member consumes ~6 rng calls for colors/style
|
|
5278
|
+
for(let m = 0; m < members.length; m++){
|
|
5279
|
+
rng();
|
|
5280
|
+
rng();
|
|
5281
|
+
rng();
|
|
5282
|
+
rng();
|
|
5283
|
+
rng();
|
|
5284
|
+
rng();
|
|
5048
5285
|
}
|
|
5049
5286
|
}
|
|
5050
5287
|
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
@@ -5055,39 +5292,47 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5055
5292
|
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5056
5293
|
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5057
5294
|
const rhythmShape = shape; // same shape for visual rhythm
|
|
5058
|
-
|
|
5295
|
+
if (extrasAllowed) {
|
|
5296
|
+
let rhythmSize = size * 0.6;
|
|
5297
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5298
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5299
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5300
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5301
|
+
if ($1f63dc64b5593c73$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5302
|
+
rhythmSize *= rhythmDecay;
|
|
5303
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5304
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5305
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5306
|
+
const rhythmFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5307
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5308
|
+
fillColor: rhythmFill,
|
|
5309
|
+
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5310
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5311
|
+
size: rhythmSize,
|
|
5312
|
+
rotation: rotation + (r + 1) * 12,
|
|
5313
|
+
proportionType: "GOLDEN_RATIO",
|
|
5314
|
+
renderStyle: finalRenderStyle,
|
|
5315
|
+
rng: rng
|
|
5316
|
+
});
|
|
5317
|
+
shapePositions.push({
|
|
5318
|
+
x: rx,
|
|
5319
|
+
y: ry,
|
|
5320
|
+
size: rhythmSize,
|
|
5321
|
+
shape: rhythmShape
|
|
5322
|
+
});
|
|
5323
|
+
spatialGrid.insert({
|
|
5324
|
+
x: rx,
|
|
5325
|
+
y: ry,
|
|
5326
|
+
size: rhythmSize,
|
|
5327
|
+
shape: rhythmShape
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
extrasSpent += rhythmCount * styleCost;
|
|
5331
|
+
} else // Drain RNG — each rhythm step consumes ~3 rng calls for colors
|
|
5059
5332
|
for(let r = 0; r < rhythmCount; r++){
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
if ($1f63dc64b5593c73$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5064
|
-
rhythmSize *= rhythmDecay;
|
|
5065
|
-
if (rhythmSize < adjustedMinSize) break;
|
|
5066
|
-
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5067
|
-
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5068
|
-
const rhythmFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5069
|
-
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5070
|
-
fillColor: rhythmFill,
|
|
5071
|
-
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5072
|
-
strokeWidth: strokeWidth * 0.7,
|
|
5073
|
-
size: rhythmSize,
|
|
5074
|
-
rotation: rotation + (r + 1) * 12,
|
|
5075
|
-
proportionType: "GOLDEN_RATIO",
|
|
5076
|
-
renderStyle: finalRenderStyle,
|
|
5077
|
-
rng: rng
|
|
5078
|
-
});
|
|
5079
|
-
shapePositions.push({
|
|
5080
|
-
x: rx,
|
|
5081
|
-
y: ry,
|
|
5082
|
-
size: rhythmSize,
|
|
5083
|
-
shape: rhythmShape
|
|
5084
|
-
});
|
|
5085
|
-
spatialGrid.insert({
|
|
5086
|
-
x: rx,
|
|
5087
|
-
y: ry,
|
|
5088
|
-
size: rhythmSize,
|
|
5089
|
-
shape: rhythmShape
|
|
5090
|
-
});
|
|
5333
|
+
rng();
|
|
5334
|
+
rng();
|
|
5335
|
+
rng();
|
|
5091
5336
|
}
|
|
5092
5337
|
}
|
|
5093
5338
|
}
|
|
@@ -5153,14 +5398,26 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5153
5398
|
}
|
|
5154
5399
|
}
|
|
5155
5400
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
5401
|
+
// Optimized: collect all segments into width-quantized buckets, then
|
|
5402
|
+
// render each bucket as a single batched path. This reduces
|
|
5403
|
+
// beginPath/stroke calls from O(segments) to O(buckets).
|
|
5156
5404
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
5157
5405
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
5406
|
+
// Width buckets — 6 buckets cover the taper×pressure range
|
|
5407
|
+
const FLOW_WIDTH_BUCKETS = 6;
|
|
5408
|
+
const flowBuckets = [];
|
|
5409
|
+
for(let b = 0; b < FLOW_WIDTH_BUCKETS; b++)flowBuckets.push([]);
|
|
5410
|
+
// Track the representative width for each bucket
|
|
5411
|
+
const flowBucketWidths = new Array(FLOW_WIDTH_BUCKETS);
|
|
5412
|
+
// Pre-compute max possible width for bucket assignment
|
|
5413
|
+
let globalMaxFlowWidth = 0;
|
|
5158
5414
|
for(let i = 0; i < numFlowLines; i++){
|
|
5159
5415
|
let fx = rng() * width;
|
|
5160
5416
|
let fy = rng() * height;
|
|
5161
5417
|
const steps = 30 + Math.floor(rng() * 40);
|
|
5162
5418
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
5163
5419
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
5420
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
5164
5421
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
5165
5422
|
const lineColorStart = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
5166
5423
|
const lineColorEnd = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
@@ -5182,19 +5439,22 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5182
5439
|
continue;
|
|
5183
5440
|
}
|
|
5184
5441
|
const t = s / steps;
|
|
5185
|
-
// Taper + pressure
|
|
5186
5442
|
const taper = 1 - t * 0.8;
|
|
5187
5443
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
5188
|
-
|
|
5189
|
-
|
|
5444
|
+
const segWidth = startWidth * taper * pressure;
|
|
5445
|
+
const segAlpha = lineAlpha * taper;
|
|
5190
5446
|
const lineColor = t < 0.5 ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $b5a262d09b87e373$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5447
|
+
// Quantize width into bucket
|
|
5448
|
+
const bucketIdx = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(segWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5449
|
+
flowBuckets[bucketIdx].push({
|
|
5450
|
+
x1: prevX,
|
|
5451
|
+
y1: prevY,
|
|
5452
|
+
x2: fx,
|
|
5453
|
+
y2: fy,
|
|
5454
|
+
color: lineColor,
|
|
5455
|
+
alpha: segAlpha
|
|
5456
|
+
});
|
|
5457
|
+
flowBucketWidths[bucketIdx] = segWidth;
|
|
5198
5458
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
5199
5459
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
5200
5460
|
const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
|
|
@@ -5210,12 +5470,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5210
5470
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
5211
5471
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
5212
5472
|
const bTaper = 1 - bs / branchSteps * 0.9;
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5473
|
+
const bSegWidth = branchWidth * bTaper;
|
|
5474
|
+
const bAlpha = lineAlpha * taper * bTaper * 0.6;
|
|
5475
|
+
const bBucket = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(bSegWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5476
|
+
flowBuckets[bBucket].push({
|
|
5477
|
+
x1: bPrevX,
|
|
5478
|
+
y1: bPrevY,
|
|
5479
|
+
x2: bx,
|
|
5480
|
+
y2: by,
|
|
5481
|
+
color: lineColor,
|
|
5482
|
+
alpha: bAlpha
|
|
5483
|
+
});
|
|
5484
|
+
flowBucketWidths[bBucket] = bSegWidth;
|
|
5219
5485
|
bPrevX = bx;
|
|
5220
5486
|
bPrevY = by;
|
|
5221
5487
|
}
|
|
@@ -5224,7 +5490,40 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5224
5490
|
prevY = fy;
|
|
5225
5491
|
}
|
|
5226
5492
|
}
|
|
5493
|
+
// Render flow line buckets — one batched path per width bucket
|
|
5494
|
+
// Within each bucket, further sub-batch by quantized alpha (4 levels)
|
|
5495
|
+
ctx.lineCap = "round";
|
|
5496
|
+
const FLOW_ALPHA_BUCKETS = 4;
|
|
5497
|
+
for(let wb = 0; wb < FLOW_WIDTH_BUCKETS; wb++){
|
|
5498
|
+
const segs = flowBuckets[wb];
|
|
5499
|
+
if (segs.length === 0) continue;
|
|
5500
|
+
ctx.lineWidth = flowBucketWidths[wb];
|
|
5501
|
+
// Sub-bucket by alpha
|
|
5502
|
+
const alphaSubs = [];
|
|
5503
|
+
for(let a = 0; a < FLOW_ALPHA_BUCKETS; a++)alphaSubs.push([]);
|
|
5504
|
+
let maxAlpha = 0;
|
|
5505
|
+
for(let j = 0; j < segs.length; j++)if (segs[j].alpha > maxAlpha) maxAlpha = segs[j].alpha;
|
|
5506
|
+
for(let j = 0; j < segs.length; j++){
|
|
5507
|
+
const ai = Math.min(FLOW_ALPHA_BUCKETS - 1, Math.floor(segs[j].alpha / (maxAlpha || 1) * FLOW_ALPHA_BUCKETS));
|
|
5508
|
+
alphaSubs[ai].push(segs[j]);
|
|
5509
|
+
}
|
|
5510
|
+
for(let ai = 0; ai < FLOW_ALPHA_BUCKETS; ai++){
|
|
5511
|
+
const sub = alphaSubs[ai];
|
|
5512
|
+
if (sub.length === 0) continue;
|
|
5513
|
+
// Use the median segment's alpha and color as representative
|
|
5514
|
+
const rep = sub[Math.floor(sub.length / 2)];
|
|
5515
|
+
ctx.globalAlpha = rep.alpha;
|
|
5516
|
+
ctx.strokeStyle = rep.color;
|
|
5517
|
+
ctx.beginPath();
|
|
5518
|
+
for(let j = 0; j < sub.length; j++){
|
|
5519
|
+
ctx.moveTo(sub[j].x1, sub[j].y1);
|
|
5520
|
+
ctx.lineTo(sub[j].x2, sub[j].y2);
|
|
5521
|
+
}
|
|
5522
|
+
ctx.stroke();
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5227
5525
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
5526
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
5228
5527
|
const energyArchetypes = [
|
|
5229
5528
|
"dense-chaotic",
|
|
5230
5529
|
"cosmic",
|
|
@@ -5235,8 +5534,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5235
5534
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
5236
5535
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
5237
5536
|
ctx.lineCap = "round";
|
|
5537
|
+
// Collect all energy segments with their computed state
|
|
5538
|
+
const ENERGY_ALPHA_BUCKETS = 3;
|
|
5539
|
+
const energyBuckets = [];
|
|
5540
|
+
for(let b = 0; b < ENERGY_ALPHA_BUCKETS; b++)energyBuckets.push([]);
|
|
5541
|
+
const energyAlphas = new Array(ENERGY_ALPHA_BUCKETS).fill(0);
|
|
5238
5542
|
for(let e = 0; e < energyCount; e++){
|
|
5239
|
-
// Pick a random shape to radiate from
|
|
5240
5543
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5241
5544
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
5242
5545
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -5248,15 +5551,38 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5248
5551
|
const sy = source.y + Math.sin(angle) * startDist;
|
|
5249
5552
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
5250
5553
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5554
|
+
const eAlpha = 0.04 + rng() * 0.06;
|
|
5555
|
+
const eColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5556
|
+
const eLw = (0.5 + rng() * 1.5) * scaleFactor;
|
|
5557
|
+
// Quantize alpha into bucket
|
|
5558
|
+
const bi = Math.min(ENERGY_ALPHA_BUCKETS - 1, Math.floor((eAlpha - 0.04) / 0.06 * ENERGY_ALPHA_BUCKETS));
|
|
5559
|
+
energyBuckets[bi].push({
|
|
5560
|
+
x1: sx,
|
|
5561
|
+
y1: sy,
|
|
5562
|
+
x2: ex,
|
|
5563
|
+
y2: ey,
|
|
5564
|
+
color: eColor,
|
|
5565
|
+
lw: eLw
|
|
5566
|
+
});
|
|
5567
|
+
energyAlphas[bi] = eAlpha;
|
|
5258
5568
|
}
|
|
5259
5569
|
}
|
|
5570
|
+
// Render batched energy lines
|
|
5571
|
+
for(let bi = 0; bi < ENERGY_ALPHA_BUCKETS; bi++){
|
|
5572
|
+
const segs = energyBuckets[bi];
|
|
5573
|
+
if (segs.length === 0) continue;
|
|
5574
|
+
ctx.globalAlpha = energyAlphas[bi];
|
|
5575
|
+
// Use median segment's color and width as representative
|
|
5576
|
+
const rep = segs[Math.floor(segs.length / 2)];
|
|
5577
|
+
ctx.strokeStyle = rep.color;
|
|
5578
|
+
ctx.lineWidth = rep.lw;
|
|
5579
|
+
ctx.beginPath();
|
|
5580
|
+
for(let j = 0; j < segs.length; j++){
|
|
5581
|
+
ctx.moveTo(segs[j].x1, segs[j].y1);
|
|
5582
|
+
ctx.lineTo(segs[j].x2, segs[j].y2);
|
|
5583
|
+
}
|
|
5584
|
+
ctx.stroke();
|
|
5585
|
+
}
|
|
5260
5586
|
}
|
|
5261
5587
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
5262
5588
|
if (symmetryMode !== "none") {
|
|
@@ -5279,27 +5605,44 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5279
5605
|
ctx.restore();
|
|
5280
5606
|
}
|
|
5281
5607
|
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
5608
|
+
// Optimized: cap density at large sizes (diminishing returns above ~2K dots),
|
|
5609
|
+
// skip inner pixelScale loop when scale=1, use Uint32Array for faster writes.
|
|
5282
5610
|
const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
|
|
5283
|
-
const
|
|
5611
|
+
const rawNoiseDensity = Math.floor(width * height / 800);
|
|
5612
|
+
// Cap at 2500 dots — beyond this the visual effect is indistinguishable
|
|
5613
|
+
// but getImageData/putImageData cost scales with canvas size
|
|
5614
|
+
const noiseDensity = Math.min(rawNoiseDensity, 2500);
|
|
5284
5615
|
try {
|
|
5285
5616
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5286
5617
|
const data = imageData.data;
|
|
5287
5618
|
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5619
|
+
if (pixelScale === 1) // Fast path — no inner loop, direct pixel write
|
|
5620
|
+
// Pre-compute alpha blend as integer math (avoid float multiply per channel)
|
|
5288
5621
|
for(let i = 0; i < noiseDensity; i++){
|
|
5289
5622
|
const nx = Math.floor(noiseRng() * width);
|
|
5290
5623
|
const ny = Math.floor(noiseRng() * height);
|
|
5291
5624
|
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5292
|
-
|
|
5293
|
-
|
|
5625
|
+
// srcA in range [0.01, 0.04] — multiply by 256 for fixed-point
|
|
5626
|
+
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
5627
|
+
const invA256 = 256 - srcA256;
|
|
5628
|
+
const bSrc = brightness * srcA256; // pre-multiply brightness × alpha
|
|
5629
|
+
const idx = ny * width + nx << 2;
|
|
5630
|
+
data[idx] = data[idx] * invA256 + bSrc >> 8;
|
|
5631
|
+
data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
|
|
5632
|
+
data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
|
|
5633
|
+
}
|
|
5634
|
+
else for(let i = 0; i < noiseDensity; i++){
|
|
5635
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5636
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5637
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5638
|
+
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
5639
|
+
const invA256 = 256 - srcA256;
|
|
5640
|
+
const bSrc = brightness * srcA256;
|
|
5294
5641
|
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5295
|
-
const idx = (
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5300
|
-
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5301
|
-
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5302
|
-
// Keep existing alpha
|
|
5642
|
+
const idx = (ny + dy) * width + (nx + dx) << 2;
|
|
5643
|
+
data[idx] = data[idx] * invA256 + bSrc >> 8;
|
|
5644
|
+
data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
|
|
5645
|
+
data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
|
|
5303
5646
|
}
|
|
5304
5647
|
}
|
|
5305
5648
|
ctx.putImageData(imageData, 0, 0);
|
|
@@ -5329,10 +5672,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5329
5672
|
ctx.fillStyle = vigGrad;
|
|
5330
5673
|
ctx.fillRect(0, 0, width, height);
|
|
5331
5674
|
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5675
|
+
// Optimized: batch all curves into alpha-quantized groups to reduce
|
|
5676
|
+
// beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
|
|
5332
5677
|
if (shapePositions.length > 1) {
|
|
5333
5678
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5334
5679
|
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5335
5680
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5681
|
+
// Collect curves into 3 alpha buckets
|
|
5682
|
+
const CURVE_ALPHA_BUCKETS = 3;
|
|
5683
|
+
const curveBuckets = [];
|
|
5684
|
+
const curveColors = [];
|
|
5685
|
+
const curveAlphas = new Array(CURVE_ALPHA_BUCKETS).fill(0);
|
|
5686
|
+
for(let b = 0; b < CURVE_ALPHA_BUCKETS; b++)curveBuckets.push([]);
|
|
5336
5687
|
for(let i = 0; i < numCurves; i++){
|
|
5337
5688
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
5338
5689
|
const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
|
|
@@ -5349,11 +5700,32 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5349
5700
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5350
5701
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5351
5702
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
5352
|
-
|
|
5353
|
-
|
|
5703
|
+
const curveAlpha = 0.06 + rng() * 0.1;
|
|
5704
|
+
const curveColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5705
|
+
const bi = Math.min(CURVE_ALPHA_BUCKETS - 1, Math.floor((curveAlpha - 0.06) / 0.1 * CURVE_ALPHA_BUCKETS));
|
|
5706
|
+
curveBuckets[bi].push({
|
|
5707
|
+
ax: a.x,
|
|
5708
|
+
ay: a.y,
|
|
5709
|
+
cpx: cpx,
|
|
5710
|
+
cpy: cpy,
|
|
5711
|
+
bx: b.x,
|
|
5712
|
+
by: b.y
|
|
5713
|
+
});
|
|
5714
|
+
curveAlphas[bi] = curveAlpha;
|
|
5715
|
+
if (!curveColors[bi]) curveColors[bi] = curveColor;
|
|
5716
|
+
}
|
|
5717
|
+
// Render batched curves
|
|
5718
|
+
for(let bi = 0; bi < CURVE_ALPHA_BUCKETS; bi++){
|
|
5719
|
+
const curves = curveBuckets[bi];
|
|
5720
|
+
if (curves.length === 0) continue;
|
|
5721
|
+
ctx.globalAlpha = curveAlphas[bi];
|
|
5722
|
+
ctx.strokeStyle = curveColors[bi];
|
|
5354
5723
|
ctx.beginPath();
|
|
5355
|
-
|
|
5356
|
-
|
|
5724
|
+
for(let j = 0; j < curves.length; j++){
|
|
5725
|
+
const c = curves[j];
|
|
5726
|
+
ctx.moveTo(c.ax, c.ay);
|
|
5727
|
+
ctx.quadraticCurveTo(c.cpx, c.cpy, c.bx, c.by);
|
|
5728
|
+
}
|
|
5357
5729
|
ctx.stroke();
|
|
5358
5730
|
}
|
|
5359
5731
|
}
|
|
@@ -5471,11 +5843,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5471
5843
|
}
|
|
5472
5844
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5473
5845
|
// Vine tendrils — organic curving lines along edges
|
|
5846
|
+
// Optimized: batch all tendrils into a single path
|
|
5474
5847
|
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5475
5848
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5476
5849
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5477
5850
|
ctx.lineCap = "round";
|
|
5478
5851
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5852
|
+
ctx.beginPath();
|
|
5853
|
+
const leafPositions = [];
|
|
5479
5854
|
for(let t = 0; t < tendrilCount; t++){
|
|
5480
5855
|
// Start from a random edge point
|
|
5481
5856
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -5493,7 +5868,6 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5493
5868
|
tx = width - borderPad;
|
|
5494
5869
|
ty = borderRng() * height;
|
|
5495
5870
|
}
|
|
5496
|
-
ctx.beginPath();
|
|
5497
5871
|
ctx.moveTo(tx, ty);
|
|
5498
5872
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5499
5873
|
for(let s = 0; s < segs; s++){
|
|
@@ -5507,14 +5881,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5507
5881
|
ty = cpy3;
|
|
5508
5882
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5509
5883
|
}
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5884
|
+
// Collect leaf positions for batch fill
|
|
5885
|
+
if (borderRng() < 0.6) leafPositions.push({
|
|
5886
|
+
x: tx,
|
|
5887
|
+
y: ty,
|
|
5888
|
+
r: borderPad * (0.15 + borderRng() * 0.2)
|
|
5889
|
+
});
|
|
5890
|
+
}
|
|
5891
|
+
ctx.stroke();
|
|
5892
|
+
// Batch all leaf dots into a single fill
|
|
5893
|
+
if (leafPositions.length > 0) {
|
|
5894
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5895
|
+
ctx.beginPath();
|
|
5896
|
+
for (const leaf of leafPositions){
|
|
5897
|
+
ctx.moveTo(leaf.x + leaf.r, leaf.y);
|
|
5898
|
+
ctx.arc(leaf.x, leaf.y, leaf.r, 0, Math.PI * 2);
|
|
5517
5899
|
}
|
|
5900
|
+
ctx.fill();
|
|
5518
5901
|
}
|
|
5519
5902
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5520
5903
|
// Star-studded arcs along edges
|
|
@@ -5529,8 +5912,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5529
5912
|
ctx.beginPath();
|
|
5530
5913
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5531
5914
|
ctx.stroke();
|
|
5532
|
-
// Scatter small stars along the border region
|
|
5915
|
+
// Scatter small stars along the border region — batched into single path
|
|
5533
5916
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5917
|
+
ctx.beginPath();
|
|
5534
5918
|
for(let s = 0; s < starCount; s++){
|
|
5535
5919
|
const edge = Math.floor(borderRng() * 4);
|
|
5536
5920
|
let sx, sy;
|
|
@@ -5549,7 +5933,6 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5549
5933
|
}
|
|
5550
5934
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5551
5935
|
// 4-point star
|
|
5552
|
-
ctx.beginPath();
|
|
5553
5936
|
for(let p = 0; p < 8; p++){
|
|
5554
5937
|
const a = p / 8 * Math.PI * 2;
|
|
5555
5938
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -5559,8 +5942,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5559
5942
|
else ctx.lineTo(px2, py2);
|
|
5560
5943
|
}
|
|
5561
5944
|
ctx.closePath();
|
|
5562
|
-
ctx.fill();
|
|
5563
5945
|
}
|
|
5946
|
+
ctx.fill();
|
|
5564
5947
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5565
5948
|
// Thin single rule — understated elegance
|
|
5566
5949
|
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|