git-hash-art 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy-www.yml +13 -3
- package/ALGORITHM.md +76 -24
- package/CHANGELOG.md +18 -0
- package/dist/browser.js +938 -251
- package/dist/browser.js.map +1 -1
- package/dist/main.js +940 -251
- package/dist/main.js.map +1 -1
- package/dist/module.js +940 -251
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/performance.test.ts +243 -0
- package/src/__tests__/phase-timing.test.ts +260 -0
- package/src/__tests__/profile-pipeline.test.ts +160 -0
- package/src/lib/archetypes.ts +29 -0
- package/src/lib/canvas/colors.ts +30 -11
- package/src/lib/canvas/draw.ts +147 -50
- package/src/lib/canvas/shapes/complex.ts +19 -10
- package/src/lib/canvas/shapes/sacred.ts +16 -17
- package/src/lib/render.ts +663 -204
package/dist/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 {
|
|
@@ -589,15 +599,17 @@ function $9d614e7d77fc2947$export$fabac4600b87056(colors, rng) {
|
|
|
589
599
|
accent: colors[colors.length - 1] || "#888888",
|
|
590
600
|
all: colors
|
|
591
601
|
};
|
|
592
|
-
// Pick dominant as the color
|
|
602
|
+
// Pick dominant as the color with the highest chroma (saturation × distance from gray)
|
|
603
|
+
// This selects the most visually prominent color rather than the average
|
|
593
604
|
const hsls = colors.map((c)=>$9d614e7d77fc2947$var$hexToHsl(c));
|
|
594
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
595
605
|
let dominantIdx = 0;
|
|
596
|
-
let
|
|
606
|
+
let maxChroma = -1;
|
|
597
607
|
for(let i = 0; i < hsls.length; i++){
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
608
|
+
// Chroma approximation: saturation × how far lightness is from 50% (gray)
|
|
609
|
+
const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
|
|
610
|
+
const chroma = hsls[i][1] * lightnessVibrancy;
|
|
611
|
+
if (chroma > maxChroma) {
|
|
612
|
+
maxChroma = chroma;
|
|
601
613
|
dominantIdx = i;
|
|
602
614
|
}
|
|
603
615
|
}
|
|
@@ -658,12 +670,21 @@ function $9d614e7d77fc2947$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
|
658
670
|
const [h, s, l] = $9d614e7d77fc2947$var$hexToHsl(hex);
|
|
659
671
|
return $9d614e7d77fc2947$var$hslToHex($9d614e7d77fc2947$var$shiftHueToward(h, target, amount), s, l);
|
|
660
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();
|
|
661
677
|
function $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe(hex) {
|
|
678
|
+
let cached = $9d614e7d77fc2947$var$_lumCache.get(hex);
|
|
679
|
+
if (cached !== undefined) return cached;
|
|
662
680
|
const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex).map((c)=>{
|
|
663
681
|
const s = c / 255;
|
|
664
682
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
665
683
|
});
|
|
666
|
-
|
|
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;
|
|
667
688
|
}
|
|
668
689
|
function $9d614e7d77fc2947$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
|
|
669
690
|
const fgLum = $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe(fgHex);
|
|
@@ -1099,21 +1120,31 @@ const $8bde0a7ee87832b5$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
|
|
|
1099
1120
|
(0, $79312e33271883e9$export$e46c5570db033611)(ctx, size, finalConfig);
|
|
1100
1121
|
const gridSize = 8;
|
|
1101
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
|
+
}
|
|
1102
1136
|
ctx.beginPath();
|
|
1103
1137
|
// Create base grid
|
|
1104
|
-
for(let i = 0; i <= gridSize; i++)
|
|
1138
|
+
for(let i = 0; i <= gridSize; i++){
|
|
1105
1139
|
const x = (i - gridSize / 2) * unit;
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
const y2 = y + radius * Math.sin(angle + Math.PI / 4);
|
|
1115
|
-
ctx.moveTo(x1, y1);
|
|
1116
|
-
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
|
+
}
|
|
1117
1148
|
}
|
|
1118
1149
|
}
|
|
1119
1150
|
ctx.stroke();
|
|
@@ -1433,20 +1464,23 @@ const $d63629e16208c310$export$eeae7765f05012e2 = (ctx, size)=>{
|
|
|
1433
1464
|
const $d63629e16208c310$export$3355220a8108efc3 = (ctx, size)=>{
|
|
1434
1465
|
const outerRadius = size / 2;
|
|
1435
1466
|
const innerRadius = size / 4;
|
|
1436
|
-
|
|
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;
|
|
1437
1472
|
ctx.beginPath();
|
|
1438
1473
|
for(let i = 0; i < steps; i++){
|
|
1439
|
-
const angle1 = i
|
|
1440
|
-
|
|
1474
|
+
const angle1 = i * angleStep;
|
|
1475
|
+
const cosA = Math.cos(angle1);
|
|
1476
|
+
const sinA = Math.sin(angle1);
|
|
1441
1477
|
for(let j = 0; j < steps; j++){
|
|
1442
|
-
const phi1 = j
|
|
1443
|
-
const phi2 =
|
|
1444
|
-
const
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
ctx.moveTo(x1, y1);
|
|
1449
|
-
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);
|
|
1450
1484
|
}
|
|
1451
1485
|
}
|
|
1452
1486
|
};
|
|
@@ -2009,6 +2043,43 @@ const $9beb8f41637c29fd$var$RENDER_STYLES = [
|
|
|
2009
2043
|
function $9beb8f41637c29fd$export$9fd4e64b2acd410e(rng) {
|
|
2010
2044
|
return $9beb8f41637c29fd$var$RENDER_STYLES[Math.floor(rng() * $9beb8f41637c29fd$var$RENDER_STYLES.length)];
|
|
2011
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
|
+
}
|
|
2012
2083
|
function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
2013
2084
|
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
|
|
2014
2085
|
ctx.save();
|
|
@@ -2128,6 +2199,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2128
2199
|
case "hatched":
|
|
2129
2200
|
{
|
|
2130
2201
|
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
2202
|
+
// Optimized: batch all parallel lines into a single path per pass
|
|
2131
2203
|
const savedAlphaH = ctx.globalAlpha;
|
|
2132
2204
|
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
2133
2205
|
ctx.fill();
|
|
@@ -2139,28 +2211,28 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2139
2211
|
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
2140
2212
|
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
2141
2213
|
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
2142
|
-
// Draw parallel lines across the bounding box
|
|
2214
|
+
// Draw parallel lines across the bounding box — batched into single path
|
|
2143
2215
|
const extent = size * 0.8;
|
|
2144
2216
|
const cos = Math.cos(hatchAngle);
|
|
2145
2217
|
const sin = Math.sin(hatchAngle);
|
|
2218
|
+
ctx.beginPath();
|
|
2146
2219
|
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
2147
|
-
ctx.beginPath();
|
|
2148
2220
|
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
2149
2221
|
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
2150
|
-
ctx.stroke();
|
|
2151
2222
|
}
|
|
2223
|
+
ctx.stroke();
|
|
2152
2224
|
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
2153
2225
|
if (!rng || rng() < 0.5) {
|
|
2154
2226
|
const crossAngle = hatchAngle + Math.PI / 2;
|
|
2155
2227
|
const cos2 = Math.cos(crossAngle);
|
|
2156
2228
|
const sin2 = Math.sin(crossAngle);
|
|
2157
2229
|
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
2230
|
+
ctx.beginPath();
|
|
2158
2231
|
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
2159
|
-
ctx.beginPath();
|
|
2160
2232
|
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
2161
2233
|
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
2162
|
-
ctx.stroke();
|
|
2163
2234
|
}
|
|
2235
|
+
ctx.stroke();
|
|
2164
2236
|
}
|
|
2165
2237
|
ctx.restore();
|
|
2166
2238
|
ctx.globalAlpha = savedAlphaH;
|
|
@@ -2198,6 +2270,8 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2198
2270
|
case "stipple":
|
|
2199
2271
|
{
|
|
2200
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.
|
|
2201
2275
|
const savedAlphaS = ctx.globalAlpha;
|
|
2202
2276
|
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
2203
2277
|
ctx.fill(); // ghost fill
|
|
@@ -2205,16 +2279,20 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2205
2279
|
ctx.save();
|
|
2206
2280
|
ctx.clip();
|
|
2207
2281
|
const dotSpacing = Math.max(2, size * 0.03);
|
|
2208
|
-
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;
|
|
2209
2286
|
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
2210
|
-
for(let
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
+
}
|
|
2218
2296
|
}
|
|
2219
2297
|
ctx.restore();
|
|
2220
2298
|
ctx.globalAlpha = savedAlphaS;
|
|
@@ -2247,6 +2325,9 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2247
2325
|
case "noise-grain":
|
|
2248
2326
|
{
|
|
2249
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
|
|
2250
2331
|
const savedAlphaN = ctx.globalAlpha;
|
|
2251
2332
|
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
2252
2333
|
ctx.fill(); // base tint
|
|
@@ -2255,17 +2336,47 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2255
2336
|
ctx.clip();
|
|
2256
2337
|
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
2257
2338
|
const extentN = size * 0.55;
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
const
|
|
2262
|
-
const
|
|
2263
|
-
|
|
2264
|
-
const
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
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
|
+
}
|
|
2269
2380
|
}
|
|
2270
2381
|
ctx.restore();
|
|
2271
2382
|
ctx.fillStyle = fillColor;
|
|
@@ -2278,6 +2389,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2278
2389
|
case "wood-grain":
|
|
2279
2390
|
{
|
|
2280
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
|
|
2281
2393
|
const savedAlphaW = ctx.globalAlpha;
|
|
2282
2394
|
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
2283
2395
|
ctx.fill(); // base tint
|
|
@@ -2293,17 +2405,19 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2293
2405
|
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
2294
2406
|
const cosG = Math.cos(grainAngle);
|
|
2295
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();
|
|
2296
2412
|
for(let d = -extentW; d <= extentW; d += grainLineSpacing){
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
if (t === -extentW) ctx.moveTo(px, py);
|
|
2303
|
-
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);
|
|
2304
2418
|
}
|
|
2305
|
-
ctx.stroke();
|
|
2306
2419
|
}
|
|
2420
|
+
ctx.stroke();
|
|
2307
2421
|
ctx.restore();
|
|
2308
2422
|
ctx.globalAlpha = savedAlphaW;
|
|
2309
2423
|
ctx.globalAlpha *= 0.35;
|
|
@@ -2365,6 +2479,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2365
2479
|
case "fabric-weave":
|
|
2366
2480
|
{
|
|
2367
2481
|
// Interlocking horizontal/vertical threads clipped to shape
|
|
2482
|
+
// Optimized: batch all horizontal threads into one path, all vertical into another
|
|
2368
2483
|
const savedAlphaF = ctx.globalAlpha;
|
|
2369
2484
|
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
2370
2485
|
ctx.fill(); // ghost base
|
|
@@ -2374,26 +2489,24 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2374
2489
|
const threadSpacing = Math.max(2, size * 0.04);
|
|
2375
2490
|
const extentF = size * 0.55;
|
|
2376
2491
|
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
2492
|
+
// Horizontal threads — batched
|
|
2377
2493
|
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
2378
|
-
|
|
2494
|
+
ctx.beginPath();
|
|
2379
2495
|
for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2380
|
-
ctx.beginPath();
|
|
2381
2496
|
ctx.moveTo(-extentF, y);
|
|
2382
2497
|
ctx.lineTo(extentF, y);
|
|
2383
|
-
ctx.stroke();
|
|
2384
2498
|
}
|
|
2385
|
-
|
|
2499
|
+
ctx.stroke();
|
|
2500
|
+
// Vertical threads (offset by half spacing for weave effect) — batched
|
|
2386
2501
|
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
2387
2502
|
ctx.strokeStyle = fillColor;
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
ctx.lineTo(x, y + threadSpacing);
|
|
2394
|
-
}
|
|
2395
|
-
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);
|
|
2396
2508
|
}
|
|
2509
|
+
ctx.stroke();
|
|
2397
2510
|
ctx.strokeStyle = strokeColor;
|
|
2398
2511
|
ctx.restore();
|
|
2399
2512
|
ctx.globalAlpha = savedAlphaF;
|
|
@@ -2459,14 +2572,17 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2459
2572
|
ctx.translate(x, y);
|
|
2460
2573
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2461
2574
|
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2462
|
-
|
|
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) {
|
|
2463
2579
|
const shadowDist = size * 0.035;
|
|
2464
2580
|
const shadowBlurR = size * 0.06;
|
|
2465
2581
|
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2466
2582
|
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2467
2583
|
ctx.shadowBlur = shadowBlurR;
|
|
2468
2584
|
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2469
|
-
} else if (glowRadius > 0) {
|
|
2585
|
+
} else if (useShadow && glowRadius > 0) {
|
|
2470
2586
|
// Glow / shadow effect (legacy path)
|
|
2471
2587
|
ctx.shadowBlur = glowRadius;
|
|
2472
2588
|
ctx.shadowColor = glowColor || fillColor;
|
|
@@ -2490,17 +2606,24 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2490
2606
|
$9beb8f41637c29fd$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2491
2607
|
}
|
|
2492
2608
|
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2493
|
-
|
|
2494
|
-
|
|
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
|
+
}
|
|
2616
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
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) {
|
|
2499
2620
|
const hlRadius = size * 0.35;
|
|
2500
2621
|
const hlDist = size * 0.15;
|
|
2501
2622
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2502
2623
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2503
2624
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2625
|
+
// Use a simple white highlight — the per-shape hex parse was expensive
|
|
2626
|
+
// and the visual difference from tinted highlights is negligible.
|
|
2504
2627
|
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2505
2628
|
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2506
2629
|
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
@@ -3565,6 +3688,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3565
3688
|
"watercolor",
|
|
3566
3689
|
"fill-only"
|
|
3567
3690
|
],
|
|
3691
|
+
preferredCompositions: [
|
|
3692
|
+
"clustered",
|
|
3693
|
+
"flow-field",
|
|
3694
|
+
"radial"
|
|
3695
|
+
],
|
|
3568
3696
|
flowLineMultiplier: 2.5,
|
|
3569
3697
|
heroShape: false,
|
|
3570
3698
|
glowMultiplier: 0.5,
|
|
@@ -3586,6 +3714,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3586
3714
|
"stroke-only",
|
|
3587
3715
|
"incomplete"
|
|
3588
3716
|
],
|
|
3717
|
+
preferredCompositions: [
|
|
3718
|
+
"golden-spiral",
|
|
3719
|
+
"grid-subdivision"
|
|
3720
|
+
],
|
|
3589
3721
|
flowLineMultiplier: 0.3,
|
|
3590
3722
|
heroShape: true,
|
|
3591
3723
|
glowMultiplier: 0,
|
|
@@ -3607,6 +3739,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3607
3739
|
"fill-only",
|
|
3608
3740
|
"incomplete"
|
|
3609
3741
|
],
|
|
3742
|
+
preferredCompositions: [
|
|
3743
|
+
"flow-field",
|
|
3744
|
+
"golden-spiral",
|
|
3745
|
+
"spiral"
|
|
3746
|
+
],
|
|
3610
3747
|
flowLineMultiplier: 4,
|
|
3611
3748
|
heroShape: false,
|
|
3612
3749
|
glowMultiplier: 0.3,
|
|
@@ -3629,6 +3766,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3629
3766
|
"double-stroke",
|
|
3630
3767
|
"hatched"
|
|
3631
3768
|
],
|
|
3769
|
+
preferredCompositions: [
|
|
3770
|
+
"grid-subdivision",
|
|
3771
|
+
"radial"
|
|
3772
|
+
],
|
|
3632
3773
|
flowLineMultiplier: 0,
|
|
3633
3774
|
heroShape: false,
|
|
3634
3775
|
glowMultiplier: 0,
|
|
@@ -3650,6 +3791,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3650
3791
|
"incomplete",
|
|
3651
3792
|
"fill-only"
|
|
3652
3793
|
],
|
|
3794
|
+
preferredCompositions: [
|
|
3795
|
+
"golden-spiral",
|
|
3796
|
+
"radial",
|
|
3797
|
+
"spiral"
|
|
3798
|
+
],
|
|
3653
3799
|
flowLineMultiplier: 1.5,
|
|
3654
3800
|
heroShape: true,
|
|
3655
3801
|
glowMultiplier: 2,
|
|
@@ -3670,6 +3816,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3670
3816
|
"fill-and-stroke",
|
|
3671
3817
|
"double-stroke"
|
|
3672
3818
|
],
|
|
3819
|
+
preferredCompositions: [
|
|
3820
|
+
"grid-subdivision",
|
|
3821
|
+
"golden-spiral"
|
|
3822
|
+
],
|
|
3673
3823
|
flowLineMultiplier: 0,
|
|
3674
3824
|
heroShape: true,
|
|
3675
3825
|
glowMultiplier: 0,
|
|
@@ -3691,6 +3841,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3691
3841
|
"double-stroke",
|
|
3692
3842
|
"dashed"
|
|
3693
3843
|
],
|
|
3844
|
+
preferredCompositions: [
|
|
3845
|
+
"radial",
|
|
3846
|
+
"spiral",
|
|
3847
|
+
"clustered"
|
|
3848
|
+
],
|
|
3694
3849
|
flowLineMultiplier: 2,
|
|
3695
3850
|
heroShape: true,
|
|
3696
3851
|
glowMultiplier: 3,
|
|
@@ -3713,6 +3868,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3713
3868
|
"stroke-only",
|
|
3714
3869
|
"dashed"
|
|
3715
3870
|
],
|
|
3871
|
+
preferredCompositions: [
|
|
3872
|
+
"flow-field",
|
|
3873
|
+
"grid-subdivision",
|
|
3874
|
+
"clustered"
|
|
3875
|
+
],
|
|
3716
3876
|
flowLineMultiplier: 1.5,
|
|
3717
3877
|
heroShape: false,
|
|
3718
3878
|
glowMultiplier: 0,
|
|
@@ -3734,6 +3894,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3734
3894
|
"watercolor",
|
|
3735
3895
|
"fill-and-stroke"
|
|
3736
3896
|
],
|
|
3897
|
+
preferredCompositions: [
|
|
3898
|
+
"radial",
|
|
3899
|
+
"spiral",
|
|
3900
|
+
"golden-spiral"
|
|
3901
|
+
],
|
|
3737
3902
|
flowLineMultiplier: 3,
|
|
3738
3903
|
heroShape: true,
|
|
3739
3904
|
glowMultiplier: 2.5,
|
|
@@ -3755,6 +3920,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3755
3920
|
"fill-only",
|
|
3756
3921
|
"incomplete"
|
|
3757
3922
|
],
|
|
3923
|
+
preferredCompositions: [
|
|
3924
|
+
"golden-spiral",
|
|
3925
|
+
"flow-field",
|
|
3926
|
+
"radial"
|
|
3927
|
+
],
|
|
3758
3928
|
flowLineMultiplier: 0.5,
|
|
3759
3929
|
heroShape: false,
|
|
3760
3930
|
glowMultiplier: 0.3,
|
|
@@ -3776,6 +3946,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3776
3946
|
"stroke-only",
|
|
3777
3947
|
"dashed"
|
|
3778
3948
|
],
|
|
3949
|
+
preferredCompositions: [
|
|
3950
|
+
"grid-subdivision",
|
|
3951
|
+
"radial"
|
|
3952
|
+
],
|
|
3779
3953
|
flowLineMultiplier: 0,
|
|
3780
3954
|
heroShape: false,
|
|
3781
3955
|
glowMultiplier: 0,
|
|
@@ -3797,6 +3971,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3797
3971
|
"fill-only",
|
|
3798
3972
|
"double-stroke"
|
|
3799
3973
|
],
|
|
3974
|
+
preferredCompositions: [
|
|
3975
|
+
"grid-subdivision",
|
|
3976
|
+
"clustered"
|
|
3977
|
+
],
|
|
3800
3978
|
flowLineMultiplier: 0,
|
|
3801
3979
|
heroShape: true,
|
|
3802
3980
|
glowMultiplier: 0,
|
|
@@ -3818,6 +3996,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3818
3996
|
"watercolor",
|
|
3819
3997
|
"fill-only"
|
|
3820
3998
|
],
|
|
3999
|
+
preferredCompositions: [
|
|
4000
|
+
"radial",
|
|
4001
|
+
"golden-spiral",
|
|
4002
|
+
"flow-field"
|
|
4003
|
+
],
|
|
3821
4004
|
flowLineMultiplier: 1,
|
|
3822
4005
|
heroShape: true,
|
|
3823
4006
|
glowMultiplier: 1,
|
|
@@ -3839,6 +4022,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3839
4022
|
"stroke-only",
|
|
3840
4023
|
"fill-only"
|
|
3841
4024
|
],
|
|
4025
|
+
preferredCompositions: [
|
|
4026
|
+
"clustered",
|
|
4027
|
+
"grid-subdivision",
|
|
4028
|
+
"radial"
|
|
4029
|
+
],
|
|
3842
4030
|
flowLineMultiplier: 0,
|
|
3843
4031
|
heroShape: false,
|
|
3844
4032
|
glowMultiplier: 0.3,
|
|
@@ -3860,6 +4048,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3860
4048
|
"fill-only",
|
|
3861
4049
|
"incomplete"
|
|
3862
4050
|
],
|
|
4051
|
+
preferredCompositions: [
|
|
4052
|
+
"flow-field",
|
|
4053
|
+
"golden-spiral",
|
|
4054
|
+
"spiral"
|
|
4055
|
+
],
|
|
3863
4056
|
flowLineMultiplier: 3,
|
|
3864
4057
|
heroShape: true,
|
|
3865
4058
|
glowMultiplier: 0.2,
|
|
@@ -3881,6 +4074,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3881
4074
|
"fill-only",
|
|
3882
4075
|
"hatched"
|
|
3883
4076
|
],
|
|
4077
|
+
preferredCompositions: [
|
|
4078
|
+
"radial",
|
|
4079
|
+
"clustered",
|
|
4080
|
+
"flow-field"
|
|
4081
|
+
],
|
|
3884
4082
|
flowLineMultiplier: 0,
|
|
3885
4083
|
heroShape: false,
|
|
3886
4084
|
glowMultiplier: 0,
|
|
@@ -3903,6 +4101,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3903
4101
|
"stroke-only",
|
|
3904
4102
|
"incomplete"
|
|
3905
4103
|
],
|
|
4104
|
+
preferredCompositions: [
|
|
4105
|
+
"spiral",
|
|
4106
|
+
"radial",
|
|
4107
|
+
"golden-spiral"
|
|
4108
|
+
],
|
|
3906
4109
|
flowLineMultiplier: 2,
|
|
3907
4110
|
heroShape: true,
|
|
3908
4111
|
glowMultiplier: 2.5,
|
|
@@ -3926,6 +4129,12 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3926
4129
|
...b.preferredStyles
|
|
3927
4130
|
])
|
|
3928
4131
|
];
|
|
4132
|
+
const mergedCompositions = [
|
|
4133
|
+
...new Set([
|
|
4134
|
+
...a.preferredCompositions,
|
|
4135
|
+
...b.preferredCompositions
|
|
4136
|
+
])
|
|
4137
|
+
];
|
|
3929
4138
|
return {
|
|
3930
4139
|
name: `${a.name}+${b.name}`,
|
|
3931
4140
|
gridSize: Math.round($3faa2521b78398cf$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3937,6 +4146,7 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3937
4146
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3938
4147
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3939
4148
|
preferredStyles: mergedStyles,
|
|
4149
|
+
preferredCompositions: mergedCompositions,
|
|
3940
4150
|
flowLineMultiplier: $3faa2521b78398cf$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3941
4151
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3942
4152
|
glowMultiplier: $3faa2521b78398cf$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3958,6 +4168,46 @@ function $3faa2521b78398cf$export$f1142fd7da4d6590(rng) {
|
|
|
3958
4168
|
}
|
|
3959
4169
|
|
|
3960
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
|
+
}
|
|
3961
4211
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
3962
4212
|
const $b623126c6e9cbb71$var$SACRED_SHAPES = [
|
|
3963
4213
|
"mandala",
|
|
@@ -3970,7 +4220,8 @@ const $b623126c6e9cbb71$var$SACRED_SHAPES = [
|
|
|
3970
4220
|
"torus",
|
|
3971
4221
|
"eggOfLife"
|
|
3972
4222
|
];
|
|
3973
|
-
|
|
4223
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4224
|
+
const $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES = [
|
|
3974
4225
|
"radial",
|
|
3975
4226
|
"flow-field",
|
|
3976
4227
|
"spiral",
|
|
@@ -4072,7 +4323,69 @@ function $b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones) {
|
|
|
4072
4323
|
}
|
|
4073
4324
|
return false;
|
|
4074
4325
|
}
|
|
4075
|
-
// ──
|
|
4326
|
+
// ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
|
|
4327
|
+
class $b623126c6e9cbb71$var$SpatialGrid {
|
|
4328
|
+
cells;
|
|
4329
|
+
cellSize;
|
|
4330
|
+
constructor(cellSize){
|
|
4331
|
+
this.cells = new Map();
|
|
4332
|
+
this.cellSize = cellSize;
|
|
4333
|
+
}
|
|
4334
|
+
key(cx, cy) {
|
|
4335
|
+
return `${cx},${cy}`;
|
|
4336
|
+
}
|
|
4337
|
+
insert(item) {
|
|
4338
|
+
const cx = Math.floor(item.x / this.cellSize);
|
|
4339
|
+
const cy = Math.floor(item.y / this.cellSize);
|
|
4340
|
+
const k = this.key(cx, cy);
|
|
4341
|
+
const cell = this.cells.get(k);
|
|
4342
|
+
if (cell) cell.push(item);
|
|
4343
|
+
else this.cells.set(k, [
|
|
4344
|
+
item
|
|
4345
|
+
]);
|
|
4346
|
+
}
|
|
4347
|
+
/** Count items within radius of (x, y) */ countNear(x, y, radius) {
|
|
4348
|
+
const r2 = radius * radius;
|
|
4349
|
+
const minCx = Math.floor((x - radius) / this.cellSize);
|
|
4350
|
+
const maxCx = Math.floor((x + radius) / this.cellSize);
|
|
4351
|
+
const minCy = Math.floor((y - radius) / this.cellSize);
|
|
4352
|
+
const maxCy = Math.floor((y + radius) / this.cellSize);
|
|
4353
|
+
let count = 0;
|
|
4354
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4355
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4356
|
+
if (!cell) continue;
|
|
4357
|
+
for (const p of cell){
|
|
4358
|
+
const dx = x - p.x;
|
|
4359
|
+
const dy = y - p.y;
|
|
4360
|
+
if (dx * dx + dy * dy < r2) count++;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
return count;
|
|
4364
|
+
}
|
|
4365
|
+
/** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
|
|
4366
|
+
const minCx = Math.floor((x - searchRadius) / this.cellSize);
|
|
4367
|
+
const maxCx = Math.floor((x + searchRadius) / this.cellSize);
|
|
4368
|
+
const minCy = Math.floor((y - searchRadius) / this.cellSize);
|
|
4369
|
+
const maxCy = Math.floor((y + searchRadius) / this.cellSize);
|
|
4370
|
+
let nearest = null;
|
|
4371
|
+
let bestDist2 = Infinity;
|
|
4372
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4373
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4374
|
+
if (!cell) continue;
|
|
4375
|
+
for (const p of cell){
|
|
4376
|
+
const dx = x - p.x;
|
|
4377
|
+
const dy = y - p.y;
|
|
4378
|
+
const d2 = dx * dx + dy * dy;
|
|
4379
|
+
if (d2 > 0 && d2 < bestDist2) {
|
|
4380
|
+
bestDist2 = d2;
|
|
4381
|
+
nearest = p;
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
return nearest;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
// ── Helper: density check (legacy wrapper) ──────────────────────────
|
|
4076
4389
|
function $b623126c6e9cbb71$var$localDensity(x, y, positions, radius) {
|
|
4077
4390
|
let count = 0;
|
|
4078
4391
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4371,42 +4684,43 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4371
4684
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4372
4685
|
const patternColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4373
4686
|
if (bgPatternRoll < 0.2) {
|
|
4374
|
-
// Dot grid
|
|
4687
|
+
// Dot grid — batched into a single path
|
|
4375
4688
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4376
4689
|
const dotR = dotSpacing * 0.08;
|
|
4377
4690
|
ctx.globalAlpha = patternOpacity;
|
|
4378
4691
|
ctx.fillStyle = patternColor;
|
|
4692
|
+
ctx.beginPath();
|
|
4379
4693
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4380
|
-
ctx.
|
|
4694
|
+
ctx.moveTo(px + dotR, py);
|
|
4381
4695
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4382
|
-
ctx.fill();
|
|
4383
4696
|
}
|
|
4697
|
+
ctx.fill();
|
|
4384
4698
|
} else if (bgPatternRoll < 0.4) {
|
|
4385
|
-
// Diagonal lines
|
|
4699
|
+
// Diagonal lines — batched into a single path
|
|
4386
4700
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4387
4701
|
ctx.globalAlpha = patternOpacity;
|
|
4388
4702
|
ctx.strokeStyle = patternColor;
|
|
4389
4703
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4390
4704
|
const diag = Math.hypot(width, height);
|
|
4705
|
+
ctx.beginPath();
|
|
4391
4706
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4392
|
-
ctx.beginPath();
|
|
4393
4707
|
ctx.moveTo(d, 0);
|
|
4394
4708
|
ctx.lineTo(d + height, height);
|
|
4395
|
-
ctx.stroke();
|
|
4396
4709
|
}
|
|
4710
|
+
ctx.stroke();
|
|
4397
4711
|
} else {
|
|
4398
|
-
// Tessellation — hexagonal grid
|
|
4712
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4399
4713
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4400
4714
|
const tessH = tessSize * Math.sqrt(3);
|
|
4401
4715
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4402
4716
|
ctx.strokeStyle = patternColor;
|
|
4403
4717
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4718
|
+
ctx.beginPath();
|
|
4404
4719
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4405
4720
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4406
4721
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4407
4722
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4408
4723
|
const hy = row * tessH;
|
|
4409
|
-
ctx.beginPath();
|
|
4410
4724
|
for(let s = 0; s < 6; s++){
|
|
4411
4725
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4412
4726
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4415,18 +4729,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4415
4729
|
else ctx.lineTo(vx, vy);
|
|
4416
4730
|
}
|
|
4417
4731
|
ctx.closePath();
|
|
4418
|
-
ctx.stroke();
|
|
4419
4732
|
}
|
|
4420
4733
|
}
|
|
4734
|
+
ctx.stroke();
|
|
4421
4735
|
}
|
|
4422
4736
|
ctx.restore();
|
|
4423
4737
|
}
|
|
4424
4738
|
ctx.globalCompositeOperation = "source-over";
|
|
4425
|
-
// ── 2. Composition mode
|
|
4426
|
-
const compositionMode = $b623126c6e9cbb71$var$
|
|
4739
|
+
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4740
|
+
const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES.length)];
|
|
4427
4741
|
const symRoll = rng();
|
|
4428
4742
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4429
|
-
// ── 3. Focal points + void zones
|
|
4743
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4430
4744
|
const THIRDS_POINTS = [
|
|
4431
4745
|
{
|
|
4432
4746
|
x: 1 / 3,
|
|
@@ -4459,9 +4773,23 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4459
4773
|
y: height * (0.2 + rng() * 0.6),
|
|
4460
4774
|
strength: 0.3 + rng() * 0.4
|
|
4461
4775
|
});
|
|
4462
|
-
|
|
4776
|
+
// Archetype-aware void zones: dense archetypes get fewer/no voids,
|
|
4777
|
+
// minimal archetypes get golden-ratio positioned voids
|
|
4778
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4779
|
+
const isMinimalArchetype = archetype.gridSize <= 3;
|
|
4780
|
+
const isDenseArchetype = archetype.gridSize >= 8;
|
|
4781
|
+
const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
|
|
4463
4782
|
const voidZones = [];
|
|
4464
|
-
for(let v = 0; v < numVoids; v++)
|
|
4783
|
+
for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
|
|
4784
|
+
// Place voids at golden-ratio positions for intentional negative space
|
|
4785
|
+
const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
|
|
4786
|
+
const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
|
|
4787
|
+
voidZones.push({
|
|
4788
|
+
x: width * (gx + (rng() - 0.5) * 0.05),
|
|
4789
|
+
y: height * (gy + (rng() - 0.5) * 0.05),
|
|
4790
|
+
radius: Math.min(width, height) * (0.08 + rng() * 0.08)
|
|
4791
|
+
});
|
|
4792
|
+
} else voidZones.push({
|
|
4465
4793
|
x: width * (0.15 + rng() * 0.7),
|
|
4466
4794
|
y: height * (0.15 + rng() * 0.7),
|
|
4467
4795
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4491,19 +4819,20 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4491
4819
|
ctx.beginPath();
|
|
4492
4820
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
4493
4821
|
ctx.stroke();
|
|
4494
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
4822
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
4495
4823
|
if (rng() < 0.5) {
|
|
4496
4824
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
4497
4825
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
4498
4826
|
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
4827
|
+
ctx.beginPath();
|
|
4499
4828
|
for(let d = 0; d < dotCount; d++){
|
|
4500
4829
|
const angle = rng() * Math.PI * 2;
|
|
4501
4830
|
const dist = rng() * zone.radius * 0.7;
|
|
4502
4831
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
4503
|
-
ctx.
|
|
4832
|
+
ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
|
|
4504
4833
|
ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
|
|
4505
|
-
ctx.fill();
|
|
4506
4834
|
}
|
|
4835
|
+
ctx.fill();
|
|
4507
4836
|
}
|
|
4508
4837
|
// ~30% chance: thin concentric ring inside
|
|
4509
4838
|
if (rng() < 0.3) {
|
|
@@ -4538,6 +4867,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4538
4867
|
}
|
|
4539
4868
|
// Track all placed shapes for density checks and connecting curves
|
|
4540
4869
|
const shapePositions = [];
|
|
4870
|
+
// Spatial grid for O(1) density and nearest-neighbor lookups
|
|
4871
|
+
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4872
|
+
const spatialGrid = new $b623126c6e9cbb71$var$SpatialGrid(densityCheckRadius);
|
|
4541
4873
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4542
4874
|
let heroCenter = null;
|
|
4543
4875
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4583,10 +4915,35 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4583
4915
|
size: heroSize,
|
|
4584
4916
|
shape: heroShape
|
|
4585
4917
|
});
|
|
4918
|
+
spatialGrid.insert({
|
|
4919
|
+
x: heroFocal.x,
|
|
4920
|
+
y: heroFocal.y,
|
|
4921
|
+
size: heroSize,
|
|
4922
|
+
shape: heroShape
|
|
4923
|
+
});
|
|
4586
4924
|
}
|
|
4587
4925
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4588
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4589
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;
|
|
4590
4947
|
for(let layer = 0; layer < layers; layer++){
|
|
4591
4948
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
4592
4949
|
const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
|
|
@@ -4624,7 +4981,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4624
4981
|
if ($b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones)) {
|
|
4625
4982
|
if (rng() < 0.85) continue;
|
|
4626
4983
|
}
|
|
4627
|
-
if (
|
|
4984
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4628
4985
|
if (rng() < 0.6) continue;
|
|
4629
4986
|
}
|
|
4630
4987
|
// Power distribution for size — archetype controls the curve
|
|
@@ -4675,7 +5032,26 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4675
5032
|
const shapeRenderStyle = (0, $24064302523652b1$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
|
|
4676
5033
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
4677
5034
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
4678
|
-
|
|
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++;
|
|
4679
5055
|
// Consistent light direction — subtle shadow offset
|
|
4680
5056
|
const shadowDist = hasGlow ? 0 : size * 0.02;
|
|
4681
5057
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
@@ -4684,17 +5060,11 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4684
5060
|
let finalX = x;
|
|
4685
5061
|
let finalY = y;
|
|
4686
5062
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4687
|
-
//
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
for (const sp of shapePositions){
|
|
4691
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4692
|
-
if (d < nearestDist && d > 0) {
|
|
4693
|
-
nearestDist = d;
|
|
4694
|
-
nearestPos = sp;
|
|
4695
|
-
}
|
|
4696
|
-
}
|
|
5063
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
5064
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
5065
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4697
5066
|
if (nearestPos) {
|
|
5067
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4698
5068
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4699
5069
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4700
5070
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4736,30 +5106,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4736
5106
|
lightAngle: lightAngle,
|
|
4737
5107
|
scaleFactor: scaleFactor
|
|
4738
5108
|
};
|
|
4739
|
-
if (shouldMirror)
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
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;
|
|
4745
5122
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
4746
5123
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
4747
5124
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
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;
|
|
4762
5142
|
}
|
|
5143
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
4763
5144
|
}
|
|
4764
5145
|
shapePositions.push({
|
|
4765
5146
|
x: finalX,
|
|
@@ -4767,35 +5148,51 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4767
5148
|
size: size,
|
|
4768
5149
|
shape: shape
|
|
4769
5150
|
});
|
|
5151
|
+
spatialGrid.insert({
|
|
5152
|
+
x: finalX,
|
|
5153
|
+
y: finalY,
|
|
5154
|
+
size: size,
|
|
5155
|
+
shape: shape
|
|
5156
|
+
});
|
|
4770
5157
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4771
5158
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4772
5159
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
4773
5160
|
const echoAngle = rng() * Math.PI * 2;
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
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;
|
|
4798
5194
|
}
|
|
5195
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
4799
5196
|
}
|
|
4800
5197
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
4801
5198
|
// Focal depth: shapes near focal points get more detail
|
|
@@ -4803,7 +5200,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4803
5200
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
4804
5201
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
4805
5202
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
4806
|
-
for(let n = 0; n < innerCount; n++){
|
|
5203
|
+
if (extrasAllowed) for(let n = 0; n < innerCount; n++){
|
|
4807
5204
|
// Pick inner shape from palette affinities
|
|
4808
5205
|
const innerSizeFraction = size * 0.25 / adjustedMaxSize;
|
|
4809
5206
|
const innerShape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
|
|
@@ -4812,6 +5209,10 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4812
5209
|
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
4813
5210
|
const innerRot = rng() * 360;
|
|
4814
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++;
|
|
4815
5216
|
ctx.globalAlpha = layerOpacity * 0.7;
|
|
4816
5217
|
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
|
|
4817
5218
|
fillColor: innerFill,
|
|
@@ -4820,9 +5221,21 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4820
5221
|
size: innerSize,
|
|
4821
5222
|
rotation: innerRot,
|
|
4822
5223
|
proportionType: "GOLDEN_RATIO",
|
|
4823
|
-
renderStyle:
|
|
5224
|
+
renderStyle: innerStyle,
|
|
4824
5225
|
rng: rng
|
|
4825
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();
|
|
4826
5239
|
}
|
|
4827
5240
|
}
|
|
4828
5241
|
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
@@ -4831,41 +5244,113 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4831
5244
|
const constellation = $b623126c6e9cbb71$var$CONSTELLATIONS[Math.floor(rng() * $b623126c6e9cbb71$var$CONSTELLATIONS.length)];
|
|
4832
5245
|
const members = constellation.build(rng, size);
|
|
4833
5246
|
const groupRotation = rng() * Math.PI * 2;
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
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();
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
5299
|
+
// ~12% of medium-large shapes spawn a rhythmic sequence
|
|
5300
|
+
if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
|
|
5301
|
+
const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
|
|
5302
|
+
const rhythmAngle = rng() * Math.PI * 2;
|
|
5303
|
+
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5304
|
+
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5305
|
+
const rhythmShape = shape; // same shape for visual rhythm
|
|
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
|
|
5343
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5344
|
+
rng();
|
|
5345
|
+
rng();
|
|
5346
|
+
rng();
|
|
4862
5347
|
}
|
|
4863
5348
|
}
|
|
4864
5349
|
}
|
|
4865
5350
|
}
|
|
4866
5351
|
// Reset blend mode for post-processing passes
|
|
4867
5352
|
ctx.globalCompositeOperation = "source-over";
|
|
4868
|
-
// ──
|
|
5353
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
4869
5354
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
4870
5355
|
// with a tinted background wash, creating a "peek through" effect.
|
|
4871
5356
|
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
@@ -4924,14 +5409,26 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4924
5409
|
}
|
|
4925
5410
|
}
|
|
4926
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).
|
|
4927
5415
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
4928
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;
|
|
4929
5425
|
for(let i = 0; i < numFlowLines; i++){
|
|
4930
5426
|
let fx = rng() * width;
|
|
4931
5427
|
let fy = rng() * height;
|
|
4932
5428
|
const steps = 30 + Math.floor(rng() * 40);
|
|
4933
5429
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
4934
5430
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
5431
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
4935
5432
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
4936
5433
|
const lineColorStart = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
4937
5434
|
const lineColorEnd = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
@@ -4946,20 +5443,29 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4946
5443
|
fx += Math.cos(angle) * stepLen;
|
|
4947
5444
|
fy += Math.sin(angle) * stepLen;
|
|
4948
5445
|
if (fx < 0 || fx > width || fy < 0 || fy > height) break;
|
|
5446
|
+
// Skip segments that pass through void zones
|
|
5447
|
+
if ($b623126c6e9cbb71$var$isInVoidZone(fx, fy, voidZones)) {
|
|
5448
|
+
prevX = fx;
|
|
5449
|
+
prevY = fy;
|
|
5450
|
+
continue;
|
|
5451
|
+
}
|
|
4949
5452
|
const t = s / steps;
|
|
4950
|
-
// Taper + pressure
|
|
4951
5453
|
const taper = 1 - t * 0.8;
|
|
4952
5454
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
4953
|
-
|
|
4954
|
-
|
|
5455
|
+
const segWidth = startWidth * taper * pressure;
|
|
5456
|
+
const segAlpha = lineAlpha * taper;
|
|
4955
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);
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
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;
|
|
4963
5469
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
4964
5470
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
4965
5471
|
const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
|
|
@@ -4975,12 +5481,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4975
5481
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
4976
5482
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
4977
5483
|
const bTaper = 1 - bs / branchSteps * 0.9;
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
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;
|
|
4984
5496
|
bPrevX = bx;
|
|
4985
5497
|
bPrevY = by;
|
|
4986
5498
|
}
|
|
@@ -4989,7 +5501,40 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4989
5501
|
prevY = fy;
|
|
4990
5502
|
}
|
|
4991
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
|
+
}
|
|
4992
5536
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
5537
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
4993
5538
|
const energyArchetypes = [
|
|
4994
5539
|
"dense-chaotic",
|
|
4995
5540
|
"cosmic",
|
|
@@ -5000,8 +5545,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5000
5545
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
5001
5546
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
5002
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);
|
|
5003
5553
|
for(let e = 0; e < energyCount; e++){
|
|
5004
|
-
// Pick a random shape to radiate from
|
|
5005
5554
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5006
5555
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
5007
5556
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -5013,14 +5562,37 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5013
5562
|
const sy = source.y + Math.sin(angle) * startDist;
|
|
5014
5563
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
5015
5564
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
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;
|
|
5579
|
+
}
|
|
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);
|
|
5023
5594
|
}
|
|
5595
|
+
ctx.stroke();
|
|
5024
5596
|
}
|
|
5025
5597
|
}
|
|
5026
5598
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
@@ -5043,50 +5615,128 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5043
5615
|
}
|
|
5044
5616
|
ctx.restore();
|
|
5045
5617
|
}
|
|
5046
|
-
// ── 7. Noise texture overlay
|
|
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.
|
|
5047
5621
|
const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
|
|
5048
|
-
const
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
const
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
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);
|
|
5626
|
+
try {
|
|
5627
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5628
|
+
const data = imageData.data;
|
|
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)
|
|
5632
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5633
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5634
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5635
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
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;
|
|
5652
|
+
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
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;
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
ctx.putImageData(imageData, 0, 0);
|
|
5660
|
+
} catch {
|
|
5661
|
+
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5662
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5663
|
+
const nx = noiseRng() * width;
|
|
5664
|
+
const ny = noiseRng() * height;
|
|
5665
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5666
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5667
|
+
ctx.globalAlpha = alpha;
|
|
5668
|
+
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5669
|
+
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5670
|
+
}
|
|
5057
5671
|
}
|
|
5058
5672
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
5059
5673
|
ctx.globalAlpha = 1;
|
|
5060
5674
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
5061
5675
|
const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
|
|
5676
|
+
// Tint vignette based on background: warm sepia for light, cool blue for dark
|
|
5677
|
+
const isLightBg = bgLum > 0.5;
|
|
5678
|
+
const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
|
|
5679
|
+
: `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
|
|
5062
5680
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
5063
5681
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
5064
|
-
vigGrad.addColorStop(1,
|
|
5682
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
5065
5683
|
ctx.fillStyle = vigGrad;
|
|
5066
5684
|
ctx.fillRect(0, 0, width, height);
|
|
5067
|
-
// ── 9. Organic connecting curves
|
|
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).
|
|
5068
5688
|
if (shapePositions.length > 1) {
|
|
5069
5689
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5690
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5070
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([]);
|
|
5071
5698
|
for(let i = 0; i < numCurves; i++){
|
|
5072
5699
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
5073
5700
|
const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
|
|
5074
5701
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
5075
5702
|
const a = shapePositions[idxA];
|
|
5076
5703
|
const b = shapePositions[idxB];
|
|
5077
|
-
const mx = (a.x + b.x) / 2;
|
|
5078
|
-
const my = (a.y + b.y) / 2;
|
|
5079
5704
|
const dx = b.x - a.x;
|
|
5080
5705
|
const dy = b.y - a.y;
|
|
5081
5706
|
const dist = Math.hypot(dx, dy);
|
|
5707
|
+
// Skip connections between distant shapes
|
|
5708
|
+
if (dist > maxCurveDist) continue;
|
|
5709
|
+
const mx = (a.x + b.x) / 2;
|
|
5710
|
+
const my = (a.y + b.y) / 2;
|
|
5082
5711
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5083
5712
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5084
5713
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
5085
|
-
|
|
5086
|
-
|
|
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];
|
|
5087
5734
|
ctx.beginPath();
|
|
5088
|
-
|
|
5089
|
-
|
|
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
|
+
}
|
|
5090
5740
|
ctx.stroke();
|
|
5091
5741
|
}
|
|
5092
5742
|
}
|
|
@@ -5204,11 +5854,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5204
5854
|
}
|
|
5205
5855
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5206
5856
|
// Vine tendrils — organic curving lines along edges
|
|
5857
|
+
// Optimized: batch all tendrils into a single path
|
|
5207
5858
|
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5208
5859
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5209
5860
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5210
5861
|
ctx.lineCap = "round";
|
|
5211
5862
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5863
|
+
ctx.beginPath();
|
|
5864
|
+
const leafPositions = [];
|
|
5212
5865
|
for(let t = 0; t < tendrilCount; t++){
|
|
5213
5866
|
// Start from a random edge point
|
|
5214
5867
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -5226,7 +5879,6 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5226
5879
|
tx = width - borderPad;
|
|
5227
5880
|
ty = borderRng() * height;
|
|
5228
5881
|
}
|
|
5229
|
-
ctx.beginPath();
|
|
5230
5882
|
ctx.moveTo(tx, ty);
|
|
5231
5883
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5232
5884
|
for(let s = 0; s < segs; s++){
|
|
@@ -5240,14 +5892,23 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5240
5892
|
ty = cpy3;
|
|
5241
5893
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5242
5894
|
}
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
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);
|
|
5250
5910
|
}
|
|
5911
|
+
ctx.fill();
|
|
5251
5912
|
}
|
|
5252
5913
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5253
5914
|
// Star-studded arcs along edges
|
|
@@ -5262,8 +5923,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5262
5923
|
ctx.beginPath();
|
|
5263
5924
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5264
5925
|
ctx.stroke();
|
|
5265
|
-
// Scatter small stars along the border region
|
|
5926
|
+
// Scatter small stars along the border region — batched into single path
|
|
5266
5927
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5928
|
+
ctx.beginPath();
|
|
5267
5929
|
for(let s = 0; s < starCount; s++){
|
|
5268
5930
|
const edge = Math.floor(borderRng() * 4);
|
|
5269
5931
|
let sx, sy;
|
|
@@ -5282,7 +5944,6 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5282
5944
|
}
|
|
5283
5945
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5284
5946
|
// 4-point star
|
|
5285
|
-
ctx.beginPath();
|
|
5286
5947
|
for(let p = 0; p < 8; p++){
|
|
5287
5948
|
const a = p / 8 * Math.PI * 2;
|
|
5288
5949
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -5292,8 +5953,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5292
5953
|
else ctx.lineTo(px2, py2);
|
|
5293
5954
|
}
|
|
5294
5955
|
ctx.closePath();
|
|
5295
|
-
ctx.fill();
|
|
5296
5956
|
}
|
|
5957
|
+
ctx.fill();
|
|
5297
5958
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5298
5959
|
// Thin single rule — understated elegance
|
|
5299
5960
|
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
@@ -5304,13 +5965,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5304
5965
|
// Other archetypes: no border (intentional — not every image needs one)
|
|
5305
5966
|
ctx.restore();
|
|
5306
5967
|
}
|
|
5307
|
-
// ── 11. Signature mark —
|
|
5968
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
5308
5969
|
{
|
|
5309
5970
|
const sigRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
|
|
5310
5971
|
const sigSize = Math.min(width, height) * 0.025;
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
const
|
|
5972
|
+
const sigMargin = sigSize * 2.5;
|
|
5973
|
+
// Find the corner with the lowest local density
|
|
5974
|
+
const cornerCandidates = [
|
|
5975
|
+
{
|
|
5976
|
+
x: sigMargin,
|
|
5977
|
+
y: sigMargin
|
|
5978
|
+
},
|
|
5979
|
+
{
|
|
5980
|
+
x: width - sigMargin,
|
|
5981
|
+
y: sigMargin
|
|
5982
|
+
},
|
|
5983
|
+
{
|
|
5984
|
+
x: sigMargin,
|
|
5985
|
+
y: height - sigMargin
|
|
5986
|
+
},
|
|
5987
|
+
{
|
|
5988
|
+
x: width - sigMargin,
|
|
5989
|
+
y: height - sigMargin
|
|
5990
|
+
}
|
|
5991
|
+
];
|
|
5992
|
+
let bestCorner = cornerCandidates[3]; // default: bottom-right
|
|
5993
|
+
let minDensity = Infinity;
|
|
5994
|
+
for (const corner of cornerCandidates){
|
|
5995
|
+
const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
|
|
5996
|
+
if (density < minDensity) {
|
|
5997
|
+
minDensity = density;
|
|
5998
|
+
bestCorner = corner;
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
const sigX = bestCorner.x;
|
|
6002
|
+
const sigY = bestCorner.y;
|
|
5314
6003
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
5315
6004
|
const sigColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
5316
6005
|
ctx.save();
|