git-hash-art 0.11.0 → 0.13.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 +17 -1
- package/dist/browser.js +707 -324
- package/dist/browser.js.map +1 -1
- package/dist/main.js +707 -324
- package/dist/main.js.map +1 -1
- package/dist/module.js +707 -324
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/performance.test.ts +243 -0
- package/src/__tests__/phase-breakdown.test.ts +44 -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 +521 -244
- package/src/types.ts +2 -0
package/dist/browser.js
CHANGED
|
@@ -508,13 +508,21 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
// ── Standalone color utilities ──────────────────────────────────────
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
511
|
+
// ── Cached hex→RGB parse — avoids repeated parseInt/substring on hot path ──
|
|
512
|
+
const $b5a262d09b87e373$var$_rgbCache = new Map();
|
|
513
|
+
const $b5a262d09b87e373$var$_RGB_CACHE_MAX = 512;
|
|
514
|
+
/** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. Cached. */ function $b5a262d09b87e373$var$hexToRgb(hex) {
|
|
515
|
+
let cached = $b5a262d09b87e373$var$_rgbCache.get(hex);
|
|
516
|
+
if (cached) return cached;
|
|
517
|
+
const c = hex.charAt(0) === "#" ? hex.substring(1) : hex;
|
|
518
|
+
cached = [
|
|
514
519
|
parseInt(c.substring(0, 2), 16),
|
|
515
520
|
parseInt(c.substring(2, 4), 16),
|
|
516
521
|
parseInt(c.substring(4, 6), 16)
|
|
517
522
|
];
|
|
523
|
+
if ($b5a262d09b87e373$var$_rgbCache.size >= $b5a262d09b87e373$var$_RGB_CACHE_MAX) $b5a262d09b87e373$var$_rgbCache.clear();
|
|
524
|
+
$b5a262d09b87e373$var$_rgbCache.set(hex, cached);
|
|
525
|
+
return cached;
|
|
518
526
|
}
|
|
519
527
|
/** Format [r, g, b] back to #RRGGBB. */ function $b5a262d09b87e373$var$rgbToHex(r, g, b) {
|
|
520
528
|
const clamp = (v)=>Math.max(0, Math.min(255, Math.round(v)));
|
|
@@ -571,7 +579,9 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
571
579
|
}
|
|
572
580
|
function $b5a262d09b87e373$export$f2121afcad3d553f(hex, alpha) {
|
|
573
581
|
const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex);
|
|
574
|
-
|
|
582
|
+
// Quantize alpha to 3 decimal places without toFixed overhead
|
|
583
|
+
const a = Math.round(alpha * 1000) / 1000;
|
|
584
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
575
585
|
}
|
|
576
586
|
function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
|
|
577
587
|
if (colors.length < 3) return {
|
|
@@ -651,12 +661,21 @@ function $b5a262d09b87e373$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
|
651
661
|
const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
|
|
652
662
|
return $b5a262d09b87e373$var$hslToHex($b5a262d09b87e373$var$shiftHueToward(h, target, amount), s, l);
|
|
653
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Compute relative luminance of a hex color (0 = black, 1 = white).
|
|
666
|
+
* Uses the sRGB luminance formula from WCAG. Cached.
|
|
667
|
+
*/ const $b5a262d09b87e373$var$_lumCache = new Map();
|
|
654
668
|
function $b5a262d09b87e373$export$5c6e3c2b59b7fbbe(hex) {
|
|
669
|
+
let cached = $b5a262d09b87e373$var$_lumCache.get(hex);
|
|
670
|
+
if (cached !== undefined) return cached;
|
|
655
671
|
const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex).map((c)=>{
|
|
656
672
|
const s = c / 255;
|
|
657
673
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
658
674
|
});
|
|
659
|
-
|
|
675
|
+
cached = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
676
|
+
if ($b5a262d09b87e373$var$_lumCache.size >= 512) $b5a262d09b87e373$var$_lumCache.clear();
|
|
677
|
+
$b5a262d09b87e373$var$_lumCache.set(hex, cached);
|
|
678
|
+
return cached;
|
|
660
679
|
}
|
|
661
680
|
function $b5a262d09b87e373$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
|
|
662
681
|
const fgLum = $b5a262d09b87e373$export$5c6e3c2b59b7fbbe(fgHex);
|
|
@@ -1092,21 +1111,31 @@ const $f0f1a7293548e501$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
|
|
|
1092
1111
|
(0, $ce2c52df8af02e62$export$e46c5570db033611)(ctx, size, finalConfig);
|
|
1093
1112
|
const gridSize = 8;
|
|
1094
1113
|
const unit = size / gridSize;
|
|
1114
|
+
const radius = unit / 2;
|
|
1115
|
+
// Pre-compute the 8 star-point angle pairs (cos/sin) — avoids 648 trig calls
|
|
1116
|
+
const starPoints = [];
|
|
1117
|
+
for(let k = 0; k < 8; k++){
|
|
1118
|
+
const angle = Math.PI / 4 * k;
|
|
1119
|
+
const angle2 = angle + Math.PI / 4;
|
|
1120
|
+
starPoints.push({
|
|
1121
|
+
c1: Math.cos(angle) * radius,
|
|
1122
|
+
s1: Math.sin(angle) * radius,
|
|
1123
|
+
c2: Math.cos(angle2) * radius,
|
|
1124
|
+
s2: Math.sin(angle2) * radius
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1095
1127
|
ctx.beginPath();
|
|
1096
1128
|
// Create base grid
|
|
1097
|
-
for(let i = 0; i <= gridSize; i++)
|
|
1129
|
+
for(let i = 0; i <= gridSize; i++){
|
|
1098
1130
|
const x = (i - gridSize / 2) * unit;
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const y2 = y + radius * Math.sin(angle + Math.PI / 4);
|
|
1108
|
-
ctx.moveTo(x1, y1);
|
|
1109
|
-
ctx.lineTo(x2, y2);
|
|
1131
|
+
for(let j = 0; j <= gridSize; j++){
|
|
1132
|
+
const y = (j - gridSize / 2) * unit;
|
|
1133
|
+
// Draw star pattern at each intersection using pre-computed offsets
|
|
1134
|
+
for(let k = 0; k < 8; k++){
|
|
1135
|
+
const sp = starPoints[k];
|
|
1136
|
+
ctx.moveTo(x + sp.c1, y + sp.s1);
|
|
1137
|
+
ctx.lineTo(x + sp.c2, y + sp.s2);
|
|
1138
|
+
}
|
|
1110
1139
|
}
|
|
1111
1140
|
}
|
|
1112
1141
|
ctx.stroke();
|
|
@@ -1426,20 +1455,23 @@ const $77711f013715e6da$export$eeae7765f05012e2 = (ctx, size)=>{
|
|
|
1426
1455
|
const $77711f013715e6da$export$3355220a8108efc3 = (ctx, size)=>{
|
|
1427
1456
|
const outerRadius = size / 2;
|
|
1428
1457
|
const innerRadius = size / 4;
|
|
1429
|
-
|
|
1458
|
+
// Adaptive step count: fewer segments for small shapes where detail isn't visible.
|
|
1459
|
+
// 36×36 = 1296 segments at full size; at size < 60 we drop to 16×16 = 256.
|
|
1460
|
+
const steps = size < 60 ? 16 : size < 150 ? 24 : 36;
|
|
1461
|
+
const TWO_PI = Math.PI * 2;
|
|
1462
|
+
const angleStep = TWO_PI / steps;
|
|
1430
1463
|
ctx.beginPath();
|
|
1431
1464
|
for(let i = 0; i < steps; i++){
|
|
1432
|
-
const angle1 = i
|
|
1433
|
-
|
|
1465
|
+
const angle1 = i * angleStep;
|
|
1466
|
+
const cosA = Math.cos(angle1);
|
|
1467
|
+
const sinA = Math.sin(angle1);
|
|
1434
1468
|
for(let j = 0; j < steps; j++){
|
|
1435
|
-
const phi1 = j
|
|
1436
|
-
const phi2 =
|
|
1437
|
-
const
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
ctx.moveTo(x1, y1);
|
|
1442
|
-
ctx.lineTo(x2, y2);
|
|
1469
|
+
const phi1 = j * angleStep;
|
|
1470
|
+
const phi2 = phi1 + angleStep;
|
|
1471
|
+
const r1 = outerRadius + innerRadius * Math.cos(phi1);
|
|
1472
|
+
const r2 = outerRadius + innerRadius * Math.cos(phi2);
|
|
1473
|
+
ctx.moveTo(r1 * cosA, r1 * sinA);
|
|
1474
|
+
ctx.lineTo(r2 * cosA, r2 * sinA);
|
|
1443
1475
|
}
|
|
1444
1476
|
}
|
|
1445
1477
|
};
|
|
@@ -2002,6 +2034,43 @@ const $e0f99502ff383dd8$var$RENDER_STYLES = [
|
|
|
2002
2034
|
function $e0f99502ff383dd8$export$9fd4e64b2acd410e(rng) {
|
|
2003
2035
|
return $e0f99502ff383dd8$var$RENDER_STYLES[Math.floor(rng() * $e0f99502ff383dd8$var$RENDER_STYLES.length)];
|
|
2004
2036
|
}
|
|
2037
|
+
const $e0f99502ff383dd8$export$2f738f61a8c15e07 = {
|
|
2038
|
+
"fill-and-stroke": 1,
|
|
2039
|
+
"fill-only": 0.5,
|
|
2040
|
+
"stroke-only": 1,
|
|
2041
|
+
"double-stroke": 1.5,
|
|
2042
|
+
"dashed": 1,
|
|
2043
|
+
"watercolor": 7,
|
|
2044
|
+
"hatched": 3,
|
|
2045
|
+
"incomplete": 1,
|
|
2046
|
+
"stipple": 90,
|
|
2047
|
+
"stencil": 2,
|
|
2048
|
+
"noise-grain": 400,
|
|
2049
|
+
"wood-grain": 10,
|
|
2050
|
+
"marble-vein": 4,
|
|
2051
|
+
"fabric-weave": 6,
|
|
2052
|
+
"hand-drawn": 5
|
|
2053
|
+
};
|
|
2054
|
+
function $e0f99502ff383dd8$export$909ab0580e273f19(style) {
|
|
2055
|
+
switch(style){
|
|
2056
|
+
case "noise-grain":
|
|
2057
|
+
return "hatched";
|
|
2058
|
+
case "stipple":
|
|
2059
|
+
return "dashed";
|
|
2060
|
+
case "wood-grain":
|
|
2061
|
+
return "hatched";
|
|
2062
|
+
case "watercolor":
|
|
2063
|
+
return "fill-and-stroke";
|
|
2064
|
+
case "fabric-weave":
|
|
2065
|
+
return "hatched";
|
|
2066
|
+
case "hand-drawn":
|
|
2067
|
+
return "fill-and-stroke";
|
|
2068
|
+
case "marble-vein":
|
|
2069
|
+
return "stroke-only";
|
|
2070
|
+
default:
|
|
2071
|
+
return style;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2005
2074
|
function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
2006
2075
|
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
|
|
2007
2076
|
ctx.save();
|
|
@@ -2121,6 +2190,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2121
2190
|
case "hatched":
|
|
2122
2191
|
{
|
|
2123
2192
|
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
2193
|
+
// Optimized: batch all parallel lines into a single path per pass
|
|
2124
2194
|
const savedAlphaH = ctx.globalAlpha;
|
|
2125
2195
|
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
2126
2196
|
ctx.fill();
|
|
@@ -2132,28 +2202,28 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2132
2202
|
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
2133
2203
|
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
2134
2204
|
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
2135
|
-
// Draw parallel lines across the bounding box
|
|
2205
|
+
// Draw parallel lines across the bounding box — batched into single path
|
|
2136
2206
|
const extent = size * 0.8;
|
|
2137
2207
|
const cos = Math.cos(hatchAngle);
|
|
2138
2208
|
const sin = Math.sin(hatchAngle);
|
|
2209
|
+
ctx.beginPath();
|
|
2139
2210
|
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
2140
|
-
ctx.beginPath();
|
|
2141
2211
|
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
2142
2212
|
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
2143
|
-
ctx.stroke();
|
|
2144
2213
|
}
|
|
2214
|
+
ctx.stroke();
|
|
2145
2215
|
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
2146
2216
|
if (!rng || rng() < 0.5) {
|
|
2147
2217
|
const crossAngle = hatchAngle + Math.PI / 2;
|
|
2148
2218
|
const cos2 = Math.cos(crossAngle);
|
|
2149
2219
|
const sin2 = Math.sin(crossAngle);
|
|
2150
2220
|
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
2221
|
+
ctx.beginPath();
|
|
2151
2222
|
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
2152
|
-
ctx.beginPath();
|
|
2153
2223
|
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
2154
2224
|
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
2155
|
-
ctx.stroke();
|
|
2156
2225
|
}
|
|
2226
|
+
ctx.stroke();
|
|
2157
2227
|
}
|
|
2158
2228
|
ctx.restore();
|
|
2159
2229
|
ctx.globalAlpha = savedAlphaH;
|
|
@@ -2191,6 +2261,8 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2191
2261
|
case "stipple":
|
|
2192
2262
|
{
|
|
2193
2263
|
// Dot-fill texture — clip to shape, then scatter dots
|
|
2264
|
+
// Optimized: use fillRect instead of arc for dots (much cheaper to render),
|
|
2265
|
+
// and cap total dot count to avoid O(size²) blowup on large shapes.
|
|
2194
2266
|
const savedAlphaS = ctx.globalAlpha;
|
|
2195
2267
|
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
2196
2268
|
ctx.fill(); // ghost fill
|
|
@@ -2198,16 +2270,20 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2198
2270
|
ctx.save();
|
|
2199
2271
|
ctx.clip();
|
|
2200
2272
|
const dotSpacing = Math.max(2, size * 0.03);
|
|
2201
|
-
const
|
|
2273
|
+
const extentS = size * 0.55;
|
|
2274
|
+
// Cap total dots: beyond ~900 (30×30 grid) the visual density plateaus
|
|
2275
|
+
const maxDotsPerAxis = Math.min(Math.ceil(extentS * 2 / dotSpacing), 30);
|
|
2276
|
+
const actualSpacing = extentS * 2 / maxDotsPerAxis;
|
|
2202
2277
|
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
2203
|
-
for(let
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2278
|
+
for(let xi = 0; xi < maxDotsPerAxis; xi++){
|
|
2279
|
+
const dx = -extentS + xi * actualSpacing;
|
|
2280
|
+
for(let yi = 0; yi < maxDotsPerAxis; yi++){
|
|
2281
|
+
const dy = -extentS + yi * actualSpacing;
|
|
2282
|
+
const jx = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2283
|
+
const jy = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2284
|
+
const dotD = rng ? actualSpacing * (0.3 + rng() * 0.4) : actualSpacing * 0.4;
|
|
2285
|
+
ctx.fillRect(dx + jx - dotD * 0.5, dy + jy - dotD * 0.5, dotD, dotD);
|
|
2286
|
+
}
|
|
2211
2287
|
}
|
|
2212
2288
|
ctx.restore();
|
|
2213
2289
|
ctx.globalAlpha = savedAlphaS;
|
|
@@ -2240,6 +2316,9 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2240
2316
|
case "noise-grain":
|
|
2241
2317
|
{
|
|
2242
2318
|
// Procedural noise grain texture clipped to shape boundary
|
|
2319
|
+
// Optimized: cap grid to max 40×40 = 1600 dots (was unbounded at O(size²)),
|
|
2320
|
+
// quantize alpha into buckets to minimize globalAlpha state changes,
|
|
2321
|
+
// and batch dots by brightness (black/white) × alpha bucket
|
|
2243
2322
|
const savedAlphaN = ctx.globalAlpha;
|
|
2244
2323
|
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
2245
2324
|
ctx.fill(); // base tint
|
|
@@ -2248,17 +2327,47 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2248
2327
|
ctx.clip();
|
|
2249
2328
|
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
2250
2329
|
const extentN = size * 0.55;
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
const
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2330
|
+
if (rng) {
|
|
2331
|
+
// Cap grid to max 40 dots per axis — beyond this the grain is
|
|
2332
|
+
// visually indistinguishable but cost scales quadratically.
|
|
2333
|
+
const maxGrainPerAxis = Math.min(Math.ceil(extentN * 2 / grainSpacing), 40);
|
|
2334
|
+
const actualGrainSpacing = extentN * 2 / maxGrainPerAxis;
|
|
2335
|
+
// 4 alpha buckets: 0.2, 0.3, 0.4, 0.5 — covers the 0.15-0.50 range
|
|
2336
|
+
const BUCKETS = 4;
|
|
2337
|
+
const bucketMin = 0.15;
|
|
2338
|
+
const bucketRange = 0.35;
|
|
2339
|
+
// [black_bucket0, black_bucket1, ..., white_bucket0, ...]
|
|
2340
|
+
const buckets = [];
|
|
2341
|
+
for(let i = 0; i < BUCKETS * 2; i++)buckets.push([]);
|
|
2342
|
+
for(let xi = 0; xi < maxGrainPerAxis; xi++){
|
|
2343
|
+
const gx = -extentN + xi * actualGrainSpacing;
|
|
2344
|
+
for(let yi = 0; yi < maxGrainPerAxis; yi++){
|
|
2345
|
+
const gy = -extentN + yi * actualGrainSpacing;
|
|
2346
|
+
const jx = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2347
|
+
const jy = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2348
|
+
const isWhite = rng() > 0.5;
|
|
2349
|
+
const dotAlpha = bucketMin + rng() * bucketRange;
|
|
2350
|
+
const dotSize = actualGrainSpacing * (0.3 + rng() * 0.5);
|
|
2351
|
+
const bucketIdx = Math.min(BUCKETS - 1, Math.floor((dotAlpha - bucketMin) / bucketRange * BUCKETS));
|
|
2352
|
+
const offset = isWhite ? BUCKETS : 0;
|
|
2353
|
+
buckets[offset + bucketIdx].push({
|
|
2354
|
+
x: gx + jx,
|
|
2355
|
+
y: gy + jy,
|
|
2356
|
+
s: dotSize
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
// Render each bucket: 2 colors × 4 alpha levels = 8 state changes total
|
|
2361
|
+
for(let color = 0; color < 2; color++){
|
|
2362
|
+
ctx.fillStyle = color === 0 ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
2363
|
+
for(let b = 0; b < BUCKETS; b++){
|
|
2364
|
+
const dots = buckets[color * BUCKETS + b];
|
|
2365
|
+
if (dots.length === 0) continue;
|
|
2366
|
+
const alpha = bucketMin + (b + 0.5) / BUCKETS * bucketRange;
|
|
2367
|
+
ctx.globalAlpha = savedAlphaN * alpha;
|
|
2368
|
+
for(let i = 0; i < dots.length; i++)ctx.fillRect(dots[i].x, dots[i].y, dots[i].s, dots[i].s);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2262
2371
|
}
|
|
2263
2372
|
ctx.restore();
|
|
2264
2373
|
ctx.fillStyle = fillColor;
|
|
@@ -2271,6 +2380,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2271
2380
|
case "wood-grain":
|
|
2272
2381
|
{
|
|
2273
2382
|
// Parallel wavy lines simulating wood grain, clipped to shape
|
|
2383
|
+
// Optimized: batch all grain lines into a single path, increased step from 2 to 4
|
|
2274
2384
|
const savedAlphaW = ctx.globalAlpha;
|
|
2275
2385
|
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
2276
2386
|
ctx.fill(); // base tint
|
|
@@ -2286,17 +2396,19 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2286
2396
|
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
2287
2397
|
const cosG = Math.cos(grainAngle);
|
|
2288
2398
|
const sinG = Math.sin(grainAngle);
|
|
2399
|
+
const waveCoeff = waveFreq * Math.PI;
|
|
2400
|
+
const invExtentW = 1 / extentW;
|
|
2401
|
+
// Batch all grain lines into a single path
|
|
2402
|
+
ctx.beginPath();
|
|
2289
2403
|
for(let d = -extentW; d <= extentW; d += grainLineSpacing){
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
if (t === -extentW) ctx.moveTo(px, py);
|
|
2296
|
-
else ctx.lineTo(px, py);
|
|
2404
|
+
const firstWave = Math.sin(-extentW * invExtentW * waveCoeff) * waveAmp;
|
|
2405
|
+
ctx.moveTo(-extentW * cosG - (d + firstWave) * sinG, -extentW * sinG + (d + firstWave) * cosG);
|
|
2406
|
+
for(let t = -extentW + 4; t <= extentW; t += 4){
|
|
2407
|
+
const wave = Math.sin(t * invExtentW * waveCoeff) * waveAmp;
|
|
2408
|
+
ctx.lineTo(t * cosG - (d + wave) * sinG, t * sinG + (d + wave) * cosG);
|
|
2297
2409
|
}
|
|
2298
|
-
ctx.stroke();
|
|
2299
2410
|
}
|
|
2411
|
+
ctx.stroke();
|
|
2300
2412
|
ctx.restore();
|
|
2301
2413
|
ctx.globalAlpha = savedAlphaW;
|
|
2302
2414
|
ctx.globalAlpha *= 0.35;
|
|
@@ -2358,6 +2470,7 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2358
2470
|
case "fabric-weave":
|
|
2359
2471
|
{
|
|
2360
2472
|
// Interlocking horizontal/vertical threads clipped to shape
|
|
2473
|
+
// Optimized: batch all horizontal threads into one path, all vertical into another
|
|
2361
2474
|
const savedAlphaF = ctx.globalAlpha;
|
|
2362
2475
|
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
2363
2476
|
ctx.fill(); // ghost base
|
|
@@ -2367,26 +2480,24 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2367
2480
|
const threadSpacing = Math.max(2, size * 0.04);
|
|
2368
2481
|
const extentF = size * 0.55;
|
|
2369
2482
|
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
2483
|
+
// Horizontal threads — batched
|
|
2370
2484
|
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
2371
|
-
|
|
2485
|
+
ctx.beginPath();
|
|
2372
2486
|
for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2373
|
-
ctx.beginPath();
|
|
2374
2487
|
ctx.moveTo(-extentF, y);
|
|
2375
2488
|
ctx.lineTo(extentF, y);
|
|
2376
|
-
ctx.stroke();
|
|
2377
2489
|
}
|
|
2378
|
-
|
|
2490
|
+
ctx.stroke();
|
|
2491
|
+
// Vertical threads (offset by half spacing for weave effect) — batched
|
|
2379
2492
|
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
2380
2493
|
ctx.strokeStyle = fillColor;
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
ctx.lineTo(x, y + threadSpacing);
|
|
2387
|
-
}
|
|
2388
|
-
ctx.stroke();
|
|
2494
|
+
ctx.beginPath();
|
|
2495
|
+
for(let x = -extentF; x <= extentF; x += threadSpacing * 2)for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2496
|
+
// Over-under: draw segment, skip segment
|
|
2497
|
+
ctx.moveTo(x, y);
|
|
2498
|
+
ctx.lineTo(x, y + threadSpacing);
|
|
2389
2499
|
}
|
|
2500
|
+
ctx.stroke();
|
|
2390
2501
|
ctx.strokeStyle = strokeColor;
|
|
2391
2502
|
ctx.restore();
|
|
2392
2503
|
ctx.globalAlpha = savedAlphaF;
|
|
@@ -2452,14 +2563,17 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2452
2563
|
ctx.translate(x, y);
|
|
2453
2564
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2454
2565
|
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2455
|
-
|
|
2566
|
+
// Skip shadow entirely for small shapes (< 20px) — the blur is expensive
|
|
2567
|
+
// and visually imperceptible at that scale.
|
|
2568
|
+
const useShadow = size >= 20;
|
|
2569
|
+
if (useShadow && lightAngle !== undefined) {
|
|
2456
2570
|
const shadowDist = size * 0.035;
|
|
2457
2571
|
const shadowBlurR = size * 0.06;
|
|
2458
2572
|
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2459
2573
|
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2460
2574
|
ctx.shadowBlur = shadowBlurR;
|
|
2461
2575
|
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2462
|
-
} else if (glowRadius > 0) {
|
|
2576
|
+
} else if (useShadow && glowRadius > 0) {
|
|
2463
2577
|
// Glow / shadow effect (legacy path)
|
|
2464
2578
|
ctx.shadowBlur = glowRadius;
|
|
2465
2579
|
ctx.shadowColor = glowColor || fillColor;
|
|
@@ -2483,30 +2597,27 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2483
2597
|
$e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2484
2598
|
}
|
|
2485
2599
|
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2600
|
+
// Only reset if we actually set shadow (avoids unnecessary state changes)
|
|
2601
|
+
if (useShadow && (lightAngle !== undefined || glowRadius > 0)) {
|
|
2602
|
+
ctx.shadowBlur = 0;
|
|
2603
|
+
ctx.shadowOffsetX = 0;
|
|
2604
|
+
ctx.shadowOffsetY = 0;
|
|
2605
|
+
ctx.shadowColor = "transparent";
|
|
2606
|
+
}
|
|
2490
2607
|
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2491
|
-
|
|
2608
|
+
// Skip for small shapes (< 30px) — gradient creation + composite op
|
|
2609
|
+
// switch is expensive and the highlight is invisible at small sizes.
|
|
2610
|
+
if (lightAngle !== undefined && size > 30 && rng) {
|
|
2492
2611
|
const hlRadius = size * 0.35;
|
|
2493
2612
|
const hlDist = size * 0.15;
|
|
2494
2613
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2495
2614
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2496
2615
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2497
|
-
//
|
|
2498
|
-
//
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2503
|
-
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2504
|
-
// Blend toward white but keep a hint of the fill's warmth
|
|
2505
|
-
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2506
|
-
}
|
|
2507
|
-
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2508
|
-
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2509
|
-
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2616
|
+
// Use a simple white highlight — the per-shape hex parse was expensive
|
|
2617
|
+
// and the visual difference from tinted highlights is negligible.
|
|
2618
|
+
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2619
|
+
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2620
|
+
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
2510
2621
|
const savedOp = ctx.globalCompositeOperation;
|
|
2511
2622
|
ctx.globalCompositeOperation = "soft-light";
|
|
2512
2623
|
ctx.fillStyle = hlGrad;
|
|
@@ -4048,6 +4159,46 @@ function $68a238ccd77f2bcd$export$f1142fd7da4d6590(rng) {
|
|
|
4048
4159
|
}
|
|
4049
4160
|
|
|
4050
4161
|
|
|
4162
|
+
// ── Render style cost weights (normalized: fill-and-stroke = 1) ─────
|
|
4163
|
+
// Based on benchmark measurements. Used by the complexity budget to
|
|
4164
|
+
// cap total rendering work and downgrade expensive styles when needed.
|
|
4165
|
+
const $1f63dc64b5593c73$var$RENDER_STYLE_COST = {
|
|
4166
|
+
"fill-and-stroke": 1,
|
|
4167
|
+
"fill-only": 0.5,
|
|
4168
|
+
"stroke-only": 1,
|
|
4169
|
+
"double-stroke": 1.5,
|
|
4170
|
+
"dashed": 1,
|
|
4171
|
+
"watercolor": 7,
|
|
4172
|
+
"hatched": 3,
|
|
4173
|
+
"incomplete": 1,
|
|
4174
|
+
"stipple": 90,
|
|
4175
|
+
"stencil": 2,
|
|
4176
|
+
"noise-grain": 400,
|
|
4177
|
+
"wood-grain": 10,
|
|
4178
|
+
"marble-vein": 4,
|
|
4179
|
+
"fabric-weave": 6,
|
|
4180
|
+
"hand-drawn": 5
|
|
4181
|
+
};
|
|
4182
|
+
function $1f63dc64b5593c73$var$downgradeRenderStyle(style) {
|
|
4183
|
+
switch(style){
|
|
4184
|
+
case "noise-grain":
|
|
4185
|
+
return "hatched";
|
|
4186
|
+
case "stipple":
|
|
4187
|
+
return "dashed";
|
|
4188
|
+
case "wood-grain":
|
|
4189
|
+
return "hatched";
|
|
4190
|
+
case "watercolor":
|
|
4191
|
+
return "fill-and-stroke";
|
|
4192
|
+
case "fabric-weave":
|
|
4193
|
+
return "hatched";
|
|
4194
|
+
case "hand-drawn":
|
|
4195
|
+
return "fill-and-stroke";
|
|
4196
|
+
case "marble-vein":
|
|
4197
|
+
return "stroke-only";
|
|
4198
|
+
default:
|
|
4199
|
+
return style;
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4051
4202
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
4052
4203
|
const $1f63dc64b5593c73$var$SACRED_SHAPES = [
|
|
4053
4204
|
"mandala",
|
|
@@ -4431,6 +4582,15 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4431
4582
|
...(0, $81c1b644006d48ec$export$c2f8e0cc249a8d8f),
|
|
4432
4583
|
...config
|
|
4433
4584
|
};
|
|
4585
|
+
const _dt = finalConfig._debugTiming;
|
|
4586
|
+
const _t = _dt ? ()=>performance.now() : undefined;
|
|
4587
|
+
let _p = _t ? _t() : 0;
|
|
4588
|
+
function _mark(name) {
|
|
4589
|
+
if (!_dt || !_t) return;
|
|
4590
|
+
const now = _t();
|
|
4591
|
+
_dt.phases[name] = now - _p;
|
|
4592
|
+
_p = now;
|
|
4593
|
+
}
|
|
4434
4594
|
const rng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash));
|
|
4435
4595
|
// ── 0. Select archetype — fundamentally different visual personality ──
|
|
4436
4596
|
const archetype = (0, $68a238ccd77f2bcd$export$f1142fd7da4d6590)(rng);
|
|
@@ -4464,12 +4624,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4464
4624
|
const adjustedMaxSize = maxShapeSize * scaleFactor;
|
|
4465
4625
|
const cx = width / 2;
|
|
4466
4626
|
const cy = height / 2;
|
|
4627
|
+
_mark("0_setup");
|
|
4467
4628
|
// ── 1. Background ──────────────────────────────────────────────
|
|
4468
4629
|
const bgRadius = Math.hypot(cx, cy);
|
|
4469
4630
|
$1f63dc64b5593c73$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
|
|
4470
4631
|
// Gradient mesh overlay — 3-4 color control points for richer backgrounds
|
|
4632
|
+
// Use source-over instead of soft-light for cheaper compositing
|
|
4471
4633
|
const meshPoints = 3 + Math.floor(rng() * 2);
|
|
4472
|
-
ctx.
|
|
4634
|
+
ctx.globalAlpha = 1;
|
|
4473
4635
|
for(let i = 0; i < meshPoints; i++){
|
|
4474
4636
|
const mx = rng() * width;
|
|
4475
4637
|
const my = rng() * height;
|
|
@@ -4478,95 +4640,103 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4478
4640
|
const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
|
|
4479
4641
|
grad.addColorStop(0, (0, $b5a262d09b87e373$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
|
|
4480
4642
|
grad.addColorStop(1, "rgba(0,0,0,0)");
|
|
4481
|
-
ctx.globalAlpha = 1;
|
|
4482
4643
|
ctx.fillStyle = grad;
|
|
4483
|
-
|
|
4644
|
+
// Clip to gradient bounding box — avoids blending transparent pixels
|
|
4645
|
+
const gx = Math.max(0, mx - mRadius);
|
|
4646
|
+
const gy = Math.max(0, my - mRadius);
|
|
4647
|
+
const gw = Math.min(width, mx + mRadius) - gx;
|
|
4648
|
+
const gh = Math.min(height, my + mRadius) - gy;
|
|
4649
|
+
ctx.fillRect(gx, gy, gw, gh);
|
|
4484
4650
|
}
|
|
4485
|
-
ctx.globalCompositeOperation = "source-over";
|
|
4486
4651
|
// Compute average background luminance for contrast enforcement
|
|
4487
4652
|
const bgLum = ((0, $b5a262d09b87e373$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $b5a262d09b87e373$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
|
|
4488
4653
|
// ── 1b. Layered background — archetype-coherent shapes ─────────
|
|
4654
|
+
// Use source-over with pre-multiplied alpha instead of soft-light
|
|
4655
|
+
// for much cheaper compositing (soft-light requires per-pixel blend)
|
|
4489
4656
|
const bgShapeCount = 3 + Math.floor(rng() * 4);
|
|
4490
|
-
ctx.globalCompositeOperation = "soft-light";
|
|
4491
4657
|
for(let i = 0; i < bgShapeCount; i++){
|
|
4492
4658
|
const bx = rng() * width;
|
|
4493
4659
|
const by = rng() * height;
|
|
4494
4660
|
const bSize = width * 0.3 + rng() * width * 0.5;
|
|
4495
4661
|
const bColor = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng);
|
|
4496
|
-
ctx.globalAlpha = 0.03 + rng() * 0.05;
|
|
4662
|
+
ctx.globalAlpha = (0.03 + rng() * 0.05) * 0.5; // halved to compensate for source-over vs soft-light
|
|
4497
4663
|
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(bColor, 0.15);
|
|
4498
4664
|
ctx.beginPath();
|
|
4499
4665
|
// Use archetype-appropriate background shapes
|
|
4500
|
-
if (archetype.name === "geometric-precision" || archetype.name === "op-art")
|
|
4501
|
-
ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
|
|
4666
|
+
if (archetype.name === "geometric-precision" || archetype.name === "op-art") ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
|
|
4502
4667
|
else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
|
|
4503
4668
|
ctx.fill();
|
|
4504
4669
|
}
|
|
4505
|
-
// Subtle concentric rings from center
|
|
4670
|
+
// Subtle concentric rings from center — batched into single stroke
|
|
4506
4671
|
const ringCount = 2 + Math.floor(rng() * 3);
|
|
4507
4672
|
ctx.globalAlpha = 0.02 + rng() * 0.03;
|
|
4508
4673
|
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
4509
4674
|
ctx.lineWidth = 1 * scaleFactor;
|
|
4675
|
+
ctx.beginPath();
|
|
4510
4676
|
for(let i = 1; i <= ringCount; i++){
|
|
4511
4677
|
const r = Math.min(width, height) * 0.15 * i;
|
|
4512
|
-
ctx.
|
|
4678
|
+
ctx.moveTo(cx + r, cy);
|
|
4513
4679
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
4514
|
-
ctx.stroke();
|
|
4515
4680
|
}
|
|
4516
|
-
ctx.
|
|
4681
|
+
ctx.stroke();
|
|
4517
4682
|
// ── 1c. Background pattern layer — subtle textured paper ───────
|
|
4518
4683
|
const bgPatternRoll = rng();
|
|
4519
4684
|
if (bgPatternRoll < 0.6) {
|
|
4520
4685
|
ctx.save();
|
|
4521
|
-
ctx.globalCompositeOperation = "soft-light";
|
|
4522
4686
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4523
4687
|
const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4524
4688
|
if (bgPatternRoll < 0.2) {
|
|
4525
|
-
// Dot grid —
|
|
4526
|
-
const dotSpacing = Math.max(
|
|
4527
|
-
const
|
|
4689
|
+
// Dot grid — use fillRect instead of arcs (much cheaper, no path building)
|
|
4690
|
+
const dotSpacing = Math.max(12, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4691
|
+
const dotDiam = Math.max(1, Math.round(dotSpacing * 0.16));
|
|
4528
4692
|
ctx.globalAlpha = patternOpacity;
|
|
4529
4693
|
ctx.fillStyle = patternColor;
|
|
4530
|
-
|
|
4531
|
-
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4532
|
-
ctx.
|
|
4533
|
-
|
|
4694
|
+
let dotCount = 0;
|
|
4695
|
+
for(let px = 0; px < width && dotCount < 2000; px += dotSpacing)for(let py = 0; py < height && dotCount < 2000; py += dotSpacing){
|
|
4696
|
+
ctx.fillRect(px, py, dotDiam, dotDiam);
|
|
4697
|
+
dotCount++;
|
|
4534
4698
|
}
|
|
4535
|
-
ctx.fill();
|
|
4536
4699
|
} else if (bgPatternRoll < 0.4) {
|
|
4537
|
-
// Diagonal lines — batched into a single path
|
|
4538
|
-
const lineSpacing = Math.max(
|
|
4700
|
+
// Diagonal lines — batched into a single path, capped at 300 lines
|
|
4701
|
+
const lineSpacing = Math.max(10, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4539
4702
|
ctx.globalAlpha = patternOpacity;
|
|
4540
4703
|
ctx.strokeStyle = patternColor;
|
|
4541
4704
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4542
4705
|
const diag = Math.hypot(width, height);
|
|
4543
4706
|
ctx.beginPath();
|
|
4544
|
-
|
|
4707
|
+
let lineCount = 0;
|
|
4708
|
+
for(let d = -diag; d < diag && lineCount < 300; d += lineSpacing){
|
|
4545
4709
|
ctx.moveTo(d, 0);
|
|
4546
4710
|
ctx.lineTo(d + height, height);
|
|
4711
|
+
lineCount++;
|
|
4547
4712
|
}
|
|
4548
4713
|
ctx.stroke();
|
|
4549
4714
|
} else {
|
|
4550
|
-
// Tessellation — hexagonal grid,
|
|
4551
|
-
const tessSize = Math.max(
|
|
4715
|
+
// Tessellation — hexagonal grid, capped at 500 hexagons
|
|
4716
|
+
const tessSize = Math.max(15, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4552
4717
|
const tessH = tessSize * Math.sqrt(3);
|
|
4553
4718
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4554
4719
|
ctx.strokeStyle = patternColor;
|
|
4555
4720
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4721
|
+
// Pre-compute hex vertex offsets (avoid trig per vertex)
|
|
4722
|
+
const hexVx = [];
|
|
4723
|
+
const hexVy = [];
|
|
4724
|
+
for(let s = 0; s < 6; s++){
|
|
4725
|
+
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4726
|
+
hexVx.push(Math.cos(angle) * tessSize * 0.5);
|
|
4727
|
+
hexVy.push(Math.sin(angle) * tessSize * 0.5);
|
|
4728
|
+
}
|
|
4556
4729
|
ctx.beginPath();
|
|
4557
|
-
|
|
4730
|
+
let hexCount = 0;
|
|
4731
|
+
for(let row = 0; row * tessH < height + tessH && hexCount < 500; row++){
|
|
4558
4732
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4559
|
-
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4733
|
+
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5 && hexCount < 500; col++){
|
|
4560
4734
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4561
4735
|
const hy = row * tessH;
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
4565
|
-
const vy = hy + Math.sin(angle) * tessSize * 0.5;
|
|
4566
|
-
if (s === 0) ctx.moveTo(vx, vy);
|
|
4567
|
-
else ctx.lineTo(vx, vy);
|
|
4568
|
-
}
|
|
4736
|
+
ctx.moveTo(hx + hexVx[0], hy + hexVy[0]);
|
|
4737
|
+
for(let s = 1; s < 6; s++)ctx.lineTo(hx + hexVx[s], hy + hexVy[s]);
|
|
4569
4738
|
ctx.closePath();
|
|
4739
|
+
hexCount++;
|
|
4570
4740
|
}
|
|
4571
4741
|
}
|
|
4572
4742
|
ctx.stroke();
|
|
@@ -4574,6 +4744,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4574
4744
|
ctx.restore();
|
|
4575
4745
|
}
|
|
4576
4746
|
ctx.globalCompositeOperation = "source-over";
|
|
4747
|
+
_mark("1_background");
|
|
4577
4748
|
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4578
4749
|
const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES.length)];
|
|
4579
4750
|
const symRoll = rng();
|
|
@@ -4657,19 +4828,20 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4657
4828
|
ctx.beginPath();
|
|
4658
4829
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
4659
4830
|
ctx.stroke();
|
|
4660
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
4831
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
4661
4832
|
if (rng() < 0.5) {
|
|
4662
4833
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
4663
4834
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
4664
4835
|
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
4836
|
+
ctx.beginPath();
|
|
4665
4837
|
for(let d = 0; d < dotCount; d++){
|
|
4666
4838
|
const angle = rng() * Math.PI * 2;
|
|
4667
4839
|
const dist = rng() * zone.radius * 0.7;
|
|
4668
4840
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
4669
|
-
ctx.
|
|
4841
|
+
ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
|
|
4670
4842
|
ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
|
|
4671
|
-
ctx.fill();
|
|
4672
4843
|
}
|
|
4844
|
+
ctx.fill();
|
|
4673
4845
|
}
|
|
4674
4846
|
// ~30% chance: thin concentric ring inside
|
|
4675
4847
|
if (rng() < 0.3) {
|
|
@@ -4683,6 +4855,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4683
4855
|
}
|
|
4684
4856
|
}
|
|
4685
4857
|
ctx.globalAlpha = 1;
|
|
4858
|
+
_mark("2_3_composition_focal");
|
|
4686
4859
|
// ── 4. Flow field — simplex noise for organic variation ─────────
|
|
4687
4860
|
// Create a seeded simplex noise field (unique per hash)
|
|
4688
4861
|
const noiseFieldRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 333));
|
|
@@ -4759,8 +4932,29 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4759
4932
|
shape: heroShape
|
|
4760
4933
|
});
|
|
4761
4934
|
}
|
|
4935
|
+
_mark("4_flowfield_hero");
|
|
4762
4936
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4763
4937
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4938
|
+
// ── Complexity budget — caps total rendering work ──────────────
|
|
4939
|
+
// Budget scales with pixel area so larger canvases get proportionally
|
|
4940
|
+
// more headroom. The multiplier extras (glazing, echoes, nesting,
|
|
4941
|
+
// constellations, rhythm) are gated behind the budget; when it runs
|
|
4942
|
+
// low they are skipped. When it's exhausted, expensive render styles
|
|
4943
|
+
// are downgraded to cheaper alternatives.
|
|
4944
|
+
//
|
|
4945
|
+
// RNG values are always consumed even when skipping, so the
|
|
4946
|
+
// deterministic sequence for shapes that *do* render is preserved.
|
|
4947
|
+
const pixelArea = width * height;
|
|
4948
|
+
const BUDGET_PER_MEGAPIXEL = 6000; // cost units per 1M pixels
|
|
4949
|
+
let complexityBudget = pixelArea / 1000000 * BUDGET_PER_MEGAPIXEL;
|
|
4950
|
+
const totalBudget = complexityBudget;
|
|
4951
|
+
const budgetForExtras = complexityBudget * 0.25; // reserve 25% for multiplier extras
|
|
4952
|
+
let extrasSpent = 0;
|
|
4953
|
+
// Hard cap on clip-heavy render styles (stipple, noise-grain).
|
|
4954
|
+
// These generate O(size²) fillRect calls per shape and dominate
|
|
4955
|
+
// worst-case render time. Cap scales with pixel area.
|
|
4956
|
+
const MAX_CLIP_HEAVY_SHAPES = Math.max(4, Math.floor(8 * (pixelArea / 1000000)));
|
|
4957
|
+
let clipHeavyCount = 0;
|
|
4764
4958
|
for(let layer = 0; layer < layers; layer++){
|
|
4765
4959
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
4766
4960
|
const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
|
|
@@ -4849,7 +5043,26 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4849
5043
|
const shapeRenderStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
|
|
4850
5044
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
4851
5045
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
4852
|
-
|
|
5046
|
+
let finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
|
|
5047
|
+
// Budget check: downgrade expensive styles proportionally —
|
|
5048
|
+
// the more expensive the style, the earlier it gets downgraded.
|
|
5049
|
+
// noise-grain (400) downgrades when budget < 20% remaining,
|
|
5050
|
+
// stipple (90) when < 82%, wood-grain (10) when < 98%.
|
|
5051
|
+
let styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5052
|
+
if (styleCost > 3) {
|
|
5053
|
+
const downgradeThreshold = Math.min(0.85, styleCost / 500);
|
|
5054
|
+
if (complexityBudget < totalBudget * (1 - downgradeThreshold)) {
|
|
5055
|
+
finalRenderStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(finalRenderStyle);
|
|
5056
|
+
styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
// Hard cap: clip-heavy styles (stipple, noise-grain) are limited
|
|
5060
|
+
// to MAX_CLIP_HEAVY_SHAPES total across the entire render.
|
|
5061
|
+
if ((finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
5062
|
+
finalRenderStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(finalRenderStyle);
|
|
5063
|
+
styleCost = $1f63dc64b5593c73$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5064
|
+
}
|
|
5065
|
+
if (finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") clipHeavyCount++;
|
|
4853
5066
|
// Consistent light direction — subtle shadow offset
|
|
4854
5067
|
const shadowDist = hasGlow ? 0 : size * 0.02;
|
|
4855
5068
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
@@ -4904,30 +5117,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4904
5117
|
lightAngle: lightAngle,
|
|
4905
5118
|
scaleFactor: scaleFactor
|
|
4906
5119
|
};
|
|
4907
|
-
if (shouldMirror)
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
5120
|
+
if (shouldMirror) {
|
|
5121
|
+
(0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
5122
|
+
...shapeConfig,
|
|
5123
|
+
mirrorAxis: mirrorAxis,
|
|
5124
|
+
mirrorGap: size * (0.1 + rng() * 0.3)
|
|
5125
|
+
});
|
|
5126
|
+
complexityBudget -= styleCost * 2; // mirrored = 2 shapes
|
|
5127
|
+
} else {
|
|
5128
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
|
|
5129
|
+
complexityBudget -= styleCost;
|
|
5130
|
+
}
|
|
5131
|
+
// ── Extras budget gate — skip multiplier sections when over budget ──
|
|
5132
|
+
const extrasAllowed = extrasSpent < budgetForExtras;
|
|
4913
5133
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
4914
5134
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
4915
5135
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
5136
|
+
if (extrasAllowed) {
|
|
5137
|
+
for(let g = 0; g < glazePasses; g++){
|
|
5138
|
+
const glazeScale = 1 - (g + 1) * 0.12;
|
|
5139
|
+
const glazeAlpha = 0.08 + g * 0.04;
|
|
5140
|
+
ctx.globalAlpha = glazeAlpha;
|
|
5141
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
|
|
5142
|
+
fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
|
|
5143
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
5144
|
+
strokeWidth: 0,
|
|
5145
|
+
size: size * glazeScale,
|
|
5146
|
+
rotation: rotation,
|
|
5147
|
+
proportionType: "GOLDEN_RATIO",
|
|
5148
|
+
renderStyle: "fill-only",
|
|
5149
|
+
rng: rng
|
|
5150
|
+
});
|
|
5151
|
+
}
|
|
5152
|
+
extrasSpent += glazePasses;
|
|
4930
5153
|
}
|
|
5154
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
4931
5155
|
}
|
|
4932
5156
|
shapePositions.push({
|
|
4933
5157
|
x: finalX,
|
|
@@ -4945,37 +5169,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4945
5169
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4946
5170
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
4947
5171
|
const echoAngle = rng() * Math.PI * 2;
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
5172
|
+
if (extrasAllowed) {
|
|
5173
|
+
for(let e = 0; e < echoCount; e++){
|
|
5174
|
+
const echoScale = 0.3 - e * 0.08;
|
|
5175
|
+
const echoDist = size * (0.6 + e * 0.4);
|
|
5176
|
+
const echoX = finalX + Math.cos(echoAngle) * echoDist;
|
|
5177
|
+
const echoY = finalY + Math.sin(echoAngle) * echoDist;
|
|
5178
|
+
const echoSize = size * Math.max(0.1, echoScale);
|
|
5179
|
+
if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
|
|
5180
|
+
ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
|
|
5181
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
|
|
5182
|
+
fillColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
|
|
5183
|
+
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.4),
|
|
5184
|
+
strokeWidth: strokeWidth * 0.6,
|
|
5185
|
+
size: echoSize,
|
|
5186
|
+
rotation: rotation + (e + 1) * 15,
|
|
5187
|
+
proportionType: "GOLDEN_RATIO",
|
|
5188
|
+
renderStyle: finalRenderStyle,
|
|
5189
|
+
rng: rng
|
|
5190
|
+
});
|
|
5191
|
+
shapePositions.push({
|
|
5192
|
+
x: echoX,
|
|
5193
|
+
y: echoY,
|
|
5194
|
+
size: echoSize,
|
|
5195
|
+
shape: shape
|
|
5196
|
+
});
|
|
5197
|
+
spatialGrid.insert({
|
|
5198
|
+
x: echoX,
|
|
5199
|
+
y: echoY,
|
|
5200
|
+
size: echoSize,
|
|
5201
|
+
shape: shape
|
|
5202
|
+
});
|
|
5203
|
+
}
|
|
5204
|
+
extrasSpent += echoCount * styleCost;
|
|
4978
5205
|
}
|
|
5206
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
4979
5207
|
}
|
|
4980
5208
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
4981
5209
|
// Focal depth: shapes near focal points get more detail
|
|
@@ -4983,7 +5211,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4983
5211
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
4984
5212
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
4985
5213
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
4986
|
-
for(let n = 0; n < innerCount; n++){
|
|
5214
|
+
if (extrasAllowed) for(let n = 0; n < innerCount; n++){
|
|
4987
5215
|
// Pick inner shape from palette affinities
|
|
4988
5216
|
const innerSizeFraction = size * 0.25 / adjustedMaxSize;
|
|
4989
5217
|
const innerShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
|
|
@@ -4992,6 +5220,10 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4992
5220
|
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
4993
5221
|
const innerRot = rng() * 360;
|
|
4994
5222
|
const innerFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
|
|
5223
|
+
let innerStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng);
|
|
5224
|
+
// Apply clip-heavy cap to nested shapes too
|
|
5225
|
+
if ((innerStyle === "stipple" || innerStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) innerStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(innerStyle);
|
|
5226
|
+
if (innerStyle === "stipple" || innerStyle === "noise-grain") clipHeavyCount++;
|
|
4995
5227
|
ctx.globalAlpha = layerOpacity * 0.7;
|
|
4996
5228
|
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
|
|
4997
5229
|
fillColor: innerFill,
|
|
@@ -5000,9 +5232,21 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5000
5232
|
size: innerSize,
|
|
5001
5233
|
rotation: innerRot,
|
|
5002
5234
|
proportionType: "GOLDEN_RATIO",
|
|
5003
|
-
renderStyle:
|
|
5235
|
+
renderStyle: innerStyle,
|
|
5004
5236
|
rng: rng
|
|
5005
5237
|
});
|
|
5238
|
+
extrasSpent += $1f63dc64b5593c73$var$RENDER_STYLE_COST[innerStyle] ?? 1;
|
|
5239
|
+
}
|
|
5240
|
+
else // Drain RNG to keep determinism — each nested shape consumes ~8 rng calls
|
|
5241
|
+
for(let n = 0; n < innerCount; n++){
|
|
5242
|
+
rng();
|
|
5243
|
+
rng();
|
|
5244
|
+
rng();
|
|
5245
|
+
rng();
|
|
5246
|
+
rng();
|
|
5247
|
+
rng();
|
|
5248
|
+
rng();
|
|
5249
|
+
rng();
|
|
5006
5250
|
}
|
|
5007
5251
|
}
|
|
5008
5252
|
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
@@ -5011,40 +5255,55 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5011
5255
|
const constellation = $1f63dc64b5593c73$var$CONSTELLATIONS[Math.floor(rng() * $1f63dc64b5593c73$var$CONSTELLATIONS.length)];
|
|
5012
5256
|
const members = constellation.build(rng, size);
|
|
5013
5257
|
const groupRotation = rng() * Math.PI * 2;
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5258
|
+
if (extrasAllowed) {
|
|
5259
|
+
const cosR = Math.cos(groupRotation);
|
|
5260
|
+
const sinR = Math.sin(groupRotation);
|
|
5261
|
+
for (const member of members){
|
|
5262
|
+
// Rotate the group offset by the group rotation
|
|
5263
|
+
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
5264
|
+
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
5265
|
+
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
5266
|
+
const memberFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
|
|
5267
|
+
const memberStroke = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
|
|
5268
|
+
ctx.globalAlpha = layerOpacity * 0.6;
|
|
5269
|
+
// Use the member's shape if available, otherwise fall back to palette
|
|
5270
|
+
const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
|
|
5271
|
+
let memberStyle = (0, $8286059160ee2e04$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng);
|
|
5272
|
+
// Apply clip-heavy cap to constellation members too
|
|
5273
|
+
if ((memberStyle === "stipple" || memberStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) memberStyle = $1f63dc64b5593c73$var$downgradeRenderStyle(memberStyle);
|
|
5274
|
+
if (memberStyle === "stipple" || memberStyle === "noise-grain") clipHeavyCount++;
|
|
5275
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
|
|
5276
|
+
fillColor: memberFill,
|
|
5277
|
+
strokeColor: memberStroke,
|
|
5278
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5279
|
+
size: member.size,
|
|
5280
|
+
rotation: member.rotation + groupRotation * 180 / Math.PI,
|
|
5281
|
+
proportionType: "GOLDEN_RATIO",
|
|
5282
|
+
renderStyle: memberStyle,
|
|
5283
|
+
rng: rng
|
|
5284
|
+
});
|
|
5285
|
+
shapePositions.push({
|
|
5286
|
+
x: mx,
|
|
5287
|
+
y: my,
|
|
5288
|
+
size: member.size,
|
|
5289
|
+
shape: memberShape
|
|
5290
|
+
});
|
|
5291
|
+
spatialGrid.insert({
|
|
5292
|
+
x: mx,
|
|
5293
|
+
y: my,
|
|
5294
|
+
size: member.size,
|
|
5295
|
+
shape: memberShape
|
|
5296
|
+
});
|
|
5297
|
+
extrasSpent += $1f63dc64b5593c73$var$RENDER_STYLE_COST[memberStyle] ?? 1;
|
|
5298
|
+
}
|
|
5299
|
+
} else // Drain RNG — each member consumes ~6 rng calls for colors/style
|
|
5300
|
+
for(let m = 0; m < members.length; m++){
|
|
5301
|
+
rng();
|
|
5302
|
+
rng();
|
|
5303
|
+
rng();
|
|
5304
|
+
rng();
|
|
5305
|
+
rng();
|
|
5306
|
+
rng();
|
|
5048
5307
|
}
|
|
5049
5308
|
}
|
|
5050
5309
|
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
@@ -5055,45 +5314,58 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5055
5314
|
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5056
5315
|
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5057
5316
|
const rhythmShape = shape; // same shape for visual rhythm
|
|
5058
|
-
|
|
5317
|
+
if (extrasAllowed) {
|
|
5318
|
+
let rhythmSize = size * 0.6;
|
|
5319
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5320
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5321
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5322
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5323
|
+
if ($1f63dc64b5593c73$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5324
|
+
rhythmSize *= rhythmDecay;
|
|
5325
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5326
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5327
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5328
|
+
const rhythmFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5329
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5330
|
+
fillColor: rhythmFill,
|
|
5331
|
+
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5332
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5333
|
+
size: rhythmSize,
|
|
5334
|
+
rotation: rotation + (r + 1) * 12,
|
|
5335
|
+
proportionType: "GOLDEN_RATIO",
|
|
5336
|
+
renderStyle: finalRenderStyle,
|
|
5337
|
+
rng: rng
|
|
5338
|
+
});
|
|
5339
|
+
shapePositions.push({
|
|
5340
|
+
x: rx,
|
|
5341
|
+
y: ry,
|
|
5342
|
+
size: rhythmSize,
|
|
5343
|
+
shape: rhythmShape
|
|
5344
|
+
});
|
|
5345
|
+
spatialGrid.insert({
|
|
5346
|
+
x: rx,
|
|
5347
|
+
y: ry,
|
|
5348
|
+
size: rhythmSize,
|
|
5349
|
+
shape: rhythmShape
|
|
5350
|
+
});
|
|
5351
|
+
}
|
|
5352
|
+
extrasSpent += rhythmCount * styleCost;
|
|
5353
|
+
} else // Drain RNG — each rhythm step consumes ~3 rng calls for colors
|
|
5059
5354
|
for(let r = 0; r < rhythmCount; r++){
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
if ($1f63dc64b5593c73$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5064
|
-
rhythmSize *= rhythmDecay;
|
|
5065
|
-
if (rhythmSize < adjustedMinSize) break;
|
|
5066
|
-
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5067
|
-
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5068
|
-
const rhythmFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5069
|
-
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5070
|
-
fillColor: rhythmFill,
|
|
5071
|
-
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5072
|
-
strokeWidth: strokeWidth * 0.7,
|
|
5073
|
-
size: rhythmSize,
|
|
5074
|
-
rotation: rotation + (r + 1) * 12,
|
|
5075
|
-
proportionType: "GOLDEN_RATIO",
|
|
5076
|
-
renderStyle: finalRenderStyle,
|
|
5077
|
-
rng: rng
|
|
5078
|
-
});
|
|
5079
|
-
shapePositions.push({
|
|
5080
|
-
x: rx,
|
|
5081
|
-
y: ry,
|
|
5082
|
-
size: rhythmSize,
|
|
5083
|
-
shape: rhythmShape
|
|
5084
|
-
});
|
|
5085
|
-
spatialGrid.insert({
|
|
5086
|
-
x: rx,
|
|
5087
|
-
y: ry,
|
|
5088
|
-
size: rhythmSize,
|
|
5089
|
-
shape: rhythmShape
|
|
5090
|
-
});
|
|
5355
|
+
rng();
|
|
5356
|
+
rng();
|
|
5357
|
+
rng();
|
|
5091
5358
|
}
|
|
5092
5359
|
}
|
|
5093
5360
|
}
|
|
5094
5361
|
}
|
|
5095
5362
|
// Reset blend mode for post-processing passes
|
|
5096
5363
|
ctx.globalCompositeOperation = "source-over";
|
|
5364
|
+
if (_dt) {
|
|
5365
|
+
_dt.shapeCount = shapePositions.length;
|
|
5366
|
+
_dt.extraCount = extrasSpent;
|
|
5367
|
+
}
|
|
5368
|
+
_mark("5_shape_layers");
|
|
5097
5369
|
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
5098
5370
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
5099
5371
|
// with a tinted background wash, creating a "peek through" effect.
|
|
@@ -5152,15 +5424,28 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5152
5424
|
ctx.restore();
|
|
5153
5425
|
}
|
|
5154
5426
|
}
|
|
5427
|
+
_mark("5g_portals");
|
|
5155
5428
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
5429
|
+
// Optimized: collect all segments into width-quantized buckets, then
|
|
5430
|
+
// render each bucket as a single batched path. This reduces
|
|
5431
|
+
// beginPath/stroke calls from O(segments) to O(buckets).
|
|
5156
5432
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
5157
5433
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
5434
|
+
// Width buckets — 6 buckets cover the taper×pressure range
|
|
5435
|
+
const FLOW_WIDTH_BUCKETS = 6;
|
|
5436
|
+
const flowBuckets = [];
|
|
5437
|
+
for(let b = 0; b < FLOW_WIDTH_BUCKETS; b++)flowBuckets.push([]);
|
|
5438
|
+
// Track the representative width for each bucket
|
|
5439
|
+
const flowBucketWidths = new Array(FLOW_WIDTH_BUCKETS);
|
|
5440
|
+
// Pre-compute max possible width for bucket assignment
|
|
5441
|
+
let globalMaxFlowWidth = 0;
|
|
5158
5442
|
for(let i = 0; i < numFlowLines; i++){
|
|
5159
5443
|
let fx = rng() * width;
|
|
5160
5444
|
let fy = rng() * height;
|
|
5161
5445
|
const steps = 30 + Math.floor(rng() * 40);
|
|
5162
5446
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
5163
5447
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
5448
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
5164
5449
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
5165
5450
|
const lineColorStart = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
5166
5451
|
const lineColorEnd = (0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
@@ -5182,19 +5467,22 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5182
5467
|
continue;
|
|
5183
5468
|
}
|
|
5184
5469
|
const t = s / steps;
|
|
5185
|
-
// Taper + pressure
|
|
5186
5470
|
const taper = 1 - t * 0.8;
|
|
5187
5471
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
5188
|
-
|
|
5189
|
-
|
|
5472
|
+
const segWidth = startWidth * taper * pressure;
|
|
5473
|
+
const segAlpha = lineAlpha * taper;
|
|
5190
5474
|
const lineColor = t < 0.5 ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $b5a262d09b87e373$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5475
|
+
// Quantize width into bucket
|
|
5476
|
+
const bucketIdx = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(segWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5477
|
+
flowBuckets[bucketIdx].push({
|
|
5478
|
+
x1: prevX,
|
|
5479
|
+
y1: prevY,
|
|
5480
|
+
x2: fx,
|
|
5481
|
+
y2: fy,
|
|
5482
|
+
color: lineColor,
|
|
5483
|
+
alpha: segAlpha
|
|
5484
|
+
});
|
|
5485
|
+
flowBucketWidths[bucketIdx] = segWidth;
|
|
5198
5486
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
5199
5487
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
5200
5488
|
const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
|
|
@@ -5210,12 +5498,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5210
5498
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
5211
5499
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
5212
5500
|
const bTaper = 1 - bs / branchSteps * 0.9;
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5501
|
+
const bSegWidth = branchWidth * bTaper;
|
|
5502
|
+
const bAlpha = lineAlpha * taper * bTaper * 0.6;
|
|
5503
|
+
const bBucket = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(bSegWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5504
|
+
flowBuckets[bBucket].push({
|
|
5505
|
+
x1: bPrevX,
|
|
5506
|
+
y1: bPrevY,
|
|
5507
|
+
x2: bx,
|
|
5508
|
+
y2: by,
|
|
5509
|
+
color: lineColor,
|
|
5510
|
+
alpha: bAlpha
|
|
5511
|
+
});
|
|
5512
|
+
flowBucketWidths[bBucket] = bSegWidth;
|
|
5219
5513
|
bPrevX = bx;
|
|
5220
5514
|
bPrevY = by;
|
|
5221
5515
|
}
|
|
@@ -5224,7 +5518,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5224
5518
|
prevY = fy;
|
|
5225
5519
|
}
|
|
5226
5520
|
}
|
|
5521
|
+
// Render flow line buckets — one batched path per width bucket
|
|
5522
|
+
// Within each bucket, further sub-batch by quantized alpha (4 levels)
|
|
5523
|
+
ctx.lineCap = "round";
|
|
5524
|
+
const FLOW_ALPHA_BUCKETS = 4;
|
|
5525
|
+
for(let wb = 0; wb < FLOW_WIDTH_BUCKETS; wb++){
|
|
5526
|
+
const segs = flowBuckets[wb];
|
|
5527
|
+
if (segs.length === 0) continue;
|
|
5528
|
+
ctx.lineWidth = flowBucketWidths[wb];
|
|
5529
|
+
// Sub-bucket by alpha
|
|
5530
|
+
const alphaSubs = [];
|
|
5531
|
+
for(let a = 0; a < FLOW_ALPHA_BUCKETS; a++)alphaSubs.push([]);
|
|
5532
|
+
let maxAlpha = 0;
|
|
5533
|
+
for(let j = 0; j < segs.length; j++)if (segs[j].alpha > maxAlpha) maxAlpha = segs[j].alpha;
|
|
5534
|
+
for(let j = 0; j < segs.length; j++){
|
|
5535
|
+
const ai = Math.min(FLOW_ALPHA_BUCKETS - 1, Math.floor(segs[j].alpha / (maxAlpha || 1) * FLOW_ALPHA_BUCKETS));
|
|
5536
|
+
alphaSubs[ai].push(segs[j]);
|
|
5537
|
+
}
|
|
5538
|
+
for(let ai = 0; ai < FLOW_ALPHA_BUCKETS; ai++){
|
|
5539
|
+
const sub = alphaSubs[ai];
|
|
5540
|
+
if (sub.length === 0) continue;
|
|
5541
|
+
// Use the median segment's alpha and color as representative
|
|
5542
|
+
const rep = sub[Math.floor(sub.length / 2)];
|
|
5543
|
+
ctx.globalAlpha = rep.alpha;
|
|
5544
|
+
ctx.strokeStyle = rep.color;
|
|
5545
|
+
ctx.beginPath();
|
|
5546
|
+
for(let j = 0; j < sub.length; j++){
|
|
5547
|
+
ctx.moveTo(sub[j].x1, sub[j].y1);
|
|
5548
|
+
ctx.lineTo(sub[j].x2, sub[j].y2);
|
|
5549
|
+
}
|
|
5550
|
+
ctx.stroke();
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
_mark("6_flow_lines");
|
|
5227
5554
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
5555
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
5228
5556
|
const energyArchetypes = [
|
|
5229
5557
|
"dense-chaotic",
|
|
5230
5558
|
"cosmic",
|
|
@@ -5235,8 +5563,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5235
5563
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
5236
5564
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
5237
5565
|
ctx.lineCap = "round";
|
|
5566
|
+
// Collect all energy segments with their computed state
|
|
5567
|
+
const ENERGY_ALPHA_BUCKETS = 3;
|
|
5568
|
+
const energyBuckets = [];
|
|
5569
|
+
for(let b = 0; b < ENERGY_ALPHA_BUCKETS; b++)energyBuckets.push([]);
|
|
5570
|
+
const energyAlphas = new Array(ENERGY_ALPHA_BUCKETS).fill(0);
|
|
5238
5571
|
for(let e = 0; e < energyCount; e++){
|
|
5239
|
-
// Pick a random shape to radiate from
|
|
5240
5572
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5241
5573
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
5242
5574
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -5248,16 +5580,40 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5248
5580
|
const sy = source.y + Math.sin(angle) * startDist;
|
|
5249
5581
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
5250
5582
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5583
|
+
const eAlpha = 0.04 + rng() * 0.06;
|
|
5584
|
+
const eColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5585
|
+
const eLw = (0.5 + rng() * 1.5) * scaleFactor;
|
|
5586
|
+
// Quantize alpha into bucket
|
|
5587
|
+
const bi = Math.min(ENERGY_ALPHA_BUCKETS - 1, Math.floor((eAlpha - 0.04) / 0.06 * ENERGY_ALPHA_BUCKETS));
|
|
5588
|
+
energyBuckets[bi].push({
|
|
5589
|
+
x1: sx,
|
|
5590
|
+
y1: sy,
|
|
5591
|
+
x2: ex,
|
|
5592
|
+
y2: ey,
|
|
5593
|
+
color: eColor,
|
|
5594
|
+
lw: eLw
|
|
5595
|
+
});
|
|
5596
|
+
energyAlphas[bi] = eAlpha;
|
|
5258
5597
|
}
|
|
5259
5598
|
}
|
|
5599
|
+
// Render batched energy lines
|
|
5600
|
+
for(let bi = 0; bi < ENERGY_ALPHA_BUCKETS; bi++){
|
|
5601
|
+
const segs = energyBuckets[bi];
|
|
5602
|
+
if (segs.length === 0) continue;
|
|
5603
|
+
ctx.globalAlpha = energyAlphas[bi];
|
|
5604
|
+
// Use median segment's color and width as representative
|
|
5605
|
+
const rep = segs[Math.floor(segs.length / 2)];
|
|
5606
|
+
ctx.strokeStyle = rep.color;
|
|
5607
|
+
ctx.lineWidth = rep.lw;
|
|
5608
|
+
ctx.beginPath();
|
|
5609
|
+
for(let j = 0; j < segs.length; j++){
|
|
5610
|
+
ctx.moveTo(segs[j].x1, segs[j].y1);
|
|
5611
|
+
ctx.lineTo(segs[j].x2, segs[j].y2);
|
|
5612
|
+
}
|
|
5613
|
+
ctx.stroke();
|
|
5614
|
+
}
|
|
5260
5615
|
}
|
|
5616
|
+
_mark("6b_energy_lines");
|
|
5261
5617
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
5262
5618
|
if (symmetryMode !== "none") {
|
|
5263
5619
|
const canvas = ctx.canvas;
|
|
@@ -5278,43 +5634,25 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5278
5634
|
}
|
|
5279
5635
|
ctx.restore();
|
|
5280
5636
|
}
|
|
5281
|
-
|
|
5637
|
+
_mark("6c_symmetry");
|
|
5638
|
+
// ── 7. Noise texture overlay ─────────────────────────────────────
|
|
5639
|
+
// With density capped at 2500 dots, direct fillRect calls are far cheaper
|
|
5640
|
+
// than the getImageData/putImageData round-trip which copies the entire
|
|
5641
|
+
// pixel buffer (4 × width × height bytes) twice.
|
|
5282
5642
|
const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
|
|
5283
|
-
const
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
const
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5295
|
-
const idx = ((ny + dy) * width + (nx + dx)) * 4;
|
|
5296
|
-
// Alpha-blend the noise dot onto existing pixel data
|
|
5297
|
-
const srcA = alpha / 255;
|
|
5298
|
-
const invA = 1 - srcA;
|
|
5299
|
-
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5300
|
-
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5301
|
-
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5302
|
-
// Keep existing alpha
|
|
5303
|
-
}
|
|
5304
|
-
}
|
|
5305
|
-
ctx.putImageData(imageData, 0, 0);
|
|
5306
|
-
} catch {
|
|
5307
|
-
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5308
|
-
for(let i = 0; i < noiseDensity; i++){
|
|
5309
|
-
const nx = noiseRng() * width;
|
|
5310
|
-
const ny = noiseRng() * height;
|
|
5311
|
-
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5312
|
-
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5313
|
-
ctx.globalAlpha = alpha;
|
|
5314
|
-
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5315
|
-
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5316
|
-
}
|
|
5643
|
+
const rawNoiseDensity = Math.floor(width * height / 800);
|
|
5644
|
+
const noiseDensity = Math.min(rawNoiseDensity, 2500);
|
|
5645
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5646
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5647
|
+
const nx = noiseRng() * width;
|
|
5648
|
+
const ny = noiseRng() * height;
|
|
5649
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5650
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5651
|
+
ctx.globalAlpha = alpha;
|
|
5652
|
+
ctx.fillStyle = `rgb(${brightness},${brightness},${brightness})`;
|
|
5653
|
+
ctx.fillRect(nx, ny, pixelScale, pixelScale);
|
|
5317
5654
|
}
|
|
5655
|
+
_mark("7_noise_texture");
|
|
5318
5656
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
5319
5657
|
ctx.globalAlpha = 1;
|
|
5320
5658
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
@@ -5328,11 +5666,20 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5328
5666
|
vigGrad.addColorStop(1, vignetteColor);
|
|
5329
5667
|
ctx.fillStyle = vigGrad;
|
|
5330
5668
|
ctx.fillRect(0, 0, width, height);
|
|
5669
|
+
_mark("8_vignette");
|
|
5331
5670
|
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5671
|
+
// Optimized: batch all curves into alpha-quantized groups to reduce
|
|
5672
|
+
// beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
|
|
5332
5673
|
if (shapePositions.length > 1) {
|
|
5333
5674
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5334
5675
|
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5335
5676
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5677
|
+
// Collect curves into 3 alpha buckets
|
|
5678
|
+
const CURVE_ALPHA_BUCKETS = 3;
|
|
5679
|
+
const curveBuckets = [];
|
|
5680
|
+
const curveColors = [];
|
|
5681
|
+
const curveAlphas = new Array(CURVE_ALPHA_BUCKETS).fill(0);
|
|
5682
|
+
for(let b = 0; b < CURVE_ALPHA_BUCKETS; b++)curveBuckets.push([]);
|
|
5336
5683
|
for(let i = 0; i < numCurves; i++){
|
|
5337
5684
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
5338
5685
|
const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
|
|
@@ -5349,14 +5696,36 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5349
5696
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5350
5697
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5351
5698
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
5352
|
-
|
|
5353
|
-
|
|
5699
|
+
const curveAlpha = 0.06 + rng() * 0.1;
|
|
5700
|
+
const curveColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$90ad0e6170cf6af5)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5701
|
+
const bi = Math.min(CURVE_ALPHA_BUCKETS - 1, Math.floor((curveAlpha - 0.06) / 0.1 * CURVE_ALPHA_BUCKETS));
|
|
5702
|
+
curveBuckets[bi].push({
|
|
5703
|
+
ax: a.x,
|
|
5704
|
+
ay: a.y,
|
|
5705
|
+
cpx: cpx,
|
|
5706
|
+
cpy: cpy,
|
|
5707
|
+
bx: b.x,
|
|
5708
|
+
by: b.y
|
|
5709
|
+
});
|
|
5710
|
+
curveAlphas[bi] = curveAlpha;
|
|
5711
|
+
if (!curveColors[bi]) curveColors[bi] = curveColor;
|
|
5712
|
+
}
|
|
5713
|
+
// Render batched curves
|
|
5714
|
+
for(let bi = 0; bi < CURVE_ALPHA_BUCKETS; bi++){
|
|
5715
|
+
const curves = curveBuckets[bi];
|
|
5716
|
+
if (curves.length === 0) continue;
|
|
5717
|
+
ctx.globalAlpha = curveAlphas[bi];
|
|
5718
|
+
ctx.strokeStyle = curveColors[bi];
|
|
5354
5719
|
ctx.beginPath();
|
|
5355
|
-
|
|
5356
|
-
|
|
5720
|
+
for(let j = 0; j < curves.length; j++){
|
|
5721
|
+
const c = curves[j];
|
|
5722
|
+
ctx.moveTo(c.ax, c.ay);
|
|
5723
|
+
ctx.quadraticCurveTo(c.cpx, c.cpy, c.bx, c.by);
|
|
5724
|
+
}
|
|
5357
5725
|
ctx.stroke();
|
|
5358
5726
|
}
|
|
5359
5727
|
}
|
|
5728
|
+
_mark("9_connecting_curves");
|
|
5360
5729
|
// ── 10. Post-processing ────────────────────────────────────────
|
|
5361
5730
|
// 10a. Color grading — unified tone across the whole image
|
|
5362
5731
|
// Apply as a semi-transparent overlay in the grade hue
|
|
@@ -5416,6 +5785,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5416
5785
|
ctx.fillRect(0, 0, width, height);
|
|
5417
5786
|
ctx.globalCompositeOperation = "source-over";
|
|
5418
5787
|
}
|
|
5788
|
+
_mark("10_post_processing");
|
|
5419
5789
|
// ── 10e. Generative borders — archetype-driven decorative frames ──
|
|
5420
5790
|
{
|
|
5421
5791
|
ctx.save();
|
|
@@ -5471,11 +5841,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5471
5841
|
}
|
|
5472
5842
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5473
5843
|
// Vine tendrils — organic curving lines along edges
|
|
5844
|
+
// Optimized: batch all tendrils into a single path
|
|
5474
5845
|
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5475
5846
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5476
5847
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5477
5848
|
ctx.lineCap = "round";
|
|
5478
5849
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5850
|
+
ctx.beginPath();
|
|
5851
|
+
const leafPositions = [];
|
|
5479
5852
|
for(let t = 0; t < tendrilCount; t++){
|
|
5480
5853
|
// Start from a random edge point
|
|
5481
5854
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -5493,7 +5866,6 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5493
5866
|
tx = width - borderPad;
|
|
5494
5867
|
ty = borderRng() * height;
|
|
5495
5868
|
}
|
|
5496
|
-
ctx.beginPath();
|
|
5497
5869
|
ctx.moveTo(tx, ty);
|
|
5498
5870
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5499
5871
|
for(let s = 0; s < segs; s++){
|
|
@@ -5507,14 +5879,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5507
5879
|
ty = cpy3;
|
|
5508
5880
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5509
5881
|
}
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5882
|
+
// Collect leaf positions for batch fill
|
|
5883
|
+
if (borderRng() < 0.6) leafPositions.push({
|
|
5884
|
+
x: tx,
|
|
5885
|
+
y: ty,
|
|
5886
|
+
r: borderPad * (0.15 + borderRng() * 0.2)
|
|
5887
|
+
});
|
|
5888
|
+
}
|
|
5889
|
+
ctx.stroke();
|
|
5890
|
+
// Batch all leaf dots into a single fill
|
|
5891
|
+
if (leafPositions.length > 0) {
|
|
5892
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5893
|
+
ctx.beginPath();
|
|
5894
|
+
for (const leaf of leafPositions){
|
|
5895
|
+
ctx.moveTo(leaf.x + leaf.r, leaf.y);
|
|
5896
|
+
ctx.arc(leaf.x, leaf.y, leaf.r, 0, Math.PI * 2);
|
|
5517
5897
|
}
|
|
5898
|
+
ctx.fill();
|
|
5518
5899
|
}
|
|
5519
5900
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5520
5901
|
// Star-studded arcs along edges
|
|
@@ -5529,8 +5910,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5529
5910
|
ctx.beginPath();
|
|
5530
5911
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5531
5912
|
ctx.stroke();
|
|
5532
|
-
// Scatter small stars along the border region
|
|
5913
|
+
// Scatter small stars along the border region — batched into single path
|
|
5533
5914
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5915
|
+
ctx.beginPath();
|
|
5534
5916
|
for(let s = 0; s < starCount; s++){
|
|
5535
5917
|
const edge = Math.floor(borderRng() * 4);
|
|
5536
5918
|
let sx, sy;
|
|
@@ -5549,7 +5931,6 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5549
5931
|
}
|
|
5550
5932
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5551
5933
|
// 4-point star
|
|
5552
|
-
ctx.beginPath();
|
|
5553
5934
|
for(let p = 0; p < 8; p++){
|
|
5554
5935
|
const a = p / 8 * Math.PI * 2;
|
|
5555
5936
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -5559,8 +5940,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5559
5940
|
else ctx.lineTo(px2, py2);
|
|
5560
5941
|
}
|
|
5561
5942
|
ctx.closePath();
|
|
5562
|
-
ctx.fill();
|
|
5563
5943
|
}
|
|
5944
|
+
ctx.fill();
|
|
5564
5945
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5565
5946
|
// Thin single rule — understated elegance
|
|
5566
5947
|
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
@@ -5571,6 +5952,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5571
5952
|
// Other archetypes: no border (intentional — not every image needs one)
|
|
5572
5953
|
ctx.restore();
|
|
5573
5954
|
}
|
|
5955
|
+
_mark("10e_borders");
|
|
5574
5956
|
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
5575
5957
|
{
|
|
5576
5958
|
const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
|
|
@@ -5638,6 +6020,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5638
6020
|
ctx.restore();
|
|
5639
6021
|
}
|
|
5640
6022
|
ctx.globalAlpha = 1;
|
|
6023
|
+
_mark("11_signature");
|
|
5641
6024
|
}
|
|
5642
6025
|
|
|
5643
6026
|
|