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/module.js
CHANGED
|
@@ -517,13 +517,21 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
|
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
519
|
// ── Standalone color utilities ──────────────────────────────────────
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
520
|
+
// ── Cached hex→RGB parse — avoids repeated parseInt/substring on hot path ──
|
|
521
|
+
const $9d614e7d77fc2947$var$_rgbCache = new Map();
|
|
522
|
+
const $9d614e7d77fc2947$var$_RGB_CACHE_MAX = 512;
|
|
523
|
+
/** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. Cached. */ function $9d614e7d77fc2947$var$hexToRgb(hex) {
|
|
524
|
+
let cached = $9d614e7d77fc2947$var$_rgbCache.get(hex);
|
|
525
|
+
if (cached) return cached;
|
|
526
|
+
const c = hex.charAt(0) === "#" ? hex.substring(1) : hex;
|
|
527
|
+
cached = [
|
|
523
528
|
parseInt(c.substring(0, 2), 16),
|
|
524
529
|
parseInt(c.substring(2, 4), 16),
|
|
525
530
|
parseInt(c.substring(4, 6), 16)
|
|
526
531
|
];
|
|
532
|
+
if ($9d614e7d77fc2947$var$_rgbCache.size >= $9d614e7d77fc2947$var$_RGB_CACHE_MAX) $9d614e7d77fc2947$var$_rgbCache.clear();
|
|
533
|
+
$9d614e7d77fc2947$var$_rgbCache.set(hex, cached);
|
|
534
|
+
return cached;
|
|
527
535
|
}
|
|
528
536
|
/** Format [r, g, b] back to #RRGGBB. */ function $9d614e7d77fc2947$var$rgbToHex(r, g, b) {
|
|
529
537
|
const clamp = (v)=>Math.max(0, Math.min(255, Math.round(v)));
|
|
@@ -580,7 +588,9 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
|
|
|
580
588
|
}
|
|
581
589
|
function $9d614e7d77fc2947$export$f2121afcad3d553f(hex, alpha) {
|
|
582
590
|
const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex);
|
|
583
|
-
|
|
591
|
+
// Quantize alpha to 3 decimal places without toFixed overhead
|
|
592
|
+
const a = Math.round(alpha * 1000) / 1000;
|
|
593
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
584
594
|
}
|
|
585
595
|
function $9d614e7d77fc2947$export$fabac4600b87056(colors, rng) {
|
|
586
596
|
if (colors.length < 3) return {
|
|
@@ -660,12 +670,21 @@ function $9d614e7d77fc2947$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
|
660
670
|
const [h, s, l] = $9d614e7d77fc2947$var$hexToHsl(hex);
|
|
661
671
|
return $9d614e7d77fc2947$var$hslToHex($9d614e7d77fc2947$var$shiftHueToward(h, target, amount), s, l);
|
|
662
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* Compute relative luminance of a hex color (0 = black, 1 = white).
|
|
675
|
+
* Uses the sRGB luminance formula from WCAG. Cached.
|
|
676
|
+
*/ const $9d614e7d77fc2947$var$_lumCache = new Map();
|
|
663
677
|
function $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe(hex) {
|
|
678
|
+
let cached = $9d614e7d77fc2947$var$_lumCache.get(hex);
|
|
679
|
+
if (cached !== undefined) return cached;
|
|
664
680
|
const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex).map((c)=>{
|
|
665
681
|
const s = c / 255;
|
|
666
682
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
667
683
|
});
|
|
668
|
-
|
|
684
|
+
cached = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
685
|
+
if ($9d614e7d77fc2947$var$_lumCache.size >= 512) $9d614e7d77fc2947$var$_lumCache.clear();
|
|
686
|
+
$9d614e7d77fc2947$var$_lumCache.set(hex, cached);
|
|
687
|
+
return cached;
|
|
669
688
|
}
|
|
670
689
|
function $9d614e7d77fc2947$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
|
|
671
690
|
const fgLum = $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe(fgHex);
|
|
@@ -1101,21 +1120,31 @@ const $8bde0a7ee87832b5$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
|
|
|
1101
1120
|
(0, $79312e33271883e9$export$e46c5570db033611)(ctx, size, finalConfig);
|
|
1102
1121
|
const gridSize = 8;
|
|
1103
1122
|
const unit = size / gridSize;
|
|
1123
|
+
const radius = unit / 2;
|
|
1124
|
+
// Pre-compute the 8 star-point angle pairs (cos/sin) — avoids 648 trig calls
|
|
1125
|
+
const starPoints = [];
|
|
1126
|
+
for(let k = 0; k < 8; k++){
|
|
1127
|
+
const angle = Math.PI / 4 * k;
|
|
1128
|
+
const angle2 = angle + Math.PI / 4;
|
|
1129
|
+
starPoints.push({
|
|
1130
|
+
c1: Math.cos(angle) * radius,
|
|
1131
|
+
s1: Math.sin(angle) * radius,
|
|
1132
|
+
c2: Math.cos(angle2) * radius,
|
|
1133
|
+
s2: Math.sin(angle2) * radius
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1104
1136
|
ctx.beginPath();
|
|
1105
1137
|
// Create base grid
|
|
1106
|
-
for(let i = 0; i <= gridSize; i++)
|
|
1138
|
+
for(let i = 0; i <= gridSize; i++){
|
|
1107
1139
|
const x = (i - gridSize / 2) * unit;
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const y2 = y + radius * Math.sin(angle + Math.PI / 4);
|
|
1117
|
-
ctx.moveTo(x1, y1);
|
|
1118
|
-
ctx.lineTo(x2, y2);
|
|
1140
|
+
for(let j = 0; j <= gridSize; j++){
|
|
1141
|
+
const y = (j - gridSize / 2) * unit;
|
|
1142
|
+
// Draw star pattern at each intersection using pre-computed offsets
|
|
1143
|
+
for(let k = 0; k < 8; k++){
|
|
1144
|
+
const sp = starPoints[k];
|
|
1145
|
+
ctx.moveTo(x + sp.c1, y + sp.s1);
|
|
1146
|
+
ctx.lineTo(x + sp.c2, y + sp.s2);
|
|
1147
|
+
}
|
|
1119
1148
|
}
|
|
1120
1149
|
}
|
|
1121
1150
|
ctx.stroke();
|
|
@@ -1435,20 +1464,23 @@ const $d63629e16208c310$export$eeae7765f05012e2 = (ctx, size)=>{
|
|
|
1435
1464
|
const $d63629e16208c310$export$3355220a8108efc3 = (ctx, size)=>{
|
|
1436
1465
|
const outerRadius = size / 2;
|
|
1437
1466
|
const innerRadius = size / 4;
|
|
1438
|
-
|
|
1467
|
+
// Adaptive step count: fewer segments for small shapes where detail isn't visible.
|
|
1468
|
+
// 36×36 = 1296 segments at full size; at size < 60 we drop to 16×16 = 256.
|
|
1469
|
+
const steps = size < 60 ? 16 : size < 150 ? 24 : 36;
|
|
1470
|
+
const TWO_PI = Math.PI * 2;
|
|
1471
|
+
const angleStep = TWO_PI / steps;
|
|
1439
1472
|
ctx.beginPath();
|
|
1440
1473
|
for(let i = 0; i < steps; i++){
|
|
1441
|
-
const angle1 = i
|
|
1442
|
-
|
|
1474
|
+
const angle1 = i * angleStep;
|
|
1475
|
+
const cosA = Math.cos(angle1);
|
|
1476
|
+
const sinA = Math.sin(angle1);
|
|
1443
1477
|
for(let j = 0; j < steps; j++){
|
|
1444
|
-
const phi1 = j
|
|
1445
|
-
const phi2 =
|
|
1446
|
-
const
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
ctx.moveTo(x1, y1);
|
|
1451
|
-
ctx.lineTo(x2, y2);
|
|
1478
|
+
const phi1 = j * angleStep;
|
|
1479
|
+
const phi2 = phi1 + angleStep;
|
|
1480
|
+
const r1 = outerRadius + innerRadius * Math.cos(phi1);
|
|
1481
|
+
const r2 = outerRadius + innerRadius * Math.cos(phi2);
|
|
1482
|
+
ctx.moveTo(r1 * cosA, r1 * sinA);
|
|
1483
|
+
ctx.lineTo(r2 * cosA, r2 * sinA);
|
|
1452
1484
|
}
|
|
1453
1485
|
}
|
|
1454
1486
|
};
|
|
@@ -2011,6 +2043,43 @@ const $9beb8f41637c29fd$var$RENDER_STYLES = [
|
|
|
2011
2043
|
function $9beb8f41637c29fd$export$9fd4e64b2acd410e(rng) {
|
|
2012
2044
|
return $9beb8f41637c29fd$var$RENDER_STYLES[Math.floor(rng() * $9beb8f41637c29fd$var$RENDER_STYLES.length)];
|
|
2013
2045
|
}
|
|
2046
|
+
const $9beb8f41637c29fd$export$2f738f61a8c15e07 = {
|
|
2047
|
+
"fill-and-stroke": 1,
|
|
2048
|
+
"fill-only": 0.5,
|
|
2049
|
+
"stroke-only": 1,
|
|
2050
|
+
"double-stroke": 1.5,
|
|
2051
|
+
"dashed": 1,
|
|
2052
|
+
"watercolor": 7,
|
|
2053
|
+
"hatched": 3,
|
|
2054
|
+
"incomplete": 1,
|
|
2055
|
+
"stipple": 90,
|
|
2056
|
+
"stencil": 2,
|
|
2057
|
+
"noise-grain": 400,
|
|
2058
|
+
"wood-grain": 10,
|
|
2059
|
+
"marble-vein": 4,
|
|
2060
|
+
"fabric-weave": 6,
|
|
2061
|
+
"hand-drawn": 5
|
|
2062
|
+
};
|
|
2063
|
+
function $9beb8f41637c29fd$export$909ab0580e273f19(style) {
|
|
2064
|
+
switch(style){
|
|
2065
|
+
case "noise-grain":
|
|
2066
|
+
return "hatched";
|
|
2067
|
+
case "stipple":
|
|
2068
|
+
return "dashed";
|
|
2069
|
+
case "wood-grain":
|
|
2070
|
+
return "hatched";
|
|
2071
|
+
case "watercolor":
|
|
2072
|
+
return "fill-and-stroke";
|
|
2073
|
+
case "fabric-weave":
|
|
2074
|
+
return "hatched";
|
|
2075
|
+
case "hand-drawn":
|
|
2076
|
+
return "fill-and-stroke";
|
|
2077
|
+
case "marble-vein":
|
|
2078
|
+
return "stroke-only";
|
|
2079
|
+
default:
|
|
2080
|
+
return style;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2014
2083
|
function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
2015
2084
|
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
|
|
2016
2085
|
ctx.save();
|
|
@@ -2130,6 +2199,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2130
2199
|
case "hatched":
|
|
2131
2200
|
{
|
|
2132
2201
|
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
2202
|
+
// Optimized: batch all parallel lines into a single path per pass
|
|
2133
2203
|
const savedAlphaH = ctx.globalAlpha;
|
|
2134
2204
|
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
2135
2205
|
ctx.fill();
|
|
@@ -2141,28 +2211,28 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2141
2211
|
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
2142
2212
|
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
2143
2213
|
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
2144
|
-
// Draw parallel lines across the bounding box
|
|
2214
|
+
// Draw parallel lines across the bounding box — batched into single path
|
|
2145
2215
|
const extent = size * 0.8;
|
|
2146
2216
|
const cos = Math.cos(hatchAngle);
|
|
2147
2217
|
const sin = Math.sin(hatchAngle);
|
|
2218
|
+
ctx.beginPath();
|
|
2148
2219
|
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
2149
|
-
ctx.beginPath();
|
|
2150
2220
|
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
2151
2221
|
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
2152
|
-
ctx.stroke();
|
|
2153
2222
|
}
|
|
2223
|
+
ctx.stroke();
|
|
2154
2224
|
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
2155
2225
|
if (!rng || rng() < 0.5) {
|
|
2156
2226
|
const crossAngle = hatchAngle + Math.PI / 2;
|
|
2157
2227
|
const cos2 = Math.cos(crossAngle);
|
|
2158
2228
|
const sin2 = Math.sin(crossAngle);
|
|
2159
2229
|
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
2230
|
+
ctx.beginPath();
|
|
2160
2231
|
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
2161
|
-
ctx.beginPath();
|
|
2162
2232
|
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
2163
2233
|
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
2164
|
-
ctx.stroke();
|
|
2165
2234
|
}
|
|
2235
|
+
ctx.stroke();
|
|
2166
2236
|
}
|
|
2167
2237
|
ctx.restore();
|
|
2168
2238
|
ctx.globalAlpha = savedAlphaH;
|
|
@@ -2200,6 +2270,8 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2200
2270
|
case "stipple":
|
|
2201
2271
|
{
|
|
2202
2272
|
// Dot-fill texture — clip to shape, then scatter dots
|
|
2273
|
+
// Optimized: use fillRect instead of arc for dots (much cheaper to render),
|
|
2274
|
+
// and cap total dot count to avoid O(size²) blowup on large shapes.
|
|
2203
2275
|
const savedAlphaS = ctx.globalAlpha;
|
|
2204
2276
|
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
2205
2277
|
ctx.fill(); // ghost fill
|
|
@@ -2207,16 +2279,20 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2207
2279
|
ctx.save();
|
|
2208
2280
|
ctx.clip();
|
|
2209
2281
|
const dotSpacing = Math.max(2, size * 0.03);
|
|
2210
|
-
const
|
|
2282
|
+
const extentS = size * 0.55;
|
|
2283
|
+
// Cap total dots: beyond ~900 (30×30 grid) the visual density plateaus
|
|
2284
|
+
const maxDotsPerAxis = Math.min(Math.ceil(extentS * 2 / dotSpacing), 30);
|
|
2285
|
+
const actualSpacing = extentS * 2 / maxDotsPerAxis;
|
|
2211
2286
|
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
2212
|
-
for(let
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2287
|
+
for(let xi = 0; xi < maxDotsPerAxis; xi++){
|
|
2288
|
+
const dx = -extentS + xi * actualSpacing;
|
|
2289
|
+
for(let yi = 0; yi < maxDotsPerAxis; yi++){
|
|
2290
|
+
const dy = -extentS + yi * actualSpacing;
|
|
2291
|
+
const jx = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2292
|
+
const jy = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2293
|
+
const dotD = rng ? actualSpacing * (0.3 + rng() * 0.4) : actualSpacing * 0.4;
|
|
2294
|
+
ctx.fillRect(dx + jx - dotD * 0.5, dy + jy - dotD * 0.5, dotD, dotD);
|
|
2295
|
+
}
|
|
2220
2296
|
}
|
|
2221
2297
|
ctx.restore();
|
|
2222
2298
|
ctx.globalAlpha = savedAlphaS;
|
|
@@ -2249,6 +2325,9 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2249
2325
|
case "noise-grain":
|
|
2250
2326
|
{
|
|
2251
2327
|
// Procedural noise grain texture clipped to shape boundary
|
|
2328
|
+
// Optimized: cap grid to max 40×40 = 1600 dots (was unbounded at O(size²)),
|
|
2329
|
+
// quantize alpha into buckets to minimize globalAlpha state changes,
|
|
2330
|
+
// and batch dots by brightness (black/white) × alpha bucket
|
|
2252
2331
|
const savedAlphaN = ctx.globalAlpha;
|
|
2253
2332
|
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
2254
2333
|
ctx.fill(); // base tint
|
|
@@ -2257,17 +2336,47 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2257
2336
|
ctx.clip();
|
|
2258
2337
|
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
2259
2338
|
const extentN = size * 0.55;
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
const
|
|
2264
|
-
const
|
|
2265
|
-
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2339
|
+
if (rng) {
|
|
2340
|
+
// Cap grid to max 40 dots per axis — beyond this the grain is
|
|
2341
|
+
// visually indistinguishable but cost scales quadratically.
|
|
2342
|
+
const maxGrainPerAxis = Math.min(Math.ceil(extentN * 2 / grainSpacing), 40);
|
|
2343
|
+
const actualGrainSpacing = extentN * 2 / maxGrainPerAxis;
|
|
2344
|
+
// 4 alpha buckets: 0.2, 0.3, 0.4, 0.5 — covers the 0.15-0.50 range
|
|
2345
|
+
const BUCKETS = 4;
|
|
2346
|
+
const bucketMin = 0.15;
|
|
2347
|
+
const bucketRange = 0.35;
|
|
2348
|
+
// [black_bucket0, black_bucket1, ..., white_bucket0, ...]
|
|
2349
|
+
const buckets = [];
|
|
2350
|
+
for(let i = 0; i < BUCKETS * 2; i++)buckets.push([]);
|
|
2351
|
+
for(let xi = 0; xi < maxGrainPerAxis; xi++){
|
|
2352
|
+
const gx = -extentN + xi * actualGrainSpacing;
|
|
2353
|
+
for(let yi = 0; yi < maxGrainPerAxis; yi++){
|
|
2354
|
+
const gy = -extentN + yi * actualGrainSpacing;
|
|
2355
|
+
const jx = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2356
|
+
const jy = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2357
|
+
const isWhite = rng() > 0.5;
|
|
2358
|
+
const dotAlpha = bucketMin + rng() * bucketRange;
|
|
2359
|
+
const dotSize = actualGrainSpacing * (0.3 + rng() * 0.5);
|
|
2360
|
+
const bucketIdx = Math.min(BUCKETS - 1, Math.floor((dotAlpha - bucketMin) / bucketRange * BUCKETS));
|
|
2361
|
+
const offset = isWhite ? BUCKETS : 0;
|
|
2362
|
+
buckets[offset + bucketIdx].push({
|
|
2363
|
+
x: gx + jx,
|
|
2364
|
+
y: gy + jy,
|
|
2365
|
+
s: dotSize
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
// Render each bucket: 2 colors × 4 alpha levels = 8 state changes total
|
|
2370
|
+
for(let color = 0; color < 2; color++){
|
|
2371
|
+
ctx.fillStyle = color === 0 ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
2372
|
+
for(let b = 0; b < BUCKETS; b++){
|
|
2373
|
+
const dots = buckets[color * BUCKETS + b];
|
|
2374
|
+
if (dots.length === 0) continue;
|
|
2375
|
+
const alpha = bucketMin + (b + 0.5) / BUCKETS * bucketRange;
|
|
2376
|
+
ctx.globalAlpha = savedAlphaN * alpha;
|
|
2377
|
+
for(let i = 0; i < dots.length; i++)ctx.fillRect(dots[i].x, dots[i].y, dots[i].s, dots[i].s);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2271
2380
|
}
|
|
2272
2381
|
ctx.restore();
|
|
2273
2382
|
ctx.fillStyle = fillColor;
|
|
@@ -2280,6 +2389,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2280
2389
|
case "wood-grain":
|
|
2281
2390
|
{
|
|
2282
2391
|
// Parallel wavy lines simulating wood grain, clipped to shape
|
|
2392
|
+
// Optimized: batch all grain lines into a single path, increased step from 2 to 4
|
|
2283
2393
|
const savedAlphaW = ctx.globalAlpha;
|
|
2284
2394
|
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
2285
2395
|
ctx.fill(); // base tint
|
|
@@ -2295,17 +2405,19 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2295
2405
|
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
2296
2406
|
const cosG = Math.cos(grainAngle);
|
|
2297
2407
|
const sinG = Math.sin(grainAngle);
|
|
2408
|
+
const waveCoeff = waveFreq * Math.PI;
|
|
2409
|
+
const invExtentW = 1 / extentW;
|
|
2410
|
+
// Batch all grain lines into a single path
|
|
2411
|
+
ctx.beginPath();
|
|
2298
2412
|
for(let d = -extentW; d <= extentW; d += grainLineSpacing){
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
if (t === -extentW) ctx.moveTo(px, py);
|
|
2305
|
-
else ctx.lineTo(px, py);
|
|
2413
|
+
const firstWave = Math.sin(-extentW * invExtentW * waveCoeff) * waveAmp;
|
|
2414
|
+
ctx.moveTo(-extentW * cosG - (d + firstWave) * sinG, -extentW * sinG + (d + firstWave) * cosG);
|
|
2415
|
+
for(let t = -extentW + 4; t <= extentW; t += 4){
|
|
2416
|
+
const wave = Math.sin(t * invExtentW * waveCoeff) * waveAmp;
|
|
2417
|
+
ctx.lineTo(t * cosG - (d + wave) * sinG, t * sinG + (d + wave) * cosG);
|
|
2306
2418
|
}
|
|
2307
|
-
ctx.stroke();
|
|
2308
2419
|
}
|
|
2420
|
+
ctx.stroke();
|
|
2309
2421
|
ctx.restore();
|
|
2310
2422
|
ctx.globalAlpha = savedAlphaW;
|
|
2311
2423
|
ctx.globalAlpha *= 0.35;
|
|
@@ -2367,6 +2479,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2367
2479
|
case "fabric-weave":
|
|
2368
2480
|
{
|
|
2369
2481
|
// Interlocking horizontal/vertical threads clipped to shape
|
|
2482
|
+
// Optimized: batch all horizontal threads into one path, all vertical into another
|
|
2370
2483
|
const savedAlphaF = ctx.globalAlpha;
|
|
2371
2484
|
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
2372
2485
|
ctx.fill(); // ghost base
|
|
@@ -2376,26 +2489,24 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2376
2489
|
const threadSpacing = Math.max(2, size * 0.04);
|
|
2377
2490
|
const extentF = size * 0.55;
|
|
2378
2491
|
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
2492
|
+
// Horizontal threads — batched
|
|
2379
2493
|
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
2380
|
-
|
|
2494
|
+
ctx.beginPath();
|
|
2381
2495
|
for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2382
|
-
ctx.beginPath();
|
|
2383
2496
|
ctx.moveTo(-extentF, y);
|
|
2384
2497
|
ctx.lineTo(extentF, y);
|
|
2385
|
-
ctx.stroke();
|
|
2386
2498
|
}
|
|
2387
|
-
|
|
2499
|
+
ctx.stroke();
|
|
2500
|
+
// Vertical threads (offset by half spacing for weave effect) — batched
|
|
2388
2501
|
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
2389
2502
|
ctx.strokeStyle = fillColor;
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
ctx.lineTo(x, y + threadSpacing);
|
|
2396
|
-
}
|
|
2397
|
-
ctx.stroke();
|
|
2503
|
+
ctx.beginPath();
|
|
2504
|
+
for(let x = -extentF; x <= extentF; x += threadSpacing * 2)for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2505
|
+
// Over-under: draw segment, skip segment
|
|
2506
|
+
ctx.moveTo(x, y);
|
|
2507
|
+
ctx.lineTo(x, y + threadSpacing);
|
|
2398
2508
|
}
|
|
2509
|
+
ctx.stroke();
|
|
2399
2510
|
ctx.strokeStyle = strokeColor;
|
|
2400
2511
|
ctx.restore();
|
|
2401
2512
|
ctx.globalAlpha = savedAlphaF;
|
|
@@ -2461,14 +2572,17 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2461
2572
|
ctx.translate(x, y);
|
|
2462
2573
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2463
2574
|
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2464
|
-
|
|
2575
|
+
// Skip shadow entirely for small shapes (< 20px) — the blur is expensive
|
|
2576
|
+
// and visually imperceptible at that scale.
|
|
2577
|
+
const useShadow = size >= 20;
|
|
2578
|
+
if (useShadow && lightAngle !== undefined) {
|
|
2465
2579
|
const shadowDist = size * 0.035;
|
|
2466
2580
|
const shadowBlurR = size * 0.06;
|
|
2467
2581
|
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2468
2582
|
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2469
2583
|
ctx.shadowBlur = shadowBlurR;
|
|
2470
2584
|
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2471
|
-
} else if (glowRadius > 0) {
|
|
2585
|
+
} else if (useShadow && glowRadius > 0) {
|
|
2472
2586
|
// Glow / shadow effect (legacy path)
|
|
2473
2587
|
ctx.shadowBlur = glowRadius;
|
|
2474
2588
|
ctx.shadowColor = glowColor || fillColor;
|
|
@@ -2492,30 +2606,27 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2492
2606
|
$9beb8f41637c29fd$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2493
2607
|
}
|
|
2494
2608
|
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2609
|
+
// Only reset if we actually set shadow (avoids unnecessary state changes)
|
|
2610
|
+
if (useShadow && (lightAngle !== undefined || glowRadius > 0)) {
|
|
2611
|
+
ctx.shadowBlur = 0;
|
|
2612
|
+
ctx.shadowOffsetX = 0;
|
|
2613
|
+
ctx.shadowOffsetY = 0;
|
|
2614
|
+
ctx.shadowColor = "transparent";
|
|
2615
|
+
}
|
|
2499
2616
|
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2500
|
-
|
|
2617
|
+
// Skip for small shapes (< 30px) — gradient creation + composite op
|
|
2618
|
+
// switch is expensive and the highlight is invisible at small sizes.
|
|
2619
|
+
if (lightAngle !== undefined && size > 30 && rng) {
|
|
2501
2620
|
const hlRadius = size * 0.35;
|
|
2502
2621
|
const hlDist = size * 0.15;
|
|
2503
2622
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2504
2623
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2505
2624
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2506
|
-
//
|
|
2507
|
-
//
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2512
|
-
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2513
|
-
// Blend toward white but keep a hint of the fill's warmth
|
|
2514
|
-
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2515
|
-
}
|
|
2516
|
-
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2517
|
-
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2518
|
-
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2625
|
+
// Use a simple white highlight — the per-shape hex parse was expensive
|
|
2626
|
+
// and the visual difference from tinted highlights is negligible.
|
|
2627
|
+
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2628
|
+
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2629
|
+
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
2519
2630
|
const savedOp = ctx.globalCompositeOperation;
|
|
2520
2631
|
ctx.globalCompositeOperation = "soft-light";
|
|
2521
2632
|
ctx.fillStyle = hlGrad;
|
|
@@ -4057,6 +4168,46 @@ function $3faa2521b78398cf$export$f1142fd7da4d6590(rng) {
|
|
|
4057
4168
|
}
|
|
4058
4169
|
|
|
4059
4170
|
|
|
4171
|
+
// ── Render style cost weights (normalized: fill-and-stroke = 1) ─────
|
|
4172
|
+
// Based on benchmark measurements. Used by the complexity budget to
|
|
4173
|
+
// cap total rendering work and downgrade expensive styles when needed.
|
|
4174
|
+
const $b623126c6e9cbb71$var$RENDER_STYLE_COST = {
|
|
4175
|
+
"fill-and-stroke": 1,
|
|
4176
|
+
"fill-only": 0.5,
|
|
4177
|
+
"stroke-only": 1,
|
|
4178
|
+
"double-stroke": 1.5,
|
|
4179
|
+
"dashed": 1,
|
|
4180
|
+
"watercolor": 7,
|
|
4181
|
+
"hatched": 3,
|
|
4182
|
+
"incomplete": 1,
|
|
4183
|
+
"stipple": 90,
|
|
4184
|
+
"stencil": 2,
|
|
4185
|
+
"noise-grain": 400,
|
|
4186
|
+
"wood-grain": 10,
|
|
4187
|
+
"marble-vein": 4,
|
|
4188
|
+
"fabric-weave": 6,
|
|
4189
|
+
"hand-drawn": 5
|
|
4190
|
+
};
|
|
4191
|
+
function $b623126c6e9cbb71$var$downgradeRenderStyle(style) {
|
|
4192
|
+
switch(style){
|
|
4193
|
+
case "noise-grain":
|
|
4194
|
+
return "hatched";
|
|
4195
|
+
case "stipple":
|
|
4196
|
+
return "dashed";
|
|
4197
|
+
case "wood-grain":
|
|
4198
|
+
return "hatched";
|
|
4199
|
+
case "watercolor":
|
|
4200
|
+
return "fill-and-stroke";
|
|
4201
|
+
case "fabric-weave":
|
|
4202
|
+
return "hatched";
|
|
4203
|
+
case "hand-drawn":
|
|
4204
|
+
return "fill-and-stroke";
|
|
4205
|
+
case "marble-vein":
|
|
4206
|
+
return "stroke-only";
|
|
4207
|
+
default:
|
|
4208
|
+
return style;
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4060
4211
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
4061
4212
|
const $b623126c6e9cbb71$var$SACRED_SHAPES = [
|
|
4062
4213
|
"mandala",
|
|
@@ -4668,19 +4819,20 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4668
4819
|
ctx.beginPath();
|
|
4669
4820
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
4670
4821
|
ctx.stroke();
|
|
4671
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
4822
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
4672
4823
|
if (rng() < 0.5) {
|
|
4673
4824
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
4674
4825
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
4675
4826
|
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
4827
|
+
ctx.beginPath();
|
|
4676
4828
|
for(let d = 0; d < dotCount; d++){
|
|
4677
4829
|
const angle = rng() * Math.PI * 2;
|
|
4678
4830
|
const dist = rng() * zone.radius * 0.7;
|
|
4679
4831
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
4680
|
-
ctx.
|
|
4832
|
+
ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
|
|
4681
4833
|
ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
|
|
4682
|
-
ctx.fill();
|
|
4683
4834
|
}
|
|
4835
|
+
ctx.fill();
|
|
4684
4836
|
}
|
|
4685
4837
|
// ~30% chance: thin concentric ring inside
|
|
4686
4838
|
if (rng() < 0.3) {
|
|
@@ -4772,6 +4924,26 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4772
4924
|
}
|
|
4773
4925
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4774
4926
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4927
|
+
// ── Complexity budget — caps total rendering work ──────────────
|
|
4928
|
+
// Budget scales with pixel area so larger canvases get proportionally
|
|
4929
|
+
// more headroom. The multiplier extras (glazing, echoes, nesting,
|
|
4930
|
+
// constellations, rhythm) are gated behind the budget; when it runs
|
|
4931
|
+
// low they are skipped. When it's exhausted, expensive render styles
|
|
4932
|
+
// are downgraded to cheaper alternatives.
|
|
4933
|
+
//
|
|
4934
|
+
// RNG values are always consumed even when skipping, so the
|
|
4935
|
+
// deterministic sequence for shapes that *do* render is preserved.
|
|
4936
|
+
const pixelArea = width * height;
|
|
4937
|
+
const BUDGET_PER_MEGAPIXEL = 6000; // cost units per 1M pixels
|
|
4938
|
+
let complexityBudget = pixelArea / 1000000 * BUDGET_PER_MEGAPIXEL;
|
|
4939
|
+
const totalBudget = complexityBudget;
|
|
4940
|
+
const budgetForExtras = complexityBudget * 0.25; // reserve 25% for multiplier extras
|
|
4941
|
+
let extrasSpent = 0;
|
|
4942
|
+
// Hard cap on clip-heavy render styles (stipple, noise-grain).
|
|
4943
|
+
// These generate O(size²) fillRect calls per shape and dominate
|
|
4944
|
+
// worst-case render time. Cap scales with pixel area.
|
|
4945
|
+
const MAX_CLIP_HEAVY_SHAPES = Math.max(4, Math.floor(8 * (pixelArea / 1000000)));
|
|
4946
|
+
let clipHeavyCount = 0;
|
|
4775
4947
|
for(let layer = 0; layer < layers; layer++){
|
|
4776
4948
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
4777
4949
|
const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
|
|
@@ -4860,7 +5032,26 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4860
5032
|
const shapeRenderStyle = (0, $24064302523652b1$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
|
|
4861
5033
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
4862
5034
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
4863
|
-
|
|
5035
|
+
let finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
|
|
5036
|
+
// Budget check: downgrade expensive styles proportionally —
|
|
5037
|
+
// the more expensive the style, the earlier it gets downgraded.
|
|
5038
|
+
// noise-grain (400) downgrades when budget < 20% remaining,
|
|
5039
|
+
// stipple (90) when < 82%, wood-grain (10) when < 98%.
|
|
5040
|
+
let styleCost = $b623126c6e9cbb71$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5041
|
+
if (styleCost > 3) {
|
|
5042
|
+
const downgradeThreshold = Math.min(0.85, styleCost / 500);
|
|
5043
|
+
if (complexityBudget < totalBudget * (1 - downgradeThreshold)) {
|
|
5044
|
+
finalRenderStyle = $b623126c6e9cbb71$var$downgradeRenderStyle(finalRenderStyle);
|
|
5045
|
+
styleCost = $b623126c6e9cbb71$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
// Hard cap: clip-heavy styles (stipple, noise-grain) are limited
|
|
5049
|
+
// to MAX_CLIP_HEAVY_SHAPES total across the entire render.
|
|
5050
|
+
if ((finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
5051
|
+
finalRenderStyle = $b623126c6e9cbb71$var$downgradeRenderStyle(finalRenderStyle);
|
|
5052
|
+
styleCost = $b623126c6e9cbb71$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5053
|
+
}
|
|
5054
|
+
if (finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") clipHeavyCount++;
|
|
4864
5055
|
// Consistent light direction — subtle shadow offset
|
|
4865
5056
|
const shadowDist = hasGlow ? 0 : size * 0.02;
|
|
4866
5057
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
@@ -4915,30 +5106,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4915
5106
|
lightAngle: lightAngle,
|
|
4916
5107
|
scaleFactor: scaleFactor
|
|
4917
5108
|
};
|
|
4918
|
-
if (shouldMirror)
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
5109
|
+
if (shouldMirror) {
|
|
5110
|
+
(0, $9beb8f41637c29fd$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
5111
|
+
...shapeConfig,
|
|
5112
|
+
mirrorAxis: mirrorAxis,
|
|
5113
|
+
mirrorGap: size * (0.1 + rng() * 0.3)
|
|
5114
|
+
});
|
|
5115
|
+
complexityBudget -= styleCost * 2; // mirrored = 2 shapes
|
|
5116
|
+
} else {
|
|
5117
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
|
|
5118
|
+
complexityBudget -= styleCost;
|
|
5119
|
+
}
|
|
5120
|
+
// ── Extras budget gate — skip multiplier sections when over budget ──
|
|
5121
|
+
const extrasAllowed = extrasSpent < budgetForExtras;
|
|
4924
5122
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
4925
5123
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
4926
5124
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
5125
|
+
if (extrasAllowed) {
|
|
5126
|
+
for(let g = 0; g < glazePasses; g++){
|
|
5127
|
+
const glazeScale = 1 - (g + 1) * 0.12;
|
|
5128
|
+
const glazeAlpha = 0.08 + g * 0.04;
|
|
5129
|
+
ctx.globalAlpha = glazeAlpha;
|
|
5130
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
|
|
5131
|
+
fillColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
|
|
5132
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
5133
|
+
strokeWidth: 0,
|
|
5134
|
+
size: size * glazeScale,
|
|
5135
|
+
rotation: rotation,
|
|
5136
|
+
proportionType: "GOLDEN_RATIO",
|
|
5137
|
+
renderStyle: "fill-only",
|
|
5138
|
+
rng: rng
|
|
5139
|
+
});
|
|
5140
|
+
}
|
|
5141
|
+
extrasSpent += glazePasses;
|
|
4941
5142
|
}
|
|
5143
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
4942
5144
|
}
|
|
4943
5145
|
shapePositions.push({
|
|
4944
5146
|
x: finalX,
|
|
@@ -4956,37 +5158,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4956
5158
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4957
5159
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
4958
5160
|
const echoAngle = rng() * Math.PI * 2;
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
5161
|
+
if (extrasAllowed) {
|
|
5162
|
+
for(let e = 0; e < echoCount; e++){
|
|
5163
|
+
const echoScale = 0.3 - e * 0.08;
|
|
5164
|
+
const echoDist = size * (0.6 + e * 0.4);
|
|
5165
|
+
const echoX = finalX + Math.cos(echoAngle) * echoDist;
|
|
5166
|
+
const echoY = finalY + Math.sin(echoAngle) * echoDist;
|
|
5167
|
+
const echoSize = size * Math.max(0.1, echoScale);
|
|
5168
|
+
if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
|
|
5169
|
+
ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
|
|
5170
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
|
|
5171
|
+
fillColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
|
|
5172
|
+
strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.4),
|
|
5173
|
+
strokeWidth: strokeWidth * 0.6,
|
|
5174
|
+
size: echoSize,
|
|
5175
|
+
rotation: rotation + (e + 1) * 15,
|
|
5176
|
+
proportionType: "GOLDEN_RATIO",
|
|
5177
|
+
renderStyle: finalRenderStyle,
|
|
5178
|
+
rng: rng
|
|
5179
|
+
});
|
|
5180
|
+
shapePositions.push({
|
|
5181
|
+
x: echoX,
|
|
5182
|
+
y: echoY,
|
|
5183
|
+
size: echoSize,
|
|
5184
|
+
shape: shape
|
|
5185
|
+
});
|
|
5186
|
+
spatialGrid.insert({
|
|
5187
|
+
x: echoX,
|
|
5188
|
+
y: echoY,
|
|
5189
|
+
size: echoSize,
|
|
5190
|
+
shape: shape
|
|
5191
|
+
});
|
|
5192
|
+
}
|
|
5193
|
+
extrasSpent += echoCount * styleCost;
|
|
4989
5194
|
}
|
|
5195
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
4990
5196
|
}
|
|
4991
5197
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
4992
5198
|
// Focal depth: shapes near focal points get more detail
|
|
@@ -4994,7 +5200,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4994
5200
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
4995
5201
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
4996
5202
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
4997
|
-
for(let n = 0; n < innerCount; n++){
|
|
5203
|
+
if (extrasAllowed) for(let n = 0; n < innerCount; n++){
|
|
4998
5204
|
// Pick inner shape from palette affinities
|
|
4999
5205
|
const innerSizeFraction = size * 0.25 / adjustedMaxSize;
|
|
5000
5206
|
const innerShape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
|
|
@@ -5003,6 +5209,10 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5003
5209
|
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
5004
5210
|
const innerRot = rng() * 360;
|
|
5005
5211
|
const innerFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
|
|
5212
|
+
let innerStyle = (0, $24064302523652b1$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng);
|
|
5213
|
+
// Apply clip-heavy cap to nested shapes too
|
|
5214
|
+
if ((innerStyle === "stipple" || innerStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) innerStyle = $b623126c6e9cbb71$var$downgradeRenderStyle(innerStyle);
|
|
5215
|
+
if (innerStyle === "stipple" || innerStyle === "noise-grain") clipHeavyCount++;
|
|
5006
5216
|
ctx.globalAlpha = layerOpacity * 0.7;
|
|
5007
5217
|
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
|
|
5008
5218
|
fillColor: innerFill,
|
|
@@ -5011,9 +5221,21 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5011
5221
|
size: innerSize,
|
|
5012
5222
|
rotation: innerRot,
|
|
5013
5223
|
proportionType: "GOLDEN_RATIO",
|
|
5014
|
-
renderStyle:
|
|
5224
|
+
renderStyle: innerStyle,
|
|
5015
5225
|
rng: rng
|
|
5016
5226
|
});
|
|
5227
|
+
extrasSpent += $b623126c6e9cbb71$var$RENDER_STYLE_COST[innerStyle] ?? 1;
|
|
5228
|
+
}
|
|
5229
|
+
else // Drain RNG to keep determinism — each nested shape consumes ~8 rng calls
|
|
5230
|
+
for(let n = 0; n < innerCount; n++){
|
|
5231
|
+
rng();
|
|
5232
|
+
rng();
|
|
5233
|
+
rng();
|
|
5234
|
+
rng();
|
|
5235
|
+
rng();
|
|
5236
|
+
rng();
|
|
5237
|
+
rng();
|
|
5238
|
+
rng();
|
|
5017
5239
|
}
|
|
5018
5240
|
}
|
|
5019
5241
|
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
@@ -5022,40 +5244,55 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5022
5244
|
const constellation = $b623126c6e9cbb71$var$CONSTELLATIONS[Math.floor(rng() * $b623126c6e9cbb71$var$CONSTELLATIONS.length)];
|
|
5023
5245
|
const members = constellation.build(rng, size);
|
|
5024
5246
|
const groupRotation = rng() * Math.PI * 2;
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5247
|
+
if (extrasAllowed) {
|
|
5248
|
+
const cosR = Math.cos(groupRotation);
|
|
5249
|
+
const sinR = Math.sin(groupRotation);
|
|
5250
|
+
for (const member of members){
|
|
5251
|
+
// Rotate the group offset by the group rotation
|
|
5252
|
+
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
5253
|
+
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
5254
|
+
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
5255
|
+
const memberFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
|
|
5256
|
+
const memberStroke = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
|
|
5257
|
+
ctx.globalAlpha = layerOpacity * 0.6;
|
|
5258
|
+
// Use the member's shape if available, otherwise fall back to palette
|
|
5259
|
+
const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
|
|
5260
|
+
let memberStyle = (0, $24064302523652b1$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng);
|
|
5261
|
+
// Apply clip-heavy cap to constellation members too
|
|
5262
|
+
if ((memberStyle === "stipple" || memberStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) memberStyle = $b623126c6e9cbb71$var$downgradeRenderStyle(memberStyle);
|
|
5263
|
+
if (memberStyle === "stipple" || memberStyle === "noise-grain") clipHeavyCount++;
|
|
5264
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
|
|
5265
|
+
fillColor: memberFill,
|
|
5266
|
+
strokeColor: memberStroke,
|
|
5267
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5268
|
+
size: member.size,
|
|
5269
|
+
rotation: member.rotation + groupRotation * 180 / Math.PI,
|
|
5270
|
+
proportionType: "GOLDEN_RATIO",
|
|
5271
|
+
renderStyle: memberStyle,
|
|
5272
|
+
rng: rng
|
|
5273
|
+
});
|
|
5274
|
+
shapePositions.push({
|
|
5275
|
+
x: mx,
|
|
5276
|
+
y: my,
|
|
5277
|
+
size: member.size,
|
|
5278
|
+
shape: memberShape
|
|
5279
|
+
});
|
|
5280
|
+
spatialGrid.insert({
|
|
5281
|
+
x: mx,
|
|
5282
|
+
y: my,
|
|
5283
|
+
size: member.size,
|
|
5284
|
+
shape: memberShape
|
|
5285
|
+
});
|
|
5286
|
+
extrasSpent += $b623126c6e9cbb71$var$RENDER_STYLE_COST[memberStyle] ?? 1;
|
|
5287
|
+
}
|
|
5288
|
+
} else // Drain RNG — each member consumes ~6 rng calls for colors/style
|
|
5289
|
+
for(let m = 0; m < members.length; m++){
|
|
5290
|
+
rng();
|
|
5291
|
+
rng();
|
|
5292
|
+
rng();
|
|
5293
|
+
rng();
|
|
5294
|
+
rng();
|
|
5295
|
+
rng();
|
|
5059
5296
|
}
|
|
5060
5297
|
}
|
|
5061
5298
|
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
@@ -5066,39 +5303,47 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5066
5303
|
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5067
5304
|
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5068
5305
|
const rhythmShape = shape; // same shape for visual rhythm
|
|
5069
|
-
|
|
5306
|
+
if (extrasAllowed) {
|
|
5307
|
+
let rhythmSize = size * 0.6;
|
|
5308
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5309
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5310
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5311
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5312
|
+
if ($b623126c6e9cbb71$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5313
|
+
rhythmSize *= rhythmDecay;
|
|
5314
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5315
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5316
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5317
|
+
const rhythmFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5318
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5319
|
+
fillColor: rhythmFill,
|
|
5320
|
+
strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5321
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5322
|
+
size: rhythmSize,
|
|
5323
|
+
rotation: rotation + (r + 1) * 12,
|
|
5324
|
+
proportionType: "GOLDEN_RATIO",
|
|
5325
|
+
renderStyle: finalRenderStyle,
|
|
5326
|
+
rng: rng
|
|
5327
|
+
});
|
|
5328
|
+
shapePositions.push({
|
|
5329
|
+
x: rx,
|
|
5330
|
+
y: ry,
|
|
5331
|
+
size: rhythmSize,
|
|
5332
|
+
shape: rhythmShape
|
|
5333
|
+
});
|
|
5334
|
+
spatialGrid.insert({
|
|
5335
|
+
x: rx,
|
|
5336
|
+
y: ry,
|
|
5337
|
+
size: rhythmSize,
|
|
5338
|
+
shape: rhythmShape
|
|
5339
|
+
});
|
|
5340
|
+
}
|
|
5341
|
+
extrasSpent += rhythmCount * styleCost;
|
|
5342
|
+
} else // Drain RNG — each rhythm step consumes ~3 rng calls for colors
|
|
5070
5343
|
for(let r = 0; r < rhythmCount; r++){
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
if ($b623126c6e9cbb71$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5075
|
-
rhythmSize *= rhythmDecay;
|
|
5076
|
-
if (rhythmSize < adjustedMinSize) break;
|
|
5077
|
-
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5078
|
-
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5079
|
-
const rhythmFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5080
|
-
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5081
|
-
fillColor: rhythmFill,
|
|
5082
|
-
strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5083
|
-
strokeWidth: strokeWidth * 0.7,
|
|
5084
|
-
size: rhythmSize,
|
|
5085
|
-
rotation: rotation + (r + 1) * 12,
|
|
5086
|
-
proportionType: "GOLDEN_RATIO",
|
|
5087
|
-
renderStyle: finalRenderStyle,
|
|
5088
|
-
rng: rng
|
|
5089
|
-
});
|
|
5090
|
-
shapePositions.push({
|
|
5091
|
-
x: rx,
|
|
5092
|
-
y: ry,
|
|
5093
|
-
size: rhythmSize,
|
|
5094
|
-
shape: rhythmShape
|
|
5095
|
-
});
|
|
5096
|
-
spatialGrid.insert({
|
|
5097
|
-
x: rx,
|
|
5098
|
-
y: ry,
|
|
5099
|
-
size: rhythmSize,
|
|
5100
|
-
shape: rhythmShape
|
|
5101
|
-
});
|
|
5344
|
+
rng();
|
|
5345
|
+
rng();
|
|
5346
|
+
rng();
|
|
5102
5347
|
}
|
|
5103
5348
|
}
|
|
5104
5349
|
}
|
|
@@ -5164,14 +5409,26 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5164
5409
|
}
|
|
5165
5410
|
}
|
|
5166
5411
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
5412
|
+
// Optimized: collect all segments into width-quantized buckets, then
|
|
5413
|
+
// render each bucket as a single batched path. This reduces
|
|
5414
|
+
// beginPath/stroke calls from O(segments) to O(buckets).
|
|
5167
5415
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
5168
5416
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
5417
|
+
// Width buckets — 6 buckets cover the taper×pressure range
|
|
5418
|
+
const FLOW_WIDTH_BUCKETS = 6;
|
|
5419
|
+
const flowBuckets = [];
|
|
5420
|
+
for(let b = 0; b < FLOW_WIDTH_BUCKETS; b++)flowBuckets.push([]);
|
|
5421
|
+
// Track the representative width for each bucket
|
|
5422
|
+
const flowBucketWidths = new Array(FLOW_WIDTH_BUCKETS);
|
|
5423
|
+
// Pre-compute max possible width for bucket assignment
|
|
5424
|
+
let globalMaxFlowWidth = 0;
|
|
5169
5425
|
for(let i = 0; i < numFlowLines; i++){
|
|
5170
5426
|
let fx = rng() * width;
|
|
5171
5427
|
let fy = rng() * height;
|
|
5172
5428
|
const steps = 30 + Math.floor(rng() * 40);
|
|
5173
5429
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
5174
5430
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
5431
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
5175
5432
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
5176
5433
|
const lineColorStart = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
5177
5434
|
const lineColorEnd = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
@@ -5193,19 +5450,22 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5193
5450
|
continue;
|
|
5194
5451
|
}
|
|
5195
5452
|
const t = s / steps;
|
|
5196
|
-
// Taper + pressure
|
|
5197
5453
|
const taper = 1 - t * 0.8;
|
|
5198
5454
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
5199
|
-
|
|
5200
|
-
|
|
5455
|
+
const segWidth = startWidth * taper * pressure;
|
|
5456
|
+
const segAlpha = lineAlpha * taper;
|
|
5201
5457
|
const lineColor = t < 0.5 ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5458
|
+
// Quantize width into bucket
|
|
5459
|
+
const bucketIdx = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(segWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5460
|
+
flowBuckets[bucketIdx].push({
|
|
5461
|
+
x1: prevX,
|
|
5462
|
+
y1: prevY,
|
|
5463
|
+
x2: fx,
|
|
5464
|
+
y2: fy,
|
|
5465
|
+
color: lineColor,
|
|
5466
|
+
alpha: segAlpha
|
|
5467
|
+
});
|
|
5468
|
+
flowBucketWidths[bucketIdx] = segWidth;
|
|
5209
5469
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
5210
5470
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
5211
5471
|
const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
|
|
@@ -5221,12 +5481,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5221
5481
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
5222
5482
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
5223
5483
|
const bTaper = 1 - bs / branchSteps * 0.9;
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5484
|
+
const bSegWidth = branchWidth * bTaper;
|
|
5485
|
+
const bAlpha = lineAlpha * taper * bTaper * 0.6;
|
|
5486
|
+
const bBucket = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(bSegWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5487
|
+
flowBuckets[bBucket].push({
|
|
5488
|
+
x1: bPrevX,
|
|
5489
|
+
y1: bPrevY,
|
|
5490
|
+
x2: bx,
|
|
5491
|
+
y2: by,
|
|
5492
|
+
color: lineColor,
|
|
5493
|
+
alpha: bAlpha
|
|
5494
|
+
});
|
|
5495
|
+
flowBucketWidths[bBucket] = bSegWidth;
|
|
5230
5496
|
bPrevX = bx;
|
|
5231
5497
|
bPrevY = by;
|
|
5232
5498
|
}
|
|
@@ -5235,7 +5501,40 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5235
5501
|
prevY = fy;
|
|
5236
5502
|
}
|
|
5237
5503
|
}
|
|
5504
|
+
// Render flow line buckets — one batched path per width bucket
|
|
5505
|
+
// Within each bucket, further sub-batch by quantized alpha (4 levels)
|
|
5506
|
+
ctx.lineCap = "round";
|
|
5507
|
+
const FLOW_ALPHA_BUCKETS = 4;
|
|
5508
|
+
for(let wb = 0; wb < FLOW_WIDTH_BUCKETS; wb++){
|
|
5509
|
+
const segs = flowBuckets[wb];
|
|
5510
|
+
if (segs.length === 0) continue;
|
|
5511
|
+
ctx.lineWidth = flowBucketWidths[wb];
|
|
5512
|
+
// Sub-bucket by alpha
|
|
5513
|
+
const alphaSubs = [];
|
|
5514
|
+
for(let a = 0; a < FLOW_ALPHA_BUCKETS; a++)alphaSubs.push([]);
|
|
5515
|
+
let maxAlpha = 0;
|
|
5516
|
+
for(let j = 0; j < segs.length; j++)if (segs[j].alpha > maxAlpha) maxAlpha = segs[j].alpha;
|
|
5517
|
+
for(let j = 0; j < segs.length; j++){
|
|
5518
|
+
const ai = Math.min(FLOW_ALPHA_BUCKETS - 1, Math.floor(segs[j].alpha / (maxAlpha || 1) * FLOW_ALPHA_BUCKETS));
|
|
5519
|
+
alphaSubs[ai].push(segs[j]);
|
|
5520
|
+
}
|
|
5521
|
+
for(let ai = 0; ai < FLOW_ALPHA_BUCKETS; ai++){
|
|
5522
|
+
const sub = alphaSubs[ai];
|
|
5523
|
+
if (sub.length === 0) continue;
|
|
5524
|
+
// Use the median segment's alpha and color as representative
|
|
5525
|
+
const rep = sub[Math.floor(sub.length / 2)];
|
|
5526
|
+
ctx.globalAlpha = rep.alpha;
|
|
5527
|
+
ctx.strokeStyle = rep.color;
|
|
5528
|
+
ctx.beginPath();
|
|
5529
|
+
for(let j = 0; j < sub.length; j++){
|
|
5530
|
+
ctx.moveTo(sub[j].x1, sub[j].y1);
|
|
5531
|
+
ctx.lineTo(sub[j].x2, sub[j].y2);
|
|
5532
|
+
}
|
|
5533
|
+
ctx.stroke();
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5238
5536
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
5537
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
5239
5538
|
const energyArchetypes = [
|
|
5240
5539
|
"dense-chaotic",
|
|
5241
5540
|
"cosmic",
|
|
@@ -5246,8 +5545,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5246
5545
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
5247
5546
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
5248
5547
|
ctx.lineCap = "round";
|
|
5548
|
+
// Collect all energy segments with their computed state
|
|
5549
|
+
const ENERGY_ALPHA_BUCKETS = 3;
|
|
5550
|
+
const energyBuckets = [];
|
|
5551
|
+
for(let b = 0; b < ENERGY_ALPHA_BUCKETS; b++)energyBuckets.push([]);
|
|
5552
|
+
const energyAlphas = new Array(ENERGY_ALPHA_BUCKETS).fill(0);
|
|
5249
5553
|
for(let e = 0; e < energyCount; e++){
|
|
5250
|
-
// Pick a random shape to radiate from
|
|
5251
5554
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5252
5555
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
5253
5556
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -5259,15 +5562,38 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5259
5562
|
const sy = source.y + Math.sin(angle) * startDist;
|
|
5260
5563
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
5261
5564
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5565
|
+
const eAlpha = 0.04 + rng() * 0.06;
|
|
5566
|
+
const eColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5567
|
+
const eLw = (0.5 + rng() * 1.5) * scaleFactor;
|
|
5568
|
+
// Quantize alpha into bucket
|
|
5569
|
+
const bi = Math.min(ENERGY_ALPHA_BUCKETS - 1, Math.floor((eAlpha - 0.04) / 0.06 * ENERGY_ALPHA_BUCKETS));
|
|
5570
|
+
energyBuckets[bi].push({
|
|
5571
|
+
x1: sx,
|
|
5572
|
+
y1: sy,
|
|
5573
|
+
x2: ex,
|
|
5574
|
+
y2: ey,
|
|
5575
|
+
color: eColor,
|
|
5576
|
+
lw: eLw
|
|
5577
|
+
});
|
|
5578
|
+
energyAlphas[bi] = eAlpha;
|
|
5269
5579
|
}
|
|
5270
5580
|
}
|
|
5581
|
+
// Render batched energy lines
|
|
5582
|
+
for(let bi = 0; bi < ENERGY_ALPHA_BUCKETS; bi++){
|
|
5583
|
+
const segs = energyBuckets[bi];
|
|
5584
|
+
if (segs.length === 0) continue;
|
|
5585
|
+
ctx.globalAlpha = energyAlphas[bi];
|
|
5586
|
+
// Use median segment's color and width as representative
|
|
5587
|
+
const rep = segs[Math.floor(segs.length / 2)];
|
|
5588
|
+
ctx.strokeStyle = rep.color;
|
|
5589
|
+
ctx.lineWidth = rep.lw;
|
|
5590
|
+
ctx.beginPath();
|
|
5591
|
+
for(let j = 0; j < segs.length; j++){
|
|
5592
|
+
ctx.moveTo(segs[j].x1, segs[j].y1);
|
|
5593
|
+
ctx.lineTo(segs[j].x2, segs[j].y2);
|
|
5594
|
+
}
|
|
5595
|
+
ctx.stroke();
|
|
5596
|
+
}
|
|
5271
5597
|
}
|
|
5272
5598
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
5273
5599
|
if (symmetryMode !== "none") {
|
|
@@ -5290,27 +5616,44 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5290
5616
|
ctx.restore();
|
|
5291
5617
|
}
|
|
5292
5618
|
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
5619
|
+
// Optimized: cap density at large sizes (diminishing returns above ~2K dots),
|
|
5620
|
+
// skip inner pixelScale loop when scale=1, use Uint32Array for faster writes.
|
|
5293
5621
|
const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
|
|
5294
|
-
const
|
|
5622
|
+
const rawNoiseDensity = Math.floor(width * height / 800);
|
|
5623
|
+
// Cap at 2500 dots — beyond this the visual effect is indistinguishable
|
|
5624
|
+
// but getImageData/putImageData cost scales with canvas size
|
|
5625
|
+
const noiseDensity = Math.min(rawNoiseDensity, 2500);
|
|
5295
5626
|
try {
|
|
5296
5627
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5297
5628
|
const data = imageData.data;
|
|
5298
5629
|
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5630
|
+
if (pixelScale === 1) // Fast path — no inner loop, direct pixel write
|
|
5631
|
+
// Pre-compute alpha blend as integer math (avoid float multiply per channel)
|
|
5299
5632
|
for(let i = 0; i < noiseDensity; i++){
|
|
5300
5633
|
const nx = Math.floor(noiseRng() * width);
|
|
5301
5634
|
const ny = Math.floor(noiseRng() * height);
|
|
5302
5635
|
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5303
|
-
|
|
5304
|
-
|
|
5636
|
+
// srcA in range [0.01, 0.04] — multiply by 256 for fixed-point
|
|
5637
|
+
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
5638
|
+
const invA256 = 256 - srcA256;
|
|
5639
|
+
const bSrc = brightness * srcA256; // pre-multiply brightness × alpha
|
|
5640
|
+
const idx = ny * width + nx << 2;
|
|
5641
|
+
data[idx] = data[idx] * invA256 + bSrc >> 8;
|
|
5642
|
+
data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
|
|
5643
|
+
data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
|
|
5644
|
+
}
|
|
5645
|
+
else for(let i = 0; i < noiseDensity; i++){
|
|
5646
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5647
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5648
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5649
|
+
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
5650
|
+
const invA256 = 256 - srcA256;
|
|
5651
|
+
const bSrc = brightness * srcA256;
|
|
5305
5652
|
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5306
|
-
const idx = (
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5311
|
-
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5312
|
-
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5313
|
-
// Keep existing alpha
|
|
5653
|
+
const idx = (ny + dy) * width + (nx + dx) << 2;
|
|
5654
|
+
data[idx] = data[idx] * invA256 + bSrc >> 8;
|
|
5655
|
+
data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
|
|
5656
|
+
data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
|
|
5314
5657
|
}
|
|
5315
5658
|
}
|
|
5316
5659
|
ctx.putImageData(imageData, 0, 0);
|
|
@@ -5340,10 +5683,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5340
5683
|
ctx.fillStyle = vigGrad;
|
|
5341
5684
|
ctx.fillRect(0, 0, width, height);
|
|
5342
5685
|
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5686
|
+
// Optimized: batch all curves into alpha-quantized groups to reduce
|
|
5687
|
+
// beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
|
|
5343
5688
|
if (shapePositions.length > 1) {
|
|
5344
5689
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5345
5690
|
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5346
5691
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5692
|
+
// Collect curves into 3 alpha buckets
|
|
5693
|
+
const CURVE_ALPHA_BUCKETS = 3;
|
|
5694
|
+
const curveBuckets = [];
|
|
5695
|
+
const curveColors = [];
|
|
5696
|
+
const curveAlphas = new Array(CURVE_ALPHA_BUCKETS).fill(0);
|
|
5697
|
+
for(let b = 0; b < CURVE_ALPHA_BUCKETS; b++)curveBuckets.push([]);
|
|
5347
5698
|
for(let i = 0; i < numCurves; i++){
|
|
5348
5699
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
5349
5700
|
const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
|
|
@@ -5360,11 +5711,32 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5360
5711
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5361
5712
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5362
5713
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
5363
|
-
|
|
5364
|
-
|
|
5714
|
+
const curveAlpha = 0.06 + rng() * 0.1;
|
|
5715
|
+
const curveColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5716
|
+
const bi = Math.min(CURVE_ALPHA_BUCKETS - 1, Math.floor((curveAlpha - 0.06) / 0.1 * CURVE_ALPHA_BUCKETS));
|
|
5717
|
+
curveBuckets[bi].push({
|
|
5718
|
+
ax: a.x,
|
|
5719
|
+
ay: a.y,
|
|
5720
|
+
cpx: cpx,
|
|
5721
|
+
cpy: cpy,
|
|
5722
|
+
bx: b.x,
|
|
5723
|
+
by: b.y
|
|
5724
|
+
});
|
|
5725
|
+
curveAlphas[bi] = curveAlpha;
|
|
5726
|
+
if (!curveColors[bi]) curveColors[bi] = curveColor;
|
|
5727
|
+
}
|
|
5728
|
+
// Render batched curves
|
|
5729
|
+
for(let bi = 0; bi < CURVE_ALPHA_BUCKETS; bi++){
|
|
5730
|
+
const curves = curveBuckets[bi];
|
|
5731
|
+
if (curves.length === 0) continue;
|
|
5732
|
+
ctx.globalAlpha = curveAlphas[bi];
|
|
5733
|
+
ctx.strokeStyle = curveColors[bi];
|
|
5365
5734
|
ctx.beginPath();
|
|
5366
|
-
|
|
5367
|
-
|
|
5735
|
+
for(let j = 0; j < curves.length; j++){
|
|
5736
|
+
const c = curves[j];
|
|
5737
|
+
ctx.moveTo(c.ax, c.ay);
|
|
5738
|
+
ctx.quadraticCurveTo(c.cpx, c.cpy, c.bx, c.by);
|
|
5739
|
+
}
|
|
5368
5740
|
ctx.stroke();
|
|
5369
5741
|
}
|
|
5370
5742
|
}
|
|
@@ -5482,11 +5854,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5482
5854
|
}
|
|
5483
5855
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5484
5856
|
// Vine tendrils — organic curving lines along edges
|
|
5857
|
+
// Optimized: batch all tendrils into a single path
|
|
5485
5858
|
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5486
5859
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5487
5860
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5488
5861
|
ctx.lineCap = "round";
|
|
5489
5862
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5863
|
+
ctx.beginPath();
|
|
5864
|
+
const leafPositions = [];
|
|
5490
5865
|
for(let t = 0; t < tendrilCount; t++){
|
|
5491
5866
|
// Start from a random edge point
|
|
5492
5867
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -5504,7 +5879,6 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5504
5879
|
tx = width - borderPad;
|
|
5505
5880
|
ty = borderRng() * height;
|
|
5506
5881
|
}
|
|
5507
|
-
ctx.beginPath();
|
|
5508
5882
|
ctx.moveTo(tx, ty);
|
|
5509
5883
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5510
5884
|
for(let s = 0; s < segs; s++){
|
|
@@ -5518,14 +5892,23 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5518
5892
|
ty = cpy3;
|
|
5519
5893
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5520
5894
|
}
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5895
|
+
// Collect leaf positions for batch fill
|
|
5896
|
+
if (borderRng() < 0.6) leafPositions.push({
|
|
5897
|
+
x: tx,
|
|
5898
|
+
y: ty,
|
|
5899
|
+
r: borderPad * (0.15 + borderRng() * 0.2)
|
|
5900
|
+
});
|
|
5901
|
+
}
|
|
5902
|
+
ctx.stroke();
|
|
5903
|
+
// Batch all leaf dots into a single fill
|
|
5904
|
+
if (leafPositions.length > 0) {
|
|
5905
|
+
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5906
|
+
ctx.beginPath();
|
|
5907
|
+
for (const leaf of leafPositions){
|
|
5908
|
+
ctx.moveTo(leaf.x + leaf.r, leaf.y);
|
|
5909
|
+
ctx.arc(leaf.x, leaf.y, leaf.r, 0, Math.PI * 2);
|
|
5528
5910
|
}
|
|
5911
|
+
ctx.fill();
|
|
5529
5912
|
}
|
|
5530
5913
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5531
5914
|
// Star-studded arcs along edges
|
|
@@ -5540,8 +5923,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5540
5923
|
ctx.beginPath();
|
|
5541
5924
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5542
5925
|
ctx.stroke();
|
|
5543
|
-
// Scatter small stars along the border region
|
|
5926
|
+
// Scatter small stars along the border region — batched into single path
|
|
5544
5927
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5928
|
+
ctx.beginPath();
|
|
5545
5929
|
for(let s = 0; s < starCount; s++){
|
|
5546
5930
|
const edge = Math.floor(borderRng() * 4);
|
|
5547
5931
|
let sx, sy;
|
|
@@ -5560,7 +5944,6 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5560
5944
|
}
|
|
5561
5945
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5562
5946
|
// 4-point star
|
|
5563
|
-
ctx.beginPath();
|
|
5564
5947
|
for(let p = 0; p < 8; p++){
|
|
5565
5948
|
const a = p / 8 * Math.PI * 2;
|
|
5566
5949
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -5570,8 +5953,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5570
5953
|
else ctx.lineTo(px2, py2);
|
|
5571
5954
|
}
|
|
5572
5955
|
ctx.closePath();
|
|
5573
|
-
ctx.fill();
|
|
5574
5956
|
}
|
|
5957
|
+
ctx.fill();
|
|
5575
5958
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5576
5959
|
// Thin single rule — understated elegance
|
|
5577
5960
|
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|