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/module.js
CHANGED
|
@@ -517,13 +517,21 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
|
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
519
|
// ── Standalone color utilities ──────────────────────────────────────
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
520
|
+
// ── Cached hex→RGB parse — avoids repeated parseInt/substring on hot path ──
|
|
521
|
+
const $9d614e7d77fc2947$var$_rgbCache = new Map();
|
|
522
|
+
const $9d614e7d77fc2947$var$_RGB_CACHE_MAX = 512;
|
|
523
|
+
/** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. Cached. */ function $9d614e7d77fc2947$var$hexToRgb(hex) {
|
|
524
|
+
let cached = $9d614e7d77fc2947$var$_rgbCache.get(hex);
|
|
525
|
+
if (cached) return cached;
|
|
526
|
+
const c = hex.charAt(0) === "#" ? hex.substring(1) : hex;
|
|
527
|
+
cached = [
|
|
523
528
|
parseInt(c.substring(0, 2), 16),
|
|
524
529
|
parseInt(c.substring(2, 4), 16),
|
|
525
530
|
parseInt(c.substring(4, 6), 16)
|
|
526
531
|
];
|
|
532
|
+
if ($9d614e7d77fc2947$var$_rgbCache.size >= $9d614e7d77fc2947$var$_RGB_CACHE_MAX) $9d614e7d77fc2947$var$_rgbCache.clear();
|
|
533
|
+
$9d614e7d77fc2947$var$_rgbCache.set(hex, cached);
|
|
534
|
+
return cached;
|
|
527
535
|
}
|
|
528
536
|
/** Format [r, g, b] back to #RRGGBB. */ function $9d614e7d77fc2947$var$rgbToHex(r, g, b) {
|
|
529
537
|
const clamp = (v)=>Math.max(0, Math.min(255, Math.round(v)));
|
|
@@ -580,7 +588,9 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
|
|
|
580
588
|
}
|
|
581
589
|
function $9d614e7d77fc2947$export$f2121afcad3d553f(hex, alpha) {
|
|
582
590
|
const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex);
|
|
583
|
-
|
|
591
|
+
// Quantize alpha to 3 decimal places without toFixed overhead
|
|
592
|
+
const a = Math.round(alpha * 1000) / 1000;
|
|
593
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
584
594
|
}
|
|
585
595
|
function $9d614e7d77fc2947$export$fabac4600b87056(colors, rng) {
|
|
586
596
|
if (colors.length < 3) return {
|
|
@@ -660,12 +670,21 @@ function $9d614e7d77fc2947$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
|
660
670
|
const [h, s, l] = $9d614e7d77fc2947$var$hexToHsl(hex);
|
|
661
671
|
return $9d614e7d77fc2947$var$hslToHex($9d614e7d77fc2947$var$shiftHueToward(h, target, amount), s, l);
|
|
662
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* Compute relative luminance of a hex color (0 = black, 1 = white).
|
|
675
|
+
* Uses the sRGB luminance formula from WCAG. Cached.
|
|
676
|
+
*/ const $9d614e7d77fc2947$var$_lumCache = new Map();
|
|
663
677
|
function $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe(hex) {
|
|
678
|
+
let cached = $9d614e7d77fc2947$var$_lumCache.get(hex);
|
|
679
|
+
if (cached !== undefined) return cached;
|
|
664
680
|
const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex).map((c)=>{
|
|
665
681
|
const s = c / 255;
|
|
666
682
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
667
683
|
});
|
|
668
|
-
|
|
684
|
+
cached = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
685
|
+
if ($9d614e7d77fc2947$var$_lumCache.size >= 512) $9d614e7d77fc2947$var$_lumCache.clear();
|
|
686
|
+
$9d614e7d77fc2947$var$_lumCache.set(hex, cached);
|
|
687
|
+
return cached;
|
|
669
688
|
}
|
|
670
689
|
function $9d614e7d77fc2947$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
|
|
671
690
|
const fgLum = $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe(fgHex);
|
|
@@ -1101,21 +1120,31 @@ const $8bde0a7ee87832b5$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
|
|
|
1101
1120
|
(0, $79312e33271883e9$export$e46c5570db033611)(ctx, size, finalConfig);
|
|
1102
1121
|
const gridSize = 8;
|
|
1103
1122
|
const unit = size / gridSize;
|
|
1123
|
+
const radius = unit / 2;
|
|
1124
|
+
// Pre-compute the 8 star-point angle pairs (cos/sin) — avoids 648 trig calls
|
|
1125
|
+
const starPoints = [];
|
|
1126
|
+
for(let k = 0; k < 8; k++){
|
|
1127
|
+
const angle = Math.PI / 4 * k;
|
|
1128
|
+
const angle2 = angle + Math.PI / 4;
|
|
1129
|
+
starPoints.push({
|
|
1130
|
+
c1: Math.cos(angle) * radius,
|
|
1131
|
+
s1: Math.sin(angle) * radius,
|
|
1132
|
+
c2: Math.cos(angle2) * radius,
|
|
1133
|
+
s2: Math.sin(angle2) * radius
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1104
1136
|
ctx.beginPath();
|
|
1105
1137
|
// Create base grid
|
|
1106
|
-
for(let i = 0; i <= gridSize; i++)
|
|
1138
|
+
for(let i = 0; i <= gridSize; i++){
|
|
1107
1139
|
const x = (i - gridSize / 2) * unit;
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const y2 = y + radius * Math.sin(angle + Math.PI / 4);
|
|
1117
|
-
ctx.moveTo(x1, y1);
|
|
1118
|
-
ctx.lineTo(x2, y2);
|
|
1140
|
+
for(let j = 0; j <= gridSize; j++){
|
|
1141
|
+
const y = (j - gridSize / 2) * unit;
|
|
1142
|
+
// Draw star pattern at each intersection using pre-computed offsets
|
|
1143
|
+
for(let k = 0; k < 8; k++){
|
|
1144
|
+
const sp = starPoints[k];
|
|
1145
|
+
ctx.moveTo(x + sp.c1, y + sp.s1);
|
|
1146
|
+
ctx.lineTo(x + sp.c2, y + sp.s2);
|
|
1147
|
+
}
|
|
1119
1148
|
}
|
|
1120
1149
|
}
|
|
1121
1150
|
ctx.stroke();
|
|
@@ -1435,20 +1464,23 @@ const $d63629e16208c310$export$eeae7765f05012e2 = (ctx, size)=>{
|
|
|
1435
1464
|
const $d63629e16208c310$export$3355220a8108efc3 = (ctx, size)=>{
|
|
1436
1465
|
const outerRadius = size / 2;
|
|
1437
1466
|
const innerRadius = size / 4;
|
|
1438
|
-
|
|
1467
|
+
// Adaptive step count: fewer segments for small shapes where detail isn't visible.
|
|
1468
|
+
// 36×36 = 1296 segments at full size; at size < 60 we drop to 16×16 = 256.
|
|
1469
|
+
const steps = size < 60 ? 16 : size < 150 ? 24 : 36;
|
|
1470
|
+
const TWO_PI = Math.PI * 2;
|
|
1471
|
+
const angleStep = TWO_PI / steps;
|
|
1439
1472
|
ctx.beginPath();
|
|
1440
1473
|
for(let i = 0; i < steps; i++){
|
|
1441
|
-
const angle1 = i
|
|
1442
|
-
|
|
1474
|
+
const angle1 = i * angleStep;
|
|
1475
|
+
const cosA = Math.cos(angle1);
|
|
1476
|
+
const sinA = Math.sin(angle1);
|
|
1443
1477
|
for(let j = 0; j < steps; j++){
|
|
1444
|
-
const phi1 = j
|
|
1445
|
-
const phi2 =
|
|
1446
|
-
const
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
ctx.moveTo(x1, y1);
|
|
1451
|
-
ctx.lineTo(x2, y2);
|
|
1478
|
+
const phi1 = j * angleStep;
|
|
1479
|
+
const phi2 = phi1 + angleStep;
|
|
1480
|
+
const r1 = outerRadius + innerRadius * Math.cos(phi1);
|
|
1481
|
+
const r2 = outerRadius + innerRadius * Math.cos(phi2);
|
|
1482
|
+
ctx.moveTo(r1 * cosA, r1 * sinA);
|
|
1483
|
+
ctx.lineTo(r2 * cosA, r2 * sinA);
|
|
1452
1484
|
}
|
|
1453
1485
|
}
|
|
1454
1486
|
};
|
|
@@ -2011,6 +2043,43 @@ const $9beb8f41637c29fd$var$RENDER_STYLES = [
|
|
|
2011
2043
|
function $9beb8f41637c29fd$export$9fd4e64b2acd410e(rng) {
|
|
2012
2044
|
return $9beb8f41637c29fd$var$RENDER_STYLES[Math.floor(rng() * $9beb8f41637c29fd$var$RENDER_STYLES.length)];
|
|
2013
2045
|
}
|
|
2046
|
+
const $9beb8f41637c29fd$export$2f738f61a8c15e07 = {
|
|
2047
|
+
"fill-and-stroke": 1,
|
|
2048
|
+
"fill-only": 0.5,
|
|
2049
|
+
"stroke-only": 1,
|
|
2050
|
+
"double-stroke": 1.5,
|
|
2051
|
+
"dashed": 1,
|
|
2052
|
+
"watercolor": 7,
|
|
2053
|
+
"hatched": 3,
|
|
2054
|
+
"incomplete": 1,
|
|
2055
|
+
"stipple": 90,
|
|
2056
|
+
"stencil": 2,
|
|
2057
|
+
"noise-grain": 400,
|
|
2058
|
+
"wood-grain": 10,
|
|
2059
|
+
"marble-vein": 4,
|
|
2060
|
+
"fabric-weave": 6,
|
|
2061
|
+
"hand-drawn": 5
|
|
2062
|
+
};
|
|
2063
|
+
function $9beb8f41637c29fd$export$909ab0580e273f19(style) {
|
|
2064
|
+
switch(style){
|
|
2065
|
+
case "noise-grain":
|
|
2066
|
+
return "hatched";
|
|
2067
|
+
case "stipple":
|
|
2068
|
+
return "dashed";
|
|
2069
|
+
case "wood-grain":
|
|
2070
|
+
return "hatched";
|
|
2071
|
+
case "watercolor":
|
|
2072
|
+
return "fill-and-stroke";
|
|
2073
|
+
case "fabric-weave":
|
|
2074
|
+
return "hatched";
|
|
2075
|
+
case "hand-drawn":
|
|
2076
|
+
return "fill-and-stroke";
|
|
2077
|
+
case "marble-vein":
|
|
2078
|
+
return "stroke-only";
|
|
2079
|
+
default:
|
|
2080
|
+
return style;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2014
2083
|
function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
2015
2084
|
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
|
|
2016
2085
|
ctx.save();
|
|
@@ -2130,6 +2199,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2130
2199
|
case "hatched":
|
|
2131
2200
|
{
|
|
2132
2201
|
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
2202
|
+
// Optimized: batch all parallel lines into a single path per pass
|
|
2133
2203
|
const savedAlphaH = ctx.globalAlpha;
|
|
2134
2204
|
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
2135
2205
|
ctx.fill();
|
|
@@ -2141,28 +2211,28 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2141
2211
|
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
2142
2212
|
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
2143
2213
|
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
2144
|
-
// Draw parallel lines across the bounding box
|
|
2214
|
+
// Draw parallel lines across the bounding box — batched into single path
|
|
2145
2215
|
const extent = size * 0.8;
|
|
2146
2216
|
const cos = Math.cos(hatchAngle);
|
|
2147
2217
|
const sin = Math.sin(hatchAngle);
|
|
2218
|
+
ctx.beginPath();
|
|
2148
2219
|
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
2149
|
-
ctx.beginPath();
|
|
2150
2220
|
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
2151
2221
|
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
2152
|
-
ctx.stroke();
|
|
2153
2222
|
}
|
|
2223
|
+
ctx.stroke();
|
|
2154
2224
|
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
2155
2225
|
if (!rng || rng() < 0.5) {
|
|
2156
2226
|
const crossAngle = hatchAngle + Math.PI / 2;
|
|
2157
2227
|
const cos2 = Math.cos(crossAngle);
|
|
2158
2228
|
const sin2 = Math.sin(crossAngle);
|
|
2159
2229
|
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
2230
|
+
ctx.beginPath();
|
|
2160
2231
|
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
2161
|
-
ctx.beginPath();
|
|
2162
2232
|
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
2163
2233
|
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
2164
|
-
ctx.stroke();
|
|
2165
2234
|
}
|
|
2235
|
+
ctx.stroke();
|
|
2166
2236
|
}
|
|
2167
2237
|
ctx.restore();
|
|
2168
2238
|
ctx.globalAlpha = savedAlphaH;
|
|
@@ -2200,6 +2270,8 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2200
2270
|
case "stipple":
|
|
2201
2271
|
{
|
|
2202
2272
|
// Dot-fill texture — clip to shape, then scatter dots
|
|
2273
|
+
// Optimized: use fillRect instead of arc for dots (much cheaper to render),
|
|
2274
|
+
// and cap total dot count to avoid O(size²) blowup on large shapes.
|
|
2203
2275
|
const savedAlphaS = ctx.globalAlpha;
|
|
2204
2276
|
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
2205
2277
|
ctx.fill(); // ghost fill
|
|
@@ -2207,16 +2279,20 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2207
2279
|
ctx.save();
|
|
2208
2280
|
ctx.clip();
|
|
2209
2281
|
const dotSpacing = Math.max(2, size * 0.03);
|
|
2210
|
-
const
|
|
2282
|
+
const extentS = size * 0.55;
|
|
2283
|
+
// Cap total dots: beyond ~900 (30×30 grid) the visual density plateaus
|
|
2284
|
+
const maxDotsPerAxis = Math.min(Math.ceil(extentS * 2 / dotSpacing), 30);
|
|
2285
|
+
const actualSpacing = extentS * 2 / maxDotsPerAxis;
|
|
2211
2286
|
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
2212
|
-
for(let
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2287
|
+
for(let xi = 0; xi < maxDotsPerAxis; xi++){
|
|
2288
|
+
const dx = -extentS + xi * actualSpacing;
|
|
2289
|
+
for(let yi = 0; yi < maxDotsPerAxis; yi++){
|
|
2290
|
+
const dy = -extentS + yi * actualSpacing;
|
|
2291
|
+
const jx = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2292
|
+
const jy = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
|
|
2293
|
+
const dotD = rng ? actualSpacing * (0.3 + rng() * 0.4) : actualSpacing * 0.4;
|
|
2294
|
+
ctx.fillRect(dx + jx - dotD * 0.5, dy + jy - dotD * 0.5, dotD, dotD);
|
|
2295
|
+
}
|
|
2220
2296
|
}
|
|
2221
2297
|
ctx.restore();
|
|
2222
2298
|
ctx.globalAlpha = savedAlphaS;
|
|
@@ -2249,6 +2325,9 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2249
2325
|
case "noise-grain":
|
|
2250
2326
|
{
|
|
2251
2327
|
// Procedural noise grain texture clipped to shape boundary
|
|
2328
|
+
// Optimized: cap grid to max 40×40 = 1600 dots (was unbounded at O(size²)),
|
|
2329
|
+
// quantize alpha into buckets to minimize globalAlpha state changes,
|
|
2330
|
+
// and batch dots by brightness (black/white) × alpha bucket
|
|
2252
2331
|
const savedAlphaN = ctx.globalAlpha;
|
|
2253
2332
|
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
2254
2333
|
ctx.fill(); // base tint
|
|
@@ -2257,17 +2336,47 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2257
2336
|
ctx.clip();
|
|
2258
2337
|
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
2259
2338
|
const extentN = size * 0.55;
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
const
|
|
2264
|
-
const
|
|
2265
|
-
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2339
|
+
if (rng) {
|
|
2340
|
+
// Cap grid to max 40 dots per axis — beyond this the grain is
|
|
2341
|
+
// visually indistinguishable but cost scales quadratically.
|
|
2342
|
+
const maxGrainPerAxis = Math.min(Math.ceil(extentN * 2 / grainSpacing), 40);
|
|
2343
|
+
const actualGrainSpacing = extentN * 2 / maxGrainPerAxis;
|
|
2344
|
+
// 4 alpha buckets: 0.2, 0.3, 0.4, 0.5 — covers the 0.15-0.50 range
|
|
2345
|
+
const BUCKETS = 4;
|
|
2346
|
+
const bucketMin = 0.15;
|
|
2347
|
+
const bucketRange = 0.35;
|
|
2348
|
+
// [black_bucket0, black_bucket1, ..., white_bucket0, ...]
|
|
2349
|
+
const buckets = [];
|
|
2350
|
+
for(let i = 0; i < BUCKETS * 2; i++)buckets.push([]);
|
|
2351
|
+
for(let xi = 0; xi < maxGrainPerAxis; xi++){
|
|
2352
|
+
const gx = -extentN + xi * actualGrainSpacing;
|
|
2353
|
+
for(let yi = 0; yi < maxGrainPerAxis; yi++){
|
|
2354
|
+
const gy = -extentN + yi * actualGrainSpacing;
|
|
2355
|
+
const jx = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2356
|
+
const jy = (rng() - 0.5) * actualGrainSpacing * 1.2;
|
|
2357
|
+
const isWhite = rng() > 0.5;
|
|
2358
|
+
const dotAlpha = bucketMin + rng() * bucketRange;
|
|
2359
|
+
const dotSize = actualGrainSpacing * (0.3 + rng() * 0.5);
|
|
2360
|
+
const bucketIdx = Math.min(BUCKETS - 1, Math.floor((dotAlpha - bucketMin) / bucketRange * BUCKETS));
|
|
2361
|
+
const offset = isWhite ? BUCKETS : 0;
|
|
2362
|
+
buckets[offset + bucketIdx].push({
|
|
2363
|
+
x: gx + jx,
|
|
2364
|
+
y: gy + jy,
|
|
2365
|
+
s: dotSize
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
// Render each bucket: 2 colors × 4 alpha levels = 8 state changes total
|
|
2370
|
+
for(let color = 0; color < 2; color++){
|
|
2371
|
+
ctx.fillStyle = color === 0 ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
2372
|
+
for(let b = 0; b < BUCKETS; b++){
|
|
2373
|
+
const dots = buckets[color * BUCKETS + b];
|
|
2374
|
+
if (dots.length === 0) continue;
|
|
2375
|
+
const alpha = bucketMin + (b + 0.5) / BUCKETS * bucketRange;
|
|
2376
|
+
ctx.globalAlpha = savedAlphaN * alpha;
|
|
2377
|
+
for(let i = 0; i < dots.length; i++)ctx.fillRect(dots[i].x, dots[i].y, dots[i].s, dots[i].s);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2271
2380
|
}
|
|
2272
2381
|
ctx.restore();
|
|
2273
2382
|
ctx.fillStyle = fillColor;
|
|
@@ -2280,6 +2389,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2280
2389
|
case "wood-grain":
|
|
2281
2390
|
{
|
|
2282
2391
|
// Parallel wavy lines simulating wood grain, clipped to shape
|
|
2392
|
+
// Optimized: batch all grain lines into a single path, increased step from 2 to 4
|
|
2283
2393
|
const savedAlphaW = ctx.globalAlpha;
|
|
2284
2394
|
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
2285
2395
|
ctx.fill(); // base tint
|
|
@@ -2295,17 +2405,19 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2295
2405
|
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
2296
2406
|
const cosG = Math.cos(grainAngle);
|
|
2297
2407
|
const sinG = Math.sin(grainAngle);
|
|
2408
|
+
const waveCoeff = waveFreq * Math.PI;
|
|
2409
|
+
const invExtentW = 1 / extentW;
|
|
2410
|
+
// Batch all grain lines into a single path
|
|
2411
|
+
ctx.beginPath();
|
|
2298
2412
|
for(let d = -extentW; d <= extentW; d += grainLineSpacing){
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
if (t === -extentW) ctx.moveTo(px, py);
|
|
2305
|
-
else ctx.lineTo(px, py);
|
|
2413
|
+
const firstWave = Math.sin(-extentW * invExtentW * waveCoeff) * waveAmp;
|
|
2414
|
+
ctx.moveTo(-extentW * cosG - (d + firstWave) * sinG, -extentW * sinG + (d + firstWave) * cosG);
|
|
2415
|
+
for(let t = -extentW + 4; t <= extentW; t += 4){
|
|
2416
|
+
const wave = Math.sin(t * invExtentW * waveCoeff) * waveAmp;
|
|
2417
|
+
ctx.lineTo(t * cosG - (d + wave) * sinG, t * sinG + (d + wave) * cosG);
|
|
2306
2418
|
}
|
|
2307
|
-
ctx.stroke();
|
|
2308
2419
|
}
|
|
2420
|
+
ctx.stroke();
|
|
2309
2421
|
ctx.restore();
|
|
2310
2422
|
ctx.globalAlpha = savedAlphaW;
|
|
2311
2423
|
ctx.globalAlpha *= 0.35;
|
|
@@ -2367,6 +2479,7 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2367
2479
|
case "fabric-weave":
|
|
2368
2480
|
{
|
|
2369
2481
|
// Interlocking horizontal/vertical threads clipped to shape
|
|
2482
|
+
// Optimized: batch all horizontal threads into one path, all vertical into another
|
|
2370
2483
|
const savedAlphaF = ctx.globalAlpha;
|
|
2371
2484
|
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
2372
2485
|
ctx.fill(); // ghost base
|
|
@@ -2376,26 +2489,24 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2376
2489
|
const threadSpacing = Math.max(2, size * 0.04);
|
|
2377
2490
|
const extentF = size * 0.55;
|
|
2378
2491
|
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
2492
|
+
// Horizontal threads — batched
|
|
2379
2493
|
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
2380
|
-
|
|
2494
|
+
ctx.beginPath();
|
|
2381
2495
|
for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2382
|
-
ctx.beginPath();
|
|
2383
2496
|
ctx.moveTo(-extentF, y);
|
|
2384
2497
|
ctx.lineTo(extentF, y);
|
|
2385
|
-
ctx.stroke();
|
|
2386
2498
|
}
|
|
2387
|
-
|
|
2499
|
+
ctx.stroke();
|
|
2500
|
+
// Vertical threads (offset by half spacing for weave effect) — batched
|
|
2388
2501
|
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
2389
2502
|
ctx.strokeStyle = fillColor;
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
ctx.lineTo(x, y + threadSpacing);
|
|
2396
|
-
}
|
|
2397
|
-
ctx.stroke();
|
|
2503
|
+
ctx.beginPath();
|
|
2504
|
+
for(let x = -extentF; x <= extentF; x += threadSpacing * 2)for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
|
|
2505
|
+
// Over-under: draw segment, skip segment
|
|
2506
|
+
ctx.moveTo(x, y);
|
|
2507
|
+
ctx.lineTo(x, y + threadSpacing);
|
|
2398
2508
|
}
|
|
2509
|
+
ctx.stroke();
|
|
2399
2510
|
ctx.strokeStyle = strokeColor;
|
|
2400
2511
|
ctx.restore();
|
|
2401
2512
|
ctx.globalAlpha = savedAlphaF;
|
|
@@ -2461,14 +2572,17 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2461
2572
|
ctx.translate(x, y);
|
|
2462
2573
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2463
2574
|
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2464
|
-
|
|
2575
|
+
// Skip shadow entirely for small shapes (< 20px) — the blur is expensive
|
|
2576
|
+
// and visually imperceptible at that scale.
|
|
2577
|
+
const useShadow = size >= 20;
|
|
2578
|
+
if (useShadow && lightAngle !== undefined) {
|
|
2465
2579
|
const shadowDist = size * 0.035;
|
|
2466
2580
|
const shadowBlurR = size * 0.06;
|
|
2467
2581
|
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2468
2582
|
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2469
2583
|
ctx.shadowBlur = shadowBlurR;
|
|
2470
2584
|
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2471
|
-
} else if (glowRadius > 0) {
|
|
2585
|
+
} else if (useShadow && glowRadius > 0) {
|
|
2472
2586
|
// Glow / shadow effect (legacy path)
|
|
2473
2587
|
ctx.shadowBlur = glowRadius;
|
|
2474
2588
|
ctx.shadowColor = glowColor || fillColor;
|
|
@@ -2492,30 +2606,27 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2492
2606
|
$9beb8f41637c29fd$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2493
2607
|
}
|
|
2494
2608
|
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2609
|
+
// Only reset if we actually set shadow (avoids unnecessary state changes)
|
|
2610
|
+
if (useShadow && (lightAngle !== undefined || glowRadius > 0)) {
|
|
2611
|
+
ctx.shadowBlur = 0;
|
|
2612
|
+
ctx.shadowOffsetX = 0;
|
|
2613
|
+
ctx.shadowOffsetY = 0;
|
|
2614
|
+
ctx.shadowColor = "transparent";
|
|
2615
|
+
}
|
|
2499
2616
|
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2500
|
-
|
|
2617
|
+
// Skip for small shapes (< 30px) — gradient creation + composite op
|
|
2618
|
+
// switch is expensive and the highlight is invisible at small sizes.
|
|
2619
|
+
if (lightAngle !== undefined && size > 30 && rng) {
|
|
2501
2620
|
const hlRadius = size * 0.35;
|
|
2502
2621
|
const hlDist = size * 0.15;
|
|
2503
2622
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2504
2623
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2505
2624
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2506
|
-
//
|
|
2507
|
-
//
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2512
|
-
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2513
|
-
// Blend toward white but keep a hint of the fill's warmth
|
|
2514
|
-
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2515
|
-
}
|
|
2516
|
-
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2517
|
-
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2518
|
-
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2625
|
+
// Use a simple white highlight — the per-shape hex parse was expensive
|
|
2626
|
+
// and the visual difference from tinted highlights is negligible.
|
|
2627
|
+
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2628
|
+
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2629
|
+
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
2519
2630
|
const savedOp = ctx.globalCompositeOperation;
|
|
2520
2631
|
ctx.globalCompositeOperation = "soft-light";
|
|
2521
2632
|
ctx.fillStyle = hlGrad;
|
|
@@ -4057,6 +4168,46 @@ function $3faa2521b78398cf$export$f1142fd7da4d6590(rng) {
|
|
|
4057
4168
|
}
|
|
4058
4169
|
|
|
4059
4170
|
|
|
4171
|
+
// ── Render style cost weights (normalized: fill-and-stroke = 1) ─────
|
|
4172
|
+
// Based on benchmark measurements. Used by the complexity budget to
|
|
4173
|
+
// cap total rendering work and downgrade expensive styles when needed.
|
|
4174
|
+
const $b623126c6e9cbb71$var$RENDER_STYLE_COST = {
|
|
4175
|
+
"fill-and-stroke": 1,
|
|
4176
|
+
"fill-only": 0.5,
|
|
4177
|
+
"stroke-only": 1,
|
|
4178
|
+
"double-stroke": 1.5,
|
|
4179
|
+
"dashed": 1,
|
|
4180
|
+
"watercolor": 7,
|
|
4181
|
+
"hatched": 3,
|
|
4182
|
+
"incomplete": 1,
|
|
4183
|
+
"stipple": 90,
|
|
4184
|
+
"stencil": 2,
|
|
4185
|
+
"noise-grain": 400,
|
|
4186
|
+
"wood-grain": 10,
|
|
4187
|
+
"marble-vein": 4,
|
|
4188
|
+
"fabric-weave": 6,
|
|
4189
|
+
"hand-drawn": 5
|
|
4190
|
+
};
|
|
4191
|
+
function $b623126c6e9cbb71$var$downgradeRenderStyle(style) {
|
|
4192
|
+
switch(style){
|
|
4193
|
+
case "noise-grain":
|
|
4194
|
+
return "hatched";
|
|
4195
|
+
case "stipple":
|
|
4196
|
+
return "dashed";
|
|
4197
|
+
case "wood-grain":
|
|
4198
|
+
return "hatched";
|
|
4199
|
+
case "watercolor":
|
|
4200
|
+
return "fill-and-stroke";
|
|
4201
|
+
case "fabric-weave":
|
|
4202
|
+
return "hatched";
|
|
4203
|
+
case "hand-drawn":
|
|
4204
|
+
return "fill-and-stroke";
|
|
4205
|
+
case "marble-vein":
|
|
4206
|
+
return "stroke-only";
|
|
4207
|
+
default:
|
|
4208
|
+
return style;
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4060
4211
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
4061
4212
|
const $b623126c6e9cbb71$var$SACRED_SHAPES = [
|
|
4062
4213
|
"mandala",
|
|
@@ -4442,6 +4593,15 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4442
4593
|
...(0, $2bfb6a1ccb7a82ae$export$c2f8e0cc249a8d8f),
|
|
4443
4594
|
...config
|
|
4444
4595
|
};
|
|
4596
|
+
const _dt = finalConfig._debugTiming;
|
|
4597
|
+
const _t = _dt ? ()=>performance.now() : undefined;
|
|
4598
|
+
let _p = _t ? _t() : 0;
|
|
4599
|
+
function _mark(name) {
|
|
4600
|
+
if (!_dt || !_t) return;
|
|
4601
|
+
const now = _t();
|
|
4602
|
+
_dt.phases[name] = now - _p;
|
|
4603
|
+
_p = now;
|
|
4604
|
+
}
|
|
4445
4605
|
const rng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash));
|
|
4446
4606
|
// ── 0. Select archetype — fundamentally different visual personality ──
|
|
4447
4607
|
const archetype = (0, $3faa2521b78398cf$export$f1142fd7da4d6590)(rng);
|
|
@@ -4475,12 +4635,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4475
4635
|
const adjustedMaxSize = maxShapeSize * scaleFactor;
|
|
4476
4636
|
const cx = width / 2;
|
|
4477
4637
|
const cy = height / 2;
|
|
4638
|
+
_mark("0_setup");
|
|
4478
4639
|
// ── 1. Background ──────────────────────────────────────────────
|
|
4479
4640
|
const bgRadius = Math.hypot(cx, cy);
|
|
4480
4641
|
$b623126c6e9cbb71$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
|
|
4481
4642
|
// Gradient mesh overlay — 3-4 color control points for richer backgrounds
|
|
4643
|
+
// Use source-over instead of soft-light for cheaper compositing
|
|
4482
4644
|
const meshPoints = 3 + Math.floor(rng() * 2);
|
|
4483
|
-
ctx.
|
|
4645
|
+
ctx.globalAlpha = 1;
|
|
4484
4646
|
for(let i = 0; i < meshPoints; i++){
|
|
4485
4647
|
const mx = rng() * width;
|
|
4486
4648
|
const my = rng() * height;
|
|
@@ -4489,95 +4651,103 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4489
4651
|
const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
|
|
4490
4652
|
grad.addColorStop(0, (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
|
|
4491
4653
|
grad.addColorStop(1, "rgba(0,0,0,0)");
|
|
4492
|
-
ctx.globalAlpha = 1;
|
|
4493
4654
|
ctx.fillStyle = grad;
|
|
4494
|
-
|
|
4655
|
+
// Clip to gradient bounding box — avoids blending transparent pixels
|
|
4656
|
+
const gx = Math.max(0, mx - mRadius);
|
|
4657
|
+
const gy = Math.max(0, my - mRadius);
|
|
4658
|
+
const gw = Math.min(width, mx + mRadius) - gx;
|
|
4659
|
+
const gh = Math.min(height, my + mRadius) - gy;
|
|
4660
|
+
ctx.fillRect(gx, gy, gw, gh);
|
|
4495
4661
|
}
|
|
4496
|
-
ctx.globalCompositeOperation = "source-over";
|
|
4497
4662
|
// Compute average background luminance for contrast enforcement
|
|
4498
4663
|
const bgLum = ((0, $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
|
|
4499
4664
|
// ── 1b. Layered background — archetype-coherent shapes ─────────
|
|
4665
|
+
// Use source-over with pre-multiplied alpha instead of soft-light
|
|
4666
|
+
// for much cheaper compositing (soft-light requires per-pixel blend)
|
|
4500
4667
|
const bgShapeCount = 3 + Math.floor(rng() * 4);
|
|
4501
|
-
ctx.globalCompositeOperation = "soft-light";
|
|
4502
4668
|
for(let i = 0; i < bgShapeCount; i++){
|
|
4503
4669
|
const bx = rng() * width;
|
|
4504
4670
|
const by = rng() * height;
|
|
4505
4671
|
const bSize = width * 0.3 + rng() * width * 0.5;
|
|
4506
4672
|
const bColor = (0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng);
|
|
4507
|
-
ctx.globalAlpha = 0.03 + rng() * 0.05;
|
|
4673
|
+
ctx.globalAlpha = (0.03 + rng() * 0.05) * 0.5; // halved to compensate for source-over vs soft-light
|
|
4508
4674
|
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(bColor, 0.15);
|
|
4509
4675
|
ctx.beginPath();
|
|
4510
4676
|
// Use archetype-appropriate background shapes
|
|
4511
|
-
if (archetype.name === "geometric-precision" || archetype.name === "op-art")
|
|
4512
|
-
ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
|
|
4677
|
+
if (archetype.name === "geometric-precision" || archetype.name === "op-art") ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
|
|
4513
4678
|
else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
|
|
4514
4679
|
ctx.fill();
|
|
4515
4680
|
}
|
|
4516
|
-
// Subtle concentric rings from center
|
|
4681
|
+
// Subtle concentric rings from center — batched into single stroke
|
|
4517
4682
|
const ringCount = 2 + Math.floor(rng() * 3);
|
|
4518
4683
|
ctx.globalAlpha = 0.02 + rng() * 0.03;
|
|
4519
4684
|
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
4520
4685
|
ctx.lineWidth = 1 * scaleFactor;
|
|
4686
|
+
ctx.beginPath();
|
|
4521
4687
|
for(let i = 1; i <= ringCount; i++){
|
|
4522
4688
|
const r = Math.min(width, height) * 0.15 * i;
|
|
4523
|
-
ctx.
|
|
4689
|
+
ctx.moveTo(cx + r, cy);
|
|
4524
4690
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
4525
|
-
ctx.stroke();
|
|
4526
4691
|
}
|
|
4527
|
-
ctx.
|
|
4692
|
+
ctx.stroke();
|
|
4528
4693
|
// ── 1c. Background pattern layer — subtle textured paper ───────
|
|
4529
4694
|
const bgPatternRoll = rng();
|
|
4530
4695
|
if (bgPatternRoll < 0.6) {
|
|
4531
4696
|
ctx.save();
|
|
4532
|
-
ctx.globalCompositeOperation = "soft-light";
|
|
4533
4697
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4534
4698
|
const patternColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4535
4699
|
if (bgPatternRoll < 0.2) {
|
|
4536
|
-
// Dot grid —
|
|
4537
|
-
const dotSpacing = Math.max(
|
|
4538
|
-
const
|
|
4700
|
+
// Dot grid — use fillRect instead of arcs (much cheaper, no path building)
|
|
4701
|
+
const dotSpacing = Math.max(12, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4702
|
+
const dotDiam = Math.max(1, Math.round(dotSpacing * 0.16));
|
|
4539
4703
|
ctx.globalAlpha = patternOpacity;
|
|
4540
4704
|
ctx.fillStyle = patternColor;
|
|
4541
|
-
|
|
4542
|
-
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4543
|
-
ctx.
|
|
4544
|
-
|
|
4705
|
+
let dotCount = 0;
|
|
4706
|
+
for(let px = 0; px < width && dotCount < 2000; px += dotSpacing)for(let py = 0; py < height && dotCount < 2000; py += dotSpacing){
|
|
4707
|
+
ctx.fillRect(px, py, dotDiam, dotDiam);
|
|
4708
|
+
dotCount++;
|
|
4545
4709
|
}
|
|
4546
|
-
ctx.fill();
|
|
4547
4710
|
} else if (bgPatternRoll < 0.4) {
|
|
4548
|
-
// Diagonal lines — batched into a single path
|
|
4549
|
-
const lineSpacing = Math.max(
|
|
4711
|
+
// Diagonal lines — batched into a single path, capped at 300 lines
|
|
4712
|
+
const lineSpacing = Math.max(10, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4550
4713
|
ctx.globalAlpha = patternOpacity;
|
|
4551
4714
|
ctx.strokeStyle = patternColor;
|
|
4552
4715
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4553
4716
|
const diag = Math.hypot(width, height);
|
|
4554
4717
|
ctx.beginPath();
|
|
4555
|
-
|
|
4718
|
+
let lineCount = 0;
|
|
4719
|
+
for(let d = -diag; d < diag && lineCount < 300; d += lineSpacing){
|
|
4556
4720
|
ctx.moveTo(d, 0);
|
|
4557
4721
|
ctx.lineTo(d + height, height);
|
|
4722
|
+
lineCount++;
|
|
4558
4723
|
}
|
|
4559
4724
|
ctx.stroke();
|
|
4560
4725
|
} else {
|
|
4561
|
-
// Tessellation — hexagonal grid,
|
|
4562
|
-
const tessSize = Math.max(
|
|
4726
|
+
// Tessellation — hexagonal grid, capped at 500 hexagons
|
|
4727
|
+
const tessSize = Math.max(15, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4563
4728
|
const tessH = tessSize * Math.sqrt(3);
|
|
4564
4729
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4565
4730
|
ctx.strokeStyle = patternColor;
|
|
4566
4731
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4732
|
+
// Pre-compute hex vertex offsets (avoid trig per vertex)
|
|
4733
|
+
const hexVx = [];
|
|
4734
|
+
const hexVy = [];
|
|
4735
|
+
for(let s = 0; s < 6; s++){
|
|
4736
|
+
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4737
|
+
hexVx.push(Math.cos(angle) * tessSize * 0.5);
|
|
4738
|
+
hexVy.push(Math.sin(angle) * tessSize * 0.5);
|
|
4739
|
+
}
|
|
4567
4740
|
ctx.beginPath();
|
|
4568
|
-
|
|
4741
|
+
let hexCount = 0;
|
|
4742
|
+
for(let row = 0; row * tessH < height + tessH && hexCount < 500; row++){
|
|
4569
4743
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4570
|
-
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4744
|
+
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5 && hexCount < 500; col++){
|
|
4571
4745
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4572
4746
|
const hy = row * tessH;
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
4576
|
-
const vy = hy + Math.sin(angle) * tessSize * 0.5;
|
|
4577
|
-
if (s === 0) ctx.moveTo(vx, vy);
|
|
4578
|
-
else ctx.lineTo(vx, vy);
|
|
4579
|
-
}
|
|
4747
|
+
ctx.moveTo(hx + hexVx[0], hy + hexVy[0]);
|
|
4748
|
+
for(let s = 1; s < 6; s++)ctx.lineTo(hx + hexVx[s], hy + hexVy[s]);
|
|
4580
4749
|
ctx.closePath();
|
|
4750
|
+
hexCount++;
|
|
4581
4751
|
}
|
|
4582
4752
|
}
|
|
4583
4753
|
ctx.stroke();
|
|
@@ -4585,6 +4755,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4585
4755
|
ctx.restore();
|
|
4586
4756
|
}
|
|
4587
4757
|
ctx.globalCompositeOperation = "source-over";
|
|
4758
|
+
_mark("1_background");
|
|
4588
4759
|
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4589
4760
|
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)];
|
|
4590
4761
|
const symRoll = rng();
|
|
@@ -4668,19 +4839,20 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4668
4839
|
ctx.beginPath();
|
|
4669
4840
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
4670
4841
|
ctx.stroke();
|
|
4671
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
4842
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
4672
4843
|
if (rng() < 0.5) {
|
|
4673
4844
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
4674
4845
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
4675
4846
|
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
4847
|
+
ctx.beginPath();
|
|
4676
4848
|
for(let d = 0; d < dotCount; d++){
|
|
4677
4849
|
const angle = rng() * Math.PI * 2;
|
|
4678
4850
|
const dist = rng() * zone.radius * 0.7;
|
|
4679
4851
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
4680
|
-
ctx.
|
|
4852
|
+
ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
|
|
4681
4853
|
ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
|
|
4682
|
-
ctx.fill();
|
|
4683
4854
|
}
|
|
4855
|
+
ctx.fill();
|
|
4684
4856
|
}
|
|
4685
4857
|
// ~30% chance: thin concentric ring inside
|
|
4686
4858
|
if (rng() < 0.3) {
|
|
@@ -4694,6 +4866,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4694
4866
|
}
|
|
4695
4867
|
}
|
|
4696
4868
|
ctx.globalAlpha = 1;
|
|
4869
|
+
_mark("2_3_composition_focal");
|
|
4697
4870
|
// ── 4. Flow field — simplex noise for organic variation ─────────
|
|
4698
4871
|
// Create a seeded simplex noise field (unique per hash)
|
|
4699
4872
|
const noiseFieldRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 333));
|
|
@@ -4770,8 +4943,29 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4770
4943
|
shape: heroShape
|
|
4771
4944
|
});
|
|
4772
4945
|
}
|
|
4946
|
+
_mark("4_flowfield_hero");
|
|
4773
4947
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4774
4948
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4949
|
+
// ── Complexity budget — caps total rendering work ──────────────
|
|
4950
|
+
// Budget scales with pixel area so larger canvases get proportionally
|
|
4951
|
+
// more headroom. The multiplier extras (glazing, echoes, nesting,
|
|
4952
|
+
// constellations, rhythm) are gated behind the budget; when it runs
|
|
4953
|
+
// low they are skipped. When it's exhausted, expensive render styles
|
|
4954
|
+
// are downgraded to cheaper alternatives.
|
|
4955
|
+
//
|
|
4956
|
+
// RNG values are always consumed even when skipping, so the
|
|
4957
|
+
// deterministic sequence for shapes that *do* render is preserved.
|
|
4958
|
+
const pixelArea = width * height;
|
|
4959
|
+
const BUDGET_PER_MEGAPIXEL = 6000; // cost units per 1M pixels
|
|
4960
|
+
let complexityBudget = pixelArea / 1000000 * BUDGET_PER_MEGAPIXEL;
|
|
4961
|
+
const totalBudget = complexityBudget;
|
|
4962
|
+
const budgetForExtras = complexityBudget * 0.25; // reserve 25% for multiplier extras
|
|
4963
|
+
let extrasSpent = 0;
|
|
4964
|
+
// Hard cap on clip-heavy render styles (stipple, noise-grain).
|
|
4965
|
+
// These generate O(size²) fillRect calls per shape and dominate
|
|
4966
|
+
// worst-case render time. Cap scales with pixel area.
|
|
4967
|
+
const MAX_CLIP_HEAVY_SHAPES = Math.max(4, Math.floor(8 * (pixelArea / 1000000)));
|
|
4968
|
+
let clipHeavyCount = 0;
|
|
4775
4969
|
for(let layer = 0; layer < layers; layer++){
|
|
4776
4970
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
4777
4971
|
const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
|
|
@@ -4860,7 +5054,26 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4860
5054
|
const shapeRenderStyle = (0, $24064302523652b1$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
|
|
4861
5055
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
4862
5056
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
4863
|
-
|
|
5057
|
+
let finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
|
|
5058
|
+
// Budget check: downgrade expensive styles proportionally —
|
|
5059
|
+
// the more expensive the style, the earlier it gets downgraded.
|
|
5060
|
+
// noise-grain (400) downgrades when budget < 20% remaining,
|
|
5061
|
+
// stipple (90) when < 82%, wood-grain (10) when < 98%.
|
|
5062
|
+
let styleCost = $b623126c6e9cbb71$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5063
|
+
if (styleCost > 3) {
|
|
5064
|
+
const downgradeThreshold = Math.min(0.85, styleCost / 500);
|
|
5065
|
+
if (complexityBudget < totalBudget * (1 - downgradeThreshold)) {
|
|
5066
|
+
finalRenderStyle = $b623126c6e9cbb71$var$downgradeRenderStyle(finalRenderStyle);
|
|
5067
|
+
styleCost = $b623126c6e9cbb71$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
// Hard cap: clip-heavy styles (stipple, noise-grain) are limited
|
|
5071
|
+
// to MAX_CLIP_HEAVY_SHAPES total across the entire render.
|
|
5072
|
+
if ((finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
5073
|
+
finalRenderStyle = $b623126c6e9cbb71$var$downgradeRenderStyle(finalRenderStyle);
|
|
5074
|
+
styleCost = $b623126c6e9cbb71$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
5075
|
+
}
|
|
5076
|
+
if (finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") clipHeavyCount++;
|
|
4864
5077
|
// Consistent light direction — subtle shadow offset
|
|
4865
5078
|
const shadowDist = hasGlow ? 0 : size * 0.02;
|
|
4866
5079
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
@@ -4915,30 +5128,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4915
5128
|
lightAngle: lightAngle,
|
|
4916
5129
|
scaleFactor: scaleFactor
|
|
4917
5130
|
};
|
|
4918
|
-
if (shouldMirror)
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
5131
|
+
if (shouldMirror) {
|
|
5132
|
+
(0, $9beb8f41637c29fd$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
5133
|
+
...shapeConfig,
|
|
5134
|
+
mirrorAxis: mirrorAxis,
|
|
5135
|
+
mirrorGap: size * (0.1 + rng() * 0.3)
|
|
5136
|
+
});
|
|
5137
|
+
complexityBudget -= styleCost * 2; // mirrored = 2 shapes
|
|
5138
|
+
} else {
|
|
5139
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
|
|
5140
|
+
complexityBudget -= styleCost;
|
|
5141
|
+
}
|
|
5142
|
+
// ── Extras budget gate — skip multiplier sections when over budget ──
|
|
5143
|
+
const extrasAllowed = extrasSpent < budgetForExtras;
|
|
4924
5144
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
4925
5145
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
4926
5146
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
5147
|
+
if (extrasAllowed) {
|
|
5148
|
+
for(let g = 0; g < glazePasses; g++){
|
|
5149
|
+
const glazeScale = 1 - (g + 1) * 0.12;
|
|
5150
|
+
const glazeAlpha = 0.08 + g * 0.04;
|
|
5151
|
+
ctx.globalAlpha = glazeAlpha;
|
|
5152
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
|
|
5153
|
+
fillColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
|
|
5154
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
5155
|
+
strokeWidth: 0,
|
|
5156
|
+
size: size * glazeScale,
|
|
5157
|
+
rotation: rotation,
|
|
5158
|
+
proportionType: "GOLDEN_RATIO",
|
|
5159
|
+
renderStyle: "fill-only",
|
|
5160
|
+
rng: rng
|
|
5161
|
+
});
|
|
5162
|
+
}
|
|
5163
|
+
extrasSpent += glazePasses;
|
|
4941
5164
|
}
|
|
5165
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
4942
5166
|
}
|
|
4943
5167
|
shapePositions.push({
|
|
4944
5168
|
x: finalX,
|
|
@@ -4956,37 +5180,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4956
5180
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4957
5181
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
4958
5182
|
const echoAngle = rng() * Math.PI * 2;
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
5183
|
+
if (extrasAllowed) {
|
|
5184
|
+
for(let e = 0; e < echoCount; e++){
|
|
5185
|
+
const echoScale = 0.3 - e * 0.08;
|
|
5186
|
+
const echoDist = size * (0.6 + e * 0.4);
|
|
5187
|
+
const echoX = finalX + Math.cos(echoAngle) * echoDist;
|
|
5188
|
+
const echoY = finalY + Math.sin(echoAngle) * echoDist;
|
|
5189
|
+
const echoSize = size * Math.max(0.1, echoScale);
|
|
5190
|
+
if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
|
|
5191
|
+
ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
|
|
5192
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
|
|
5193
|
+
fillColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
|
|
5194
|
+
strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.4),
|
|
5195
|
+
strokeWidth: strokeWidth * 0.6,
|
|
5196
|
+
size: echoSize,
|
|
5197
|
+
rotation: rotation + (e + 1) * 15,
|
|
5198
|
+
proportionType: "GOLDEN_RATIO",
|
|
5199
|
+
renderStyle: finalRenderStyle,
|
|
5200
|
+
rng: rng
|
|
5201
|
+
});
|
|
5202
|
+
shapePositions.push({
|
|
5203
|
+
x: echoX,
|
|
5204
|
+
y: echoY,
|
|
5205
|
+
size: echoSize,
|
|
5206
|
+
shape: shape
|
|
5207
|
+
});
|
|
5208
|
+
spatialGrid.insert({
|
|
5209
|
+
x: echoX,
|
|
5210
|
+
y: echoY,
|
|
5211
|
+
size: echoSize,
|
|
5212
|
+
shape: shape
|
|
5213
|
+
});
|
|
5214
|
+
}
|
|
5215
|
+
extrasSpent += echoCount * styleCost;
|
|
4989
5216
|
}
|
|
5217
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
4990
5218
|
}
|
|
4991
5219
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
4992
5220
|
// Focal depth: shapes near focal points get more detail
|
|
@@ -4994,7 +5222,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4994
5222
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
4995
5223
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
4996
5224
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
4997
|
-
for(let n = 0; n < innerCount; n++){
|
|
5225
|
+
if (extrasAllowed) for(let n = 0; n < innerCount; n++){
|
|
4998
5226
|
// Pick inner shape from palette affinities
|
|
4999
5227
|
const innerSizeFraction = size * 0.25 / adjustedMaxSize;
|
|
5000
5228
|
const innerShape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
|
|
@@ -5003,6 +5231,10 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5003
5231
|
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
5004
5232
|
const innerRot = rng() * 360;
|
|
5005
5233
|
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);
|
|
5234
|
+
let innerStyle = (0, $24064302523652b1$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng);
|
|
5235
|
+
// Apply clip-heavy cap to nested shapes too
|
|
5236
|
+
if ((innerStyle === "stipple" || innerStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) innerStyle = $b623126c6e9cbb71$var$downgradeRenderStyle(innerStyle);
|
|
5237
|
+
if (innerStyle === "stipple" || innerStyle === "noise-grain") clipHeavyCount++;
|
|
5006
5238
|
ctx.globalAlpha = layerOpacity * 0.7;
|
|
5007
5239
|
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
|
|
5008
5240
|
fillColor: innerFill,
|
|
@@ -5011,9 +5243,21 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5011
5243
|
size: innerSize,
|
|
5012
5244
|
rotation: innerRot,
|
|
5013
5245
|
proportionType: "GOLDEN_RATIO",
|
|
5014
|
-
renderStyle:
|
|
5246
|
+
renderStyle: innerStyle,
|
|
5015
5247
|
rng: rng
|
|
5016
5248
|
});
|
|
5249
|
+
extrasSpent += $b623126c6e9cbb71$var$RENDER_STYLE_COST[innerStyle] ?? 1;
|
|
5250
|
+
}
|
|
5251
|
+
else // Drain RNG to keep determinism — each nested shape consumes ~8 rng calls
|
|
5252
|
+
for(let n = 0; n < innerCount; n++){
|
|
5253
|
+
rng();
|
|
5254
|
+
rng();
|
|
5255
|
+
rng();
|
|
5256
|
+
rng();
|
|
5257
|
+
rng();
|
|
5258
|
+
rng();
|
|
5259
|
+
rng();
|
|
5260
|
+
rng();
|
|
5017
5261
|
}
|
|
5018
5262
|
}
|
|
5019
5263
|
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
@@ -5022,40 +5266,55 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5022
5266
|
const constellation = $b623126c6e9cbb71$var$CONSTELLATIONS[Math.floor(rng() * $b623126c6e9cbb71$var$CONSTELLATIONS.length)];
|
|
5023
5267
|
const members = constellation.build(rng, size);
|
|
5024
5268
|
const groupRotation = rng() * Math.PI * 2;
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5269
|
+
if (extrasAllowed) {
|
|
5270
|
+
const cosR = Math.cos(groupRotation);
|
|
5271
|
+
const sinR = Math.sin(groupRotation);
|
|
5272
|
+
for (const member of members){
|
|
5273
|
+
// Rotate the group offset by the group rotation
|
|
5274
|
+
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
5275
|
+
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
5276
|
+
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
5277
|
+
const memberFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
|
|
5278
|
+
const memberStroke = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
|
|
5279
|
+
ctx.globalAlpha = layerOpacity * 0.6;
|
|
5280
|
+
// Use the member's shape if available, otherwise fall back to palette
|
|
5281
|
+
const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
|
|
5282
|
+
let memberStyle = (0, $24064302523652b1$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng);
|
|
5283
|
+
// Apply clip-heavy cap to constellation members too
|
|
5284
|
+
if ((memberStyle === "stipple" || memberStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) memberStyle = $b623126c6e9cbb71$var$downgradeRenderStyle(memberStyle);
|
|
5285
|
+
if (memberStyle === "stipple" || memberStyle === "noise-grain") clipHeavyCount++;
|
|
5286
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
|
|
5287
|
+
fillColor: memberFill,
|
|
5288
|
+
strokeColor: memberStroke,
|
|
5289
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5290
|
+
size: member.size,
|
|
5291
|
+
rotation: member.rotation + groupRotation * 180 / Math.PI,
|
|
5292
|
+
proportionType: "GOLDEN_RATIO",
|
|
5293
|
+
renderStyle: memberStyle,
|
|
5294
|
+
rng: rng
|
|
5295
|
+
});
|
|
5296
|
+
shapePositions.push({
|
|
5297
|
+
x: mx,
|
|
5298
|
+
y: my,
|
|
5299
|
+
size: member.size,
|
|
5300
|
+
shape: memberShape
|
|
5301
|
+
});
|
|
5302
|
+
spatialGrid.insert({
|
|
5303
|
+
x: mx,
|
|
5304
|
+
y: my,
|
|
5305
|
+
size: member.size,
|
|
5306
|
+
shape: memberShape
|
|
5307
|
+
});
|
|
5308
|
+
extrasSpent += $b623126c6e9cbb71$var$RENDER_STYLE_COST[memberStyle] ?? 1;
|
|
5309
|
+
}
|
|
5310
|
+
} else // Drain RNG — each member consumes ~6 rng calls for colors/style
|
|
5311
|
+
for(let m = 0; m < members.length; m++){
|
|
5312
|
+
rng();
|
|
5313
|
+
rng();
|
|
5314
|
+
rng();
|
|
5315
|
+
rng();
|
|
5316
|
+
rng();
|
|
5317
|
+
rng();
|
|
5059
5318
|
}
|
|
5060
5319
|
}
|
|
5061
5320
|
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
@@ -5066,45 +5325,58 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5066
5325
|
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5067
5326
|
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5068
5327
|
const rhythmShape = shape; // same shape for visual rhythm
|
|
5069
|
-
|
|
5328
|
+
if (extrasAllowed) {
|
|
5329
|
+
let rhythmSize = size * 0.6;
|
|
5330
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5331
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5332
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5333
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5334
|
+
if ($b623126c6e9cbb71$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5335
|
+
rhythmSize *= rhythmDecay;
|
|
5336
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5337
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5338
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5339
|
+
const rhythmFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5340
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5341
|
+
fillColor: rhythmFill,
|
|
5342
|
+
strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5343
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5344
|
+
size: rhythmSize,
|
|
5345
|
+
rotation: rotation + (r + 1) * 12,
|
|
5346
|
+
proportionType: "GOLDEN_RATIO",
|
|
5347
|
+
renderStyle: finalRenderStyle,
|
|
5348
|
+
rng: rng
|
|
5349
|
+
});
|
|
5350
|
+
shapePositions.push({
|
|
5351
|
+
x: rx,
|
|
5352
|
+
y: ry,
|
|
5353
|
+
size: rhythmSize,
|
|
5354
|
+
shape: rhythmShape
|
|
5355
|
+
});
|
|
5356
|
+
spatialGrid.insert({
|
|
5357
|
+
x: rx,
|
|
5358
|
+
y: ry,
|
|
5359
|
+
size: rhythmSize,
|
|
5360
|
+
shape: rhythmShape
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5363
|
+
extrasSpent += rhythmCount * styleCost;
|
|
5364
|
+
} else // Drain RNG — each rhythm step consumes ~3 rng calls for colors
|
|
5070
5365
|
for(let r = 0; r < rhythmCount; r++){
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
if ($b623126c6e9cbb71$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5075
|
-
rhythmSize *= rhythmDecay;
|
|
5076
|
-
if (rhythmSize < adjustedMinSize) break;
|
|
5077
|
-
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5078
|
-
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5079
|
-
const rhythmFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5080
|
-
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5081
|
-
fillColor: rhythmFill,
|
|
5082
|
-
strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5083
|
-
strokeWidth: strokeWidth * 0.7,
|
|
5084
|
-
size: rhythmSize,
|
|
5085
|
-
rotation: rotation + (r + 1) * 12,
|
|
5086
|
-
proportionType: "GOLDEN_RATIO",
|
|
5087
|
-
renderStyle: finalRenderStyle,
|
|
5088
|
-
rng: rng
|
|
5089
|
-
});
|
|
5090
|
-
shapePositions.push({
|
|
5091
|
-
x: rx,
|
|
5092
|
-
y: ry,
|
|
5093
|
-
size: rhythmSize,
|
|
5094
|
-
shape: rhythmShape
|
|
5095
|
-
});
|
|
5096
|
-
spatialGrid.insert({
|
|
5097
|
-
x: rx,
|
|
5098
|
-
y: ry,
|
|
5099
|
-
size: rhythmSize,
|
|
5100
|
-
shape: rhythmShape
|
|
5101
|
-
});
|
|
5366
|
+
rng();
|
|
5367
|
+
rng();
|
|
5368
|
+
rng();
|
|
5102
5369
|
}
|
|
5103
5370
|
}
|
|
5104
5371
|
}
|
|
5105
5372
|
}
|
|
5106
5373
|
// Reset blend mode for post-processing passes
|
|
5107
5374
|
ctx.globalCompositeOperation = "source-over";
|
|
5375
|
+
if (_dt) {
|
|
5376
|
+
_dt.shapeCount = shapePositions.length;
|
|
5377
|
+
_dt.extraCount = extrasSpent;
|
|
5378
|
+
}
|
|
5379
|
+
_mark("5_shape_layers");
|
|
5108
5380
|
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
5109
5381
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
5110
5382
|
// with a tinted background wash, creating a "peek through" effect.
|
|
@@ -5163,15 +5435,28 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5163
5435
|
ctx.restore();
|
|
5164
5436
|
}
|
|
5165
5437
|
}
|
|
5438
|
+
_mark("5g_portals");
|
|
5166
5439
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
5440
|
+
// Optimized: collect all segments into width-quantized buckets, then
|
|
5441
|
+
// render each bucket as a single batched path. This reduces
|
|
5442
|
+
// beginPath/stroke calls from O(segments) to O(buckets).
|
|
5167
5443
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
5168
5444
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
5445
|
+
// Width buckets — 6 buckets cover the taper×pressure range
|
|
5446
|
+
const FLOW_WIDTH_BUCKETS = 6;
|
|
5447
|
+
const flowBuckets = [];
|
|
5448
|
+
for(let b = 0; b < FLOW_WIDTH_BUCKETS; b++)flowBuckets.push([]);
|
|
5449
|
+
// Track the representative width for each bucket
|
|
5450
|
+
const flowBucketWidths = new Array(FLOW_WIDTH_BUCKETS);
|
|
5451
|
+
// Pre-compute max possible width for bucket assignment
|
|
5452
|
+
let globalMaxFlowWidth = 0;
|
|
5169
5453
|
for(let i = 0; i < numFlowLines; i++){
|
|
5170
5454
|
let fx = rng() * width;
|
|
5171
5455
|
let fy = rng() * height;
|
|
5172
5456
|
const steps = 30 + Math.floor(rng() * 40);
|
|
5173
5457
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
5174
5458
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
5459
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
5175
5460
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
5176
5461
|
const lineColorStart = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
5177
5462
|
const lineColorEnd = (0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
|
|
@@ -5193,19 +5478,22 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5193
5478
|
continue;
|
|
5194
5479
|
}
|
|
5195
5480
|
const t = s / steps;
|
|
5196
|
-
// Taper + pressure
|
|
5197
5481
|
const taper = 1 - t * 0.8;
|
|
5198
5482
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
5199
|
-
|
|
5200
|
-
|
|
5483
|
+
const segWidth = startWidth * taper * pressure;
|
|
5484
|
+
const segAlpha = lineAlpha * taper;
|
|
5201
5485
|
const lineColor = t < 0.5 ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5486
|
+
// Quantize width into bucket
|
|
5487
|
+
const bucketIdx = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(segWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5488
|
+
flowBuckets[bucketIdx].push({
|
|
5489
|
+
x1: prevX,
|
|
5490
|
+
y1: prevY,
|
|
5491
|
+
x2: fx,
|
|
5492
|
+
y2: fy,
|
|
5493
|
+
color: lineColor,
|
|
5494
|
+
alpha: segAlpha
|
|
5495
|
+
});
|
|
5496
|
+
flowBucketWidths[bucketIdx] = segWidth;
|
|
5209
5497
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
5210
5498
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
5211
5499
|
const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
|
|
@@ -5221,12 +5509,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5221
5509
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
5222
5510
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
5223
5511
|
const bTaper = 1 - bs / branchSteps * 0.9;
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5512
|
+
const bSegWidth = branchWidth * bTaper;
|
|
5513
|
+
const bAlpha = lineAlpha * taper * bTaper * 0.6;
|
|
5514
|
+
const bBucket = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(bSegWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
|
|
5515
|
+
flowBuckets[bBucket].push({
|
|
5516
|
+
x1: bPrevX,
|
|
5517
|
+
y1: bPrevY,
|
|
5518
|
+
x2: bx,
|
|
5519
|
+
y2: by,
|
|
5520
|
+
color: lineColor,
|
|
5521
|
+
alpha: bAlpha
|
|
5522
|
+
});
|
|
5523
|
+
flowBucketWidths[bBucket] = bSegWidth;
|
|
5230
5524
|
bPrevX = bx;
|
|
5231
5525
|
bPrevY = by;
|
|
5232
5526
|
}
|
|
@@ -5235,7 +5529,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5235
5529
|
prevY = fy;
|
|
5236
5530
|
}
|
|
5237
5531
|
}
|
|
5532
|
+
// Render flow line buckets — one batched path per width bucket
|
|
5533
|
+
// Within each bucket, further sub-batch by quantized alpha (4 levels)
|
|
5534
|
+
ctx.lineCap = "round";
|
|
5535
|
+
const FLOW_ALPHA_BUCKETS = 4;
|
|
5536
|
+
for(let wb = 0; wb < FLOW_WIDTH_BUCKETS; wb++){
|
|
5537
|
+
const segs = flowBuckets[wb];
|
|
5538
|
+
if (segs.length === 0) continue;
|
|
5539
|
+
ctx.lineWidth = flowBucketWidths[wb];
|
|
5540
|
+
// Sub-bucket by alpha
|
|
5541
|
+
const alphaSubs = [];
|
|
5542
|
+
for(let a = 0; a < FLOW_ALPHA_BUCKETS; a++)alphaSubs.push([]);
|
|
5543
|
+
let maxAlpha = 0;
|
|
5544
|
+
for(let j = 0; j < segs.length; j++)if (segs[j].alpha > maxAlpha) maxAlpha = segs[j].alpha;
|
|
5545
|
+
for(let j = 0; j < segs.length; j++){
|
|
5546
|
+
const ai = Math.min(FLOW_ALPHA_BUCKETS - 1, Math.floor(segs[j].alpha / (maxAlpha || 1) * FLOW_ALPHA_BUCKETS));
|
|
5547
|
+
alphaSubs[ai].push(segs[j]);
|
|
5548
|
+
}
|
|
5549
|
+
for(let ai = 0; ai < FLOW_ALPHA_BUCKETS; ai++){
|
|
5550
|
+
const sub = alphaSubs[ai];
|
|
5551
|
+
if (sub.length === 0) continue;
|
|
5552
|
+
// Use the median segment's alpha and color as representative
|
|
5553
|
+
const rep = sub[Math.floor(sub.length / 2)];
|
|
5554
|
+
ctx.globalAlpha = rep.alpha;
|
|
5555
|
+
ctx.strokeStyle = rep.color;
|
|
5556
|
+
ctx.beginPath();
|
|
5557
|
+
for(let j = 0; j < sub.length; j++){
|
|
5558
|
+
ctx.moveTo(sub[j].x1, sub[j].y1);
|
|
5559
|
+
ctx.lineTo(sub[j].x2, sub[j].y2);
|
|
5560
|
+
}
|
|
5561
|
+
ctx.stroke();
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
_mark("6_flow_lines");
|
|
5238
5565
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
5566
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
5239
5567
|
const energyArchetypes = [
|
|
5240
5568
|
"dense-chaotic",
|
|
5241
5569
|
"cosmic",
|
|
@@ -5246,8 +5574,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5246
5574
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
5247
5575
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
5248
5576
|
ctx.lineCap = "round";
|
|
5577
|
+
// Collect all energy segments with their computed state
|
|
5578
|
+
const ENERGY_ALPHA_BUCKETS = 3;
|
|
5579
|
+
const energyBuckets = [];
|
|
5580
|
+
for(let b = 0; b < ENERGY_ALPHA_BUCKETS; b++)energyBuckets.push([]);
|
|
5581
|
+
const energyAlphas = new Array(ENERGY_ALPHA_BUCKETS).fill(0);
|
|
5249
5582
|
for(let e = 0; e < energyCount; e++){
|
|
5250
|
-
// Pick a random shape to radiate from
|
|
5251
5583
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5252
5584
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
5253
5585
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -5259,16 +5591,40 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5259
5591
|
const sy = source.y + Math.sin(angle) * startDist;
|
|
5260
5592
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
5261
5593
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5594
|
+
const eAlpha = 0.04 + rng() * 0.06;
|
|
5595
|
+
const eColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5596
|
+
const eLw = (0.5 + rng() * 1.5) * scaleFactor;
|
|
5597
|
+
// Quantize alpha into bucket
|
|
5598
|
+
const bi = Math.min(ENERGY_ALPHA_BUCKETS - 1, Math.floor((eAlpha - 0.04) / 0.06 * ENERGY_ALPHA_BUCKETS));
|
|
5599
|
+
energyBuckets[bi].push({
|
|
5600
|
+
x1: sx,
|
|
5601
|
+
y1: sy,
|
|
5602
|
+
x2: ex,
|
|
5603
|
+
y2: ey,
|
|
5604
|
+
color: eColor,
|
|
5605
|
+
lw: eLw
|
|
5606
|
+
});
|
|
5607
|
+
energyAlphas[bi] = eAlpha;
|
|
5269
5608
|
}
|
|
5270
5609
|
}
|
|
5610
|
+
// Render batched energy lines
|
|
5611
|
+
for(let bi = 0; bi < ENERGY_ALPHA_BUCKETS; bi++){
|
|
5612
|
+
const segs = energyBuckets[bi];
|
|
5613
|
+
if (segs.length === 0) continue;
|
|
5614
|
+
ctx.globalAlpha = energyAlphas[bi];
|
|
5615
|
+
// Use median segment's color and width as representative
|
|
5616
|
+
const rep = segs[Math.floor(segs.length / 2)];
|
|
5617
|
+
ctx.strokeStyle = rep.color;
|
|
5618
|
+
ctx.lineWidth = rep.lw;
|
|
5619
|
+
ctx.beginPath();
|
|
5620
|
+
for(let j = 0; j < segs.length; j++){
|
|
5621
|
+
ctx.moveTo(segs[j].x1, segs[j].y1);
|
|
5622
|
+
ctx.lineTo(segs[j].x2, segs[j].y2);
|
|
5623
|
+
}
|
|
5624
|
+
ctx.stroke();
|
|
5625
|
+
}
|
|
5271
5626
|
}
|
|
5627
|
+
_mark("6b_energy_lines");
|
|
5272
5628
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
5273
5629
|
if (symmetryMode !== "none") {
|
|
5274
5630
|
const canvas = ctx.canvas;
|
|
@@ -5289,43 +5645,25 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5289
5645
|
}
|
|
5290
5646
|
ctx.restore();
|
|
5291
5647
|
}
|
|
5292
|
-
|
|
5648
|
+
_mark("6c_symmetry");
|
|
5649
|
+
// ── 7. Noise texture overlay ─────────────────────────────────────
|
|
5650
|
+
// With density capped at 2500 dots, direct fillRect calls are far cheaper
|
|
5651
|
+
// than the getImageData/putImageData round-trip which copies the entire
|
|
5652
|
+
// pixel buffer (4 × width × height bytes) twice.
|
|
5293
5653
|
const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
|
|
5294
|
-
const
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
const
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5306
|
-
const idx = ((ny + dy) * width + (nx + dx)) * 4;
|
|
5307
|
-
// Alpha-blend the noise dot onto existing pixel data
|
|
5308
|
-
const srcA = alpha / 255;
|
|
5309
|
-
const invA = 1 - srcA;
|
|
5310
|
-
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5311
|
-
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5312
|
-
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5313
|
-
// Keep existing alpha
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
ctx.putImageData(imageData, 0, 0);
|
|
5317
|
-
} catch {
|
|
5318
|
-
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5319
|
-
for(let i = 0; i < noiseDensity; i++){
|
|
5320
|
-
const nx = noiseRng() * width;
|
|
5321
|
-
const ny = noiseRng() * height;
|
|
5322
|
-
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5323
|
-
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5324
|
-
ctx.globalAlpha = alpha;
|
|
5325
|
-
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5326
|
-
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5327
|
-
}
|
|
5654
|
+
const rawNoiseDensity = Math.floor(width * height / 800);
|
|
5655
|
+
const noiseDensity = Math.min(rawNoiseDensity, 2500);
|
|
5656
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5657
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5658
|
+
const nx = noiseRng() * width;
|
|
5659
|
+
const ny = noiseRng() * height;
|
|
5660
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5661
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5662
|
+
ctx.globalAlpha = alpha;
|
|
5663
|
+
ctx.fillStyle = `rgb(${brightness},${brightness},${brightness})`;
|
|
5664
|
+
ctx.fillRect(nx, ny, pixelScale, pixelScale);
|
|
5328
5665
|
}
|
|
5666
|
+
_mark("7_noise_texture");
|
|
5329
5667
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
5330
5668
|
ctx.globalAlpha = 1;
|
|
5331
5669
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
@@ -5339,11 +5677,20 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5339
5677
|
vigGrad.addColorStop(1, vignetteColor);
|
|
5340
5678
|
ctx.fillStyle = vigGrad;
|
|
5341
5679
|
ctx.fillRect(0, 0, width, height);
|
|
5680
|
+
_mark("8_vignette");
|
|
5342
5681
|
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5682
|
+
// Optimized: batch all curves into alpha-quantized groups to reduce
|
|
5683
|
+
// beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
|
|
5343
5684
|
if (shapePositions.length > 1) {
|
|
5344
5685
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5345
5686
|
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5346
5687
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5688
|
+
// Collect curves into 3 alpha buckets
|
|
5689
|
+
const CURVE_ALPHA_BUCKETS = 3;
|
|
5690
|
+
const curveBuckets = [];
|
|
5691
|
+
const curveColors = [];
|
|
5692
|
+
const curveAlphas = new Array(CURVE_ALPHA_BUCKETS).fill(0);
|
|
5693
|
+
for(let b = 0; b < CURVE_ALPHA_BUCKETS; b++)curveBuckets.push([]);
|
|
5347
5694
|
for(let i = 0; i < numCurves; i++){
|
|
5348
5695
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
5349
5696
|
const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
|
|
@@ -5360,14 +5707,36 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5360
5707
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5361
5708
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5362
5709
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
5363
|
-
|
|
5364
|
-
|
|
5710
|
+
const curveAlpha = 0.06 + rng() * 0.1;
|
|
5711
|
+
const curveColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$90ad0e6170cf6af5)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
|
|
5712
|
+
const bi = Math.min(CURVE_ALPHA_BUCKETS - 1, Math.floor((curveAlpha - 0.06) / 0.1 * CURVE_ALPHA_BUCKETS));
|
|
5713
|
+
curveBuckets[bi].push({
|
|
5714
|
+
ax: a.x,
|
|
5715
|
+
ay: a.y,
|
|
5716
|
+
cpx: cpx,
|
|
5717
|
+
cpy: cpy,
|
|
5718
|
+
bx: b.x,
|
|
5719
|
+
by: b.y
|
|
5720
|
+
});
|
|
5721
|
+
curveAlphas[bi] = curveAlpha;
|
|
5722
|
+
if (!curveColors[bi]) curveColors[bi] = curveColor;
|
|
5723
|
+
}
|
|
5724
|
+
// Render batched curves
|
|
5725
|
+
for(let bi = 0; bi < CURVE_ALPHA_BUCKETS; bi++){
|
|
5726
|
+
const curves = curveBuckets[bi];
|
|
5727
|
+
if (curves.length === 0) continue;
|
|
5728
|
+
ctx.globalAlpha = curveAlphas[bi];
|
|
5729
|
+
ctx.strokeStyle = curveColors[bi];
|
|
5365
5730
|
ctx.beginPath();
|
|
5366
|
-
|
|
5367
|
-
|
|
5731
|
+
for(let j = 0; j < curves.length; j++){
|
|
5732
|
+
const c = curves[j];
|
|
5733
|
+
ctx.moveTo(c.ax, c.ay);
|
|
5734
|
+
ctx.quadraticCurveTo(c.cpx, c.cpy, c.bx, c.by);
|
|
5735
|
+
}
|
|
5368
5736
|
ctx.stroke();
|
|
5369
5737
|
}
|
|
5370
5738
|
}
|
|
5739
|
+
_mark("9_connecting_curves");
|
|
5371
5740
|
// ── 10. Post-processing ────────────────────────────────────────
|
|
5372
5741
|
// 10a. Color grading — unified tone across the whole image
|
|
5373
5742
|
// Apply as a semi-transparent overlay in the grade hue
|
|
@@ -5427,6 +5796,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5427
5796
|
ctx.fillRect(0, 0, width, height);
|
|
5428
5797
|
ctx.globalCompositeOperation = "source-over";
|
|
5429
5798
|
}
|
|
5799
|
+
_mark("10_post_processing");
|
|
5430
5800
|
// ── 10e. Generative borders — archetype-driven decorative frames ──
|
|
5431
5801
|
{
|
|
5432
5802
|
ctx.save();
|
|
@@ -5482,11 +5852,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5482
5852
|
}
|
|
5483
5853
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5484
5854
|
// Vine tendrils — organic curving lines along edges
|
|
5855
|
+
// Optimized: batch all tendrils into a single path
|
|
5485
5856
|
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5486
5857
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5487
5858
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5488
5859
|
ctx.lineCap = "round";
|
|
5489
5860
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5861
|
+
ctx.beginPath();
|
|
5862
|
+
const leafPositions = [];
|
|
5490
5863
|
for(let t = 0; t < tendrilCount; t++){
|
|
5491
5864
|
// Start from a random edge point
|
|
5492
5865
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -5504,7 +5877,6 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5504
5877
|
tx = width - borderPad;
|
|
5505
5878
|
ty = borderRng() * height;
|
|
5506
5879
|
}
|
|
5507
|
-
ctx.beginPath();
|
|
5508
5880
|
ctx.moveTo(tx, ty);
|
|
5509
5881
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5510
5882
|
for(let s = 0; s < segs; s++){
|
|
@@ -5518,14 +5890,23 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5518
5890
|
ty = cpy3;
|
|
5519
5891
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5520
5892
|
}
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5893
|
+
// Collect leaf positions for batch fill
|
|
5894
|
+
if (borderRng() < 0.6) leafPositions.push({
|
|
5895
|
+
x: tx,
|
|
5896
|
+
y: ty,
|
|
5897
|
+
r: borderPad * (0.15 + borderRng() * 0.2)
|
|
5898
|
+
});
|
|
5899
|
+
}
|
|
5900
|
+
ctx.stroke();
|
|
5901
|
+
// Batch all leaf dots into a single fill
|
|
5902
|
+
if (leafPositions.length > 0) {
|
|
5903
|
+
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5904
|
+
ctx.beginPath();
|
|
5905
|
+
for (const leaf of leafPositions){
|
|
5906
|
+
ctx.moveTo(leaf.x + leaf.r, leaf.y);
|
|
5907
|
+
ctx.arc(leaf.x, leaf.y, leaf.r, 0, Math.PI * 2);
|
|
5528
5908
|
}
|
|
5909
|
+
ctx.fill();
|
|
5529
5910
|
}
|
|
5530
5911
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5531
5912
|
// Star-studded arcs along edges
|
|
@@ -5540,8 +5921,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5540
5921
|
ctx.beginPath();
|
|
5541
5922
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5542
5923
|
ctx.stroke();
|
|
5543
|
-
// Scatter small stars along the border region
|
|
5924
|
+
// Scatter small stars along the border region — batched into single path
|
|
5544
5925
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5926
|
+
ctx.beginPath();
|
|
5545
5927
|
for(let s = 0; s < starCount; s++){
|
|
5546
5928
|
const edge = Math.floor(borderRng() * 4);
|
|
5547
5929
|
let sx, sy;
|
|
@@ -5560,7 +5942,6 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5560
5942
|
}
|
|
5561
5943
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5562
5944
|
// 4-point star
|
|
5563
|
-
ctx.beginPath();
|
|
5564
5945
|
for(let p = 0; p < 8; p++){
|
|
5565
5946
|
const a = p / 8 * Math.PI * 2;
|
|
5566
5947
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -5570,8 +5951,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5570
5951
|
else ctx.lineTo(px2, py2);
|
|
5571
5952
|
}
|
|
5572
5953
|
ctx.closePath();
|
|
5573
|
-
ctx.fill();
|
|
5574
5954
|
}
|
|
5955
|
+
ctx.fill();
|
|
5575
5956
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5576
5957
|
// Thin single rule — understated elegance
|
|
5577
5958
|
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
@@ -5582,6 +5963,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5582
5963
|
// Other archetypes: no border (intentional — not every image needs one)
|
|
5583
5964
|
ctx.restore();
|
|
5584
5965
|
}
|
|
5966
|
+
_mark("10e_borders");
|
|
5585
5967
|
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
5586
5968
|
{
|
|
5587
5969
|
const sigRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
|
|
@@ -5649,6 +6031,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5649
6031
|
ctx.restore();
|
|
5650
6032
|
}
|
|
5651
6033
|
ctx.globalAlpha = 1;
|
|
6034
|
+
_mark("11_signature");
|
|
5652
6035
|
}
|
|
5653
6036
|
|
|
5654
6037
|
|