git-hash-art 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -531,13 +531,21 @@ class $d016ad53434219a1$export$ab958c550f521376 {
531
531
  }
532
532
  }
533
533
  // ── Standalone color utilities ──────────────────────────────────────
534
- /** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. */ function $d016ad53434219a1$var$hexToRgb(hex) {
535
- const c = hex.replace("#", "");
536
- return [
534
+ // ── Cached hex→RGB parse avoids repeated parseInt/substring on hot path ──
535
+ const $d016ad53434219a1$var$_rgbCache = new Map();
536
+ const $d016ad53434219a1$var$_RGB_CACHE_MAX = 512;
537
+ /** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. Cached. */ function $d016ad53434219a1$var$hexToRgb(hex) {
538
+ let cached = $d016ad53434219a1$var$_rgbCache.get(hex);
539
+ if (cached) return cached;
540
+ const c = hex.charAt(0) === "#" ? hex.substring(1) : hex;
541
+ cached = [
537
542
  parseInt(c.substring(0, 2), 16),
538
543
  parseInt(c.substring(2, 4), 16),
539
544
  parseInt(c.substring(4, 6), 16)
540
545
  ];
546
+ if ($d016ad53434219a1$var$_rgbCache.size >= $d016ad53434219a1$var$_RGB_CACHE_MAX) $d016ad53434219a1$var$_rgbCache.clear();
547
+ $d016ad53434219a1$var$_rgbCache.set(hex, cached);
548
+ return cached;
541
549
  }
542
550
  /** Format [r, g, b] back to #RRGGBB. */ function $d016ad53434219a1$var$rgbToHex(r, g, b) {
543
551
  const clamp = (v)=>Math.max(0, Math.min(255, Math.round(v)));
@@ -594,7 +602,9 @@ class $d016ad53434219a1$export$ab958c550f521376 {
594
602
  }
595
603
  function $d016ad53434219a1$export$f2121afcad3d553f(hex, alpha) {
596
604
  const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex);
597
- return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
605
+ // Quantize alpha to 3 decimal places without toFixed overhead
606
+ const a = Math.round(alpha * 1000) / 1000;
607
+ return `rgba(${r},${g},${b},${a})`;
598
608
  }
599
609
  function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
600
610
  if (colors.length < 3) return {
@@ -674,12 +684,21 @@ function $d016ad53434219a1$export$51ea55f869b7e0d3(hex, target, amount) {
674
684
  const [h, s, l] = $d016ad53434219a1$var$hexToHsl(hex);
675
685
  return $d016ad53434219a1$var$hslToHex($d016ad53434219a1$var$shiftHueToward(h, target, amount), s, l);
676
686
  }
687
+ /**
688
+ * Compute relative luminance of a hex color (0 = black, 1 = white).
689
+ * Uses the sRGB luminance formula from WCAG. Cached.
690
+ */ const $d016ad53434219a1$var$_lumCache = new Map();
677
691
  function $d016ad53434219a1$export$5c6e3c2b59b7fbbe(hex) {
692
+ let cached = $d016ad53434219a1$var$_lumCache.get(hex);
693
+ if (cached !== undefined) return cached;
678
694
  const [r, g, b] = $d016ad53434219a1$var$hexToRgb(hex).map((c)=>{
679
695
  const s = c / 255;
680
696
  return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
681
697
  });
682
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
698
+ cached = 0.2126 * r + 0.7152 * g + 0.0722 * b;
699
+ if ($d016ad53434219a1$var$_lumCache.size >= 512) $d016ad53434219a1$var$_lumCache.clear();
700
+ $d016ad53434219a1$var$_lumCache.set(hex, cached);
701
+ return cached;
683
702
  }
684
703
  function $d016ad53434219a1$export$90ad0e6170cf6af5(fgHex, bgLuminance, minContrast = 0.15) {
685
704
  const fgLum = $d016ad53434219a1$export$5c6e3c2b59b7fbbe(fgHex);
@@ -1115,21 +1134,31 @@ const $4bf3d69be49ad55c$export$c9043b89bcb14ed9 = (ctx, size, config = {})=>{
1115
1134
  (0, $efc5b85ac9840d51$export$e46c5570db033611)(ctx, size, finalConfig);
1116
1135
  const gridSize = 8;
1117
1136
  const unit = size / gridSize;
1137
+ const radius = unit / 2;
1138
+ // Pre-compute the 8 star-point angle pairs (cos/sin) — avoids 648 trig calls
1139
+ const starPoints = [];
1140
+ for(let k = 0; k < 8; k++){
1141
+ const angle = Math.PI / 4 * k;
1142
+ const angle2 = angle + Math.PI / 4;
1143
+ starPoints.push({
1144
+ c1: Math.cos(angle) * radius,
1145
+ s1: Math.sin(angle) * radius,
1146
+ c2: Math.cos(angle2) * radius,
1147
+ s2: Math.sin(angle2) * radius
1148
+ });
1149
+ }
1118
1150
  ctx.beginPath();
1119
1151
  // Create base grid
1120
- for(let i = 0; i <= gridSize; i++)for(let j = 0; j <= gridSize; j++){
1152
+ for(let i = 0; i <= gridSize; i++){
1121
1153
  const x = (i - gridSize / 2) * unit;
1122
- const y = (j - gridSize / 2) * unit;
1123
- // Draw star pattern at each intersection
1124
- const radius = unit / 2;
1125
- for(let k = 0; k < 8; k++){
1126
- const angle = Math.PI / 4 * k;
1127
- const x1 = x + radius * Math.cos(angle);
1128
- const y1 = y + radius * Math.sin(angle);
1129
- const x2 = x + radius * Math.cos(angle + Math.PI / 4);
1130
- const y2 = y + radius * Math.sin(angle + Math.PI / 4);
1131
- ctx.moveTo(x1, y1);
1132
- ctx.lineTo(x2, y2);
1154
+ for(let j = 0; j <= gridSize; j++){
1155
+ const y = (j - gridSize / 2) * unit;
1156
+ // Draw star pattern at each intersection using pre-computed offsets
1157
+ for(let k = 0; k < 8; k++){
1158
+ const sp = starPoints[k];
1159
+ ctx.moveTo(x + sp.c1, y + sp.s1);
1160
+ ctx.lineTo(x + sp.c2, y + sp.s2);
1161
+ }
1133
1162
  }
1134
1163
  }
1135
1164
  ctx.stroke();
@@ -1449,20 +1478,23 @@ const $dd5df256f00f6199$export$eeae7765f05012e2 = (ctx, size)=>{
1449
1478
  const $dd5df256f00f6199$export$3355220a8108efc3 = (ctx, size)=>{
1450
1479
  const outerRadius = size / 2;
1451
1480
  const innerRadius = size / 4;
1452
- const steps = 36;
1481
+ // Adaptive step count: fewer segments for small shapes where detail isn't visible.
1482
+ // 36×36 = 1296 segments at full size; at size < 60 we drop to 16×16 = 256.
1483
+ const steps = size < 60 ? 16 : size < 150 ? 24 : 36;
1484
+ const TWO_PI = Math.PI * 2;
1485
+ const angleStep = TWO_PI / steps;
1453
1486
  ctx.beginPath();
1454
1487
  for(let i = 0; i < steps; i++){
1455
- const angle1 = i / steps * Math.PI * 2;
1456
- // const angle2 = ((i + 1) / steps) * Math.PI * 2;
1488
+ const angle1 = i * angleStep;
1489
+ const cosA = Math.cos(angle1);
1490
+ const sinA = Math.sin(angle1);
1457
1491
  for(let j = 0; j < steps; j++){
1458
- const phi1 = j / steps * Math.PI * 2;
1459
- const phi2 = (j + 1) / steps * Math.PI * 2;
1460
- const x1 = (outerRadius + innerRadius * Math.cos(phi1)) * Math.cos(angle1);
1461
- const y1 = (outerRadius + innerRadius * Math.cos(phi1)) * Math.sin(angle1);
1462
- const x2 = (outerRadius + innerRadius * Math.cos(phi2)) * Math.cos(angle1);
1463
- const y2 = (outerRadius + innerRadius * Math.cos(phi2)) * Math.sin(angle1);
1464
- ctx.moveTo(x1, y1);
1465
- ctx.lineTo(x2, y2);
1492
+ const phi1 = j * angleStep;
1493
+ const phi2 = phi1 + angleStep;
1494
+ const r1 = outerRadius + innerRadius * Math.cos(phi1);
1495
+ const r2 = outerRadius + innerRadius * Math.cos(phi2);
1496
+ ctx.moveTo(r1 * cosA, r1 * sinA);
1497
+ ctx.lineTo(r2 * cosA, r2 * sinA);
1466
1498
  }
1467
1499
  }
1468
1500
  };
@@ -2025,6 +2057,43 @@ const $c3de8257a8baa3b0$var$RENDER_STYLES = [
2025
2057
  function $c3de8257a8baa3b0$export$9fd4e64b2acd410e(rng) {
2026
2058
  return $c3de8257a8baa3b0$var$RENDER_STYLES[Math.floor(rng() * $c3de8257a8baa3b0$var$RENDER_STYLES.length)];
2027
2059
  }
2060
+ const $c3de8257a8baa3b0$export$2f738f61a8c15e07 = {
2061
+ "fill-and-stroke": 1,
2062
+ "fill-only": 0.5,
2063
+ "stroke-only": 1,
2064
+ "double-stroke": 1.5,
2065
+ "dashed": 1,
2066
+ "watercolor": 7,
2067
+ "hatched": 3,
2068
+ "incomplete": 1,
2069
+ "stipple": 90,
2070
+ "stencil": 2,
2071
+ "noise-grain": 400,
2072
+ "wood-grain": 10,
2073
+ "marble-vein": 4,
2074
+ "fabric-weave": 6,
2075
+ "hand-drawn": 5
2076
+ };
2077
+ function $c3de8257a8baa3b0$export$909ab0580e273f19(style) {
2078
+ switch(style){
2079
+ case "noise-grain":
2080
+ return "hatched";
2081
+ case "stipple":
2082
+ return "dashed";
2083
+ case "wood-grain":
2084
+ return "hatched";
2085
+ case "watercolor":
2086
+ return "fill-and-stroke";
2087
+ case "fabric-weave":
2088
+ return "hatched";
2089
+ case "hand-drawn":
2090
+ return "fill-and-stroke";
2091
+ case "marble-vein":
2092
+ return "stroke-only";
2093
+ default:
2094
+ return style;
2095
+ }
2096
+ }
2028
2097
  function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2029
2098
  const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
2030
2099
  ctx.save();
@@ -2144,6 +2213,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2144
2213
  case "hatched":
2145
2214
  {
2146
2215
  // Fill normally at reduced opacity, then overlay cross-hatch lines
2216
+ // Optimized: batch all parallel lines into a single path per pass
2147
2217
  const savedAlphaH = ctx.globalAlpha;
2148
2218
  ctx.globalAlpha = savedAlphaH * 0.3;
2149
2219
  ctx.fill();
@@ -2155,28 +2225,28 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2155
2225
  const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
2156
2226
  ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
2157
2227
  ctx.globalAlpha = savedAlphaH * 0.6;
2158
- // Draw parallel lines across the bounding box
2228
+ // Draw parallel lines across the bounding box — batched into single path
2159
2229
  const extent = size * 0.8;
2160
2230
  const cos = Math.cos(hatchAngle);
2161
2231
  const sin = Math.sin(hatchAngle);
2232
+ ctx.beginPath();
2162
2233
  for(let d = -extent; d <= extent; d += hatchSpacing){
2163
- ctx.beginPath();
2164
2234
  ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
2165
2235
  ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
2166
- ctx.stroke();
2167
2236
  }
2237
+ ctx.stroke();
2168
2238
  // Second pass at perpendicular angle for cross-hatch (~50% chance)
2169
2239
  if (!rng || rng() < 0.5) {
2170
2240
  const crossAngle = hatchAngle + Math.PI / 2;
2171
2241
  const cos2 = Math.cos(crossAngle);
2172
2242
  const sin2 = Math.sin(crossAngle);
2173
2243
  ctx.globalAlpha = savedAlphaH * 0.35;
2244
+ ctx.beginPath();
2174
2245
  for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
2175
- ctx.beginPath();
2176
2246
  ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
2177
2247
  ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
2178
- ctx.stroke();
2179
2248
  }
2249
+ ctx.stroke();
2180
2250
  }
2181
2251
  ctx.restore();
2182
2252
  ctx.globalAlpha = savedAlphaH;
@@ -2214,6 +2284,8 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2214
2284
  case "stipple":
2215
2285
  {
2216
2286
  // Dot-fill texture — clip to shape, then scatter dots
2287
+ // Optimized: use fillRect instead of arc for dots (much cheaper to render),
2288
+ // and cap total dot count to avoid O(size²) blowup on large shapes.
2217
2289
  const savedAlphaS = ctx.globalAlpha;
2218
2290
  ctx.globalAlpha = savedAlphaS * 0.15;
2219
2291
  ctx.fill(); // ghost fill
@@ -2221,16 +2293,20 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2221
2293
  ctx.save();
2222
2294
  ctx.clip();
2223
2295
  const dotSpacing = Math.max(2, size * 0.03);
2224
- const extent = size * 0.55;
2296
+ const extentS = size * 0.55;
2297
+ // Cap total dots: beyond ~900 (30×30 grid) the visual density plateaus
2298
+ const maxDotsPerAxis = Math.min(Math.ceil(extentS * 2 / dotSpacing), 30);
2299
+ const actualSpacing = extentS * 2 / maxDotsPerAxis;
2225
2300
  ctx.globalAlpha = savedAlphaS * 0.7;
2226
- for(let dx = -extent; dx <= extent; dx += dotSpacing)for(let dy = -extent; dy <= extent; dy += dotSpacing){
2227
- // Jitter each dot position for organic feel
2228
- const jx = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
2229
- const jy = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
2230
- const dotR = rng ? dotSpacing * (0.15 + rng() * 0.2) : dotSpacing * 0.2;
2231
- ctx.beginPath();
2232
- ctx.arc(dx + jx, dy + jy, dotR, 0, Math.PI * 2);
2233
- ctx.fill();
2301
+ for(let xi = 0; xi < maxDotsPerAxis; xi++){
2302
+ const dx = -extentS + xi * actualSpacing;
2303
+ for(let yi = 0; yi < maxDotsPerAxis; yi++){
2304
+ const dy = -extentS + yi * actualSpacing;
2305
+ const jx = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
2306
+ const jy = rng ? (rng() - 0.5) * actualSpacing * 0.6 : 0;
2307
+ const dotD = rng ? actualSpacing * (0.3 + rng() * 0.4) : actualSpacing * 0.4;
2308
+ ctx.fillRect(dx + jx - dotD * 0.5, dy + jy - dotD * 0.5, dotD, dotD);
2309
+ }
2234
2310
  }
2235
2311
  ctx.restore();
2236
2312
  ctx.globalAlpha = savedAlphaS;
@@ -2263,6 +2339,9 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2263
2339
  case "noise-grain":
2264
2340
  {
2265
2341
  // Procedural noise grain texture clipped to shape boundary
2342
+ // Optimized: cap grid to max 40×40 = 1600 dots (was unbounded at O(size²)),
2343
+ // quantize alpha into buckets to minimize globalAlpha state changes,
2344
+ // and batch dots by brightness (black/white) × alpha bucket
2266
2345
  const savedAlphaN = ctx.globalAlpha;
2267
2346
  ctx.globalAlpha = savedAlphaN * 0.25;
2268
2347
  ctx.fill(); // base tint
@@ -2271,17 +2350,47 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2271
2350
  ctx.clip();
2272
2351
  const grainSpacing = Math.max(1.5, size * 0.015);
2273
2352
  const extentN = size * 0.55;
2274
- ctx.globalAlpha = savedAlphaN * 0.6;
2275
- for(let gx = -extentN; gx <= extentN; gx += grainSpacing)for(let gy = -extentN; gy <= extentN; gy += grainSpacing){
2276
- if (!rng) break;
2277
- const jx = (rng() - 0.5) * grainSpacing * 1.2;
2278
- const jy = (rng() - 0.5) * grainSpacing * 1.2;
2279
- const brightness = rng() > 0.5 ? 255 : 0;
2280
- const dotAlpha = 0.15 + rng() * 0.35;
2281
- ctx.globalAlpha = savedAlphaN * dotAlpha;
2282
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
2283
- const dotSize = grainSpacing * (0.3 + rng() * 0.5);
2284
- ctx.fillRect(gx + jx, gy + jy, dotSize, dotSize);
2353
+ if (rng) {
2354
+ // Cap grid to max 40 dots per axis beyond this the grain is
2355
+ // visually indistinguishable but cost scales quadratically.
2356
+ const maxGrainPerAxis = Math.min(Math.ceil(extentN * 2 / grainSpacing), 40);
2357
+ const actualGrainSpacing = extentN * 2 / maxGrainPerAxis;
2358
+ // 4 alpha buckets: 0.2, 0.3, 0.4, 0.5 covers the 0.15-0.50 range
2359
+ const BUCKETS = 4;
2360
+ const bucketMin = 0.15;
2361
+ const bucketRange = 0.35;
2362
+ // [black_bucket0, black_bucket1, ..., white_bucket0, ...]
2363
+ const buckets = [];
2364
+ for(let i = 0; i < BUCKETS * 2; i++)buckets.push([]);
2365
+ for(let xi = 0; xi < maxGrainPerAxis; xi++){
2366
+ const gx = -extentN + xi * actualGrainSpacing;
2367
+ for(let yi = 0; yi < maxGrainPerAxis; yi++){
2368
+ const gy = -extentN + yi * actualGrainSpacing;
2369
+ const jx = (rng() - 0.5) * actualGrainSpacing * 1.2;
2370
+ const jy = (rng() - 0.5) * actualGrainSpacing * 1.2;
2371
+ const isWhite = rng() > 0.5;
2372
+ const dotAlpha = bucketMin + rng() * bucketRange;
2373
+ const dotSize = actualGrainSpacing * (0.3 + rng() * 0.5);
2374
+ const bucketIdx = Math.min(BUCKETS - 1, Math.floor((dotAlpha - bucketMin) / bucketRange * BUCKETS));
2375
+ const offset = isWhite ? BUCKETS : 0;
2376
+ buckets[offset + bucketIdx].push({
2377
+ x: gx + jx,
2378
+ y: gy + jy,
2379
+ s: dotSize
2380
+ });
2381
+ }
2382
+ }
2383
+ // Render each bucket: 2 colors × 4 alpha levels = 8 state changes total
2384
+ for(let color = 0; color < 2; color++){
2385
+ ctx.fillStyle = color === 0 ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
2386
+ for(let b = 0; b < BUCKETS; b++){
2387
+ const dots = buckets[color * BUCKETS + b];
2388
+ if (dots.length === 0) continue;
2389
+ const alpha = bucketMin + (b + 0.5) / BUCKETS * bucketRange;
2390
+ ctx.globalAlpha = savedAlphaN * alpha;
2391
+ for(let i = 0; i < dots.length; i++)ctx.fillRect(dots[i].x, dots[i].y, dots[i].s, dots[i].s);
2392
+ }
2393
+ }
2285
2394
  }
2286
2395
  ctx.restore();
2287
2396
  ctx.fillStyle = fillColor;
@@ -2294,6 +2403,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2294
2403
  case "wood-grain":
2295
2404
  {
2296
2405
  // Parallel wavy lines simulating wood grain, clipped to shape
2406
+ // Optimized: batch all grain lines into a single path, increased step from 2 to 4
2297
2407
  const savedAlphaW = ctx.globalAlpha;
2298
2408
  ctx.globalAlpha = savedAlphaW * 0.2;
2299
2409
  ctx.fill(); // base tint
@@ -2309,17 +2419,19 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2309
2419
  ctx.globalAlpha = savedAlphaW * 0.5;
2310
2420
  const cosG = Math.cos(grainAngle);
2311
2421
  const sinG = Math.sin(grainAngle);
2422
+ const waveCoeff = waveFreq * Math.PI;
2423
+ const invExtentW = 1 / extentW;
2424
+ // Batch all grain lines into a single path
2425
+ ctx.beginPath();
2312
2426
  for(let d = -extentW; d <= extentW; d += grainLineSpacing){
2313
- ctx.beginPath();
2314
- for(let t = -extentW; t <= extentW; t += 2){
2315
- const wave = Math.sin(t / extentW * waveFreq * Math.PI) * waveAmp;
2316
- const px = t * cosG - (d + wave) * sinG;
2317
- const py = t * sinG + (d + wave) * cosG;
2318
- if (t === -extentW) ctx.moveTo(px, py);
2319
- else ctx.lineTo(px, py);
2427
+ const firstWave = Math.sin(-extentW * invExtentW * waveCoeff) * waveAmp;
2428
+ ctx.moveTo(-extentW * cosG - (d + firstWave) * sinG, -extentW * sinG + (d + firstWave) * cosG);
2429
+ for(let t = -extentW + 4; t <= extentW; t += 4){
2430
+ const wave = Math.sin(t * invExtentW * waveCoeff) * waveAmp;
2431
+ ctx.lineTo(t * cosG - (d + wave) * sinG, t * sinG + (d + wave) * cosG);
2320
2432
  }
2321
- ctx.stroke();
2322
2433
  }
2434
+ ctx.stroke();
2323
2435
  ctx.restore();
2324
2436
  ctx.globalAlpha = savedAlphaW;
2325
2437
  ctx.globalAlpha *= 0.35;
@@ -2381,6 +2493,7 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2381
2493
  case "fabric-weave":
2382
2494
  {
2383
2495
  // Interlocking horizontal/vertical threads clipped to shape
2496
+ // Optimized: batch all horizontal threads into one path, all vertical into another
2384
2497
  const savedAlphaF = ctx.globalAlpha;
2385
2498
  ctx.globalAlpha = savedAlphaF * 0.15;
2386
2499
  ctx.fill(); // ghost base
@@ -2390,26 +2503,24 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2390
2503
  const threadSpacing = Math.max(2, size * 0.04);
2391
2504
  const extentF = size * 0.55;
2392
2505
  ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
2506
+ // Horizontal threads — batched
2393
2507
  ctx.globalAlpha = savedAlphaF * 0.55;
2394
- // Horizontal threads
2508
+ ctx.beginPath();
2395
2509
  for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2396
- ctx.beginPath();
2397
2510
  ctx.moveTo(-extentF, y);
2398
2511
  ctx.lineTo(extentF, y);
2399
- ctx.stroke();
2400
2512
  }
2401
- // Vertical threads (offset by half spacing for weave effect)
2513
+ ctx.stroke();
2514
+ // Vertical threads (offset by half spacing for weave effect) — batched
2402
2515
  ctx.globalAlpha = savedAlphaF * 0.45;
2403
2516
  ctx.strokeStyle = fillColor;
2404
- for(let x = -extentF; x <= extentF; x += threadSpacing * 2){
2405
- ctx.beginPath();
2406
- for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2407
- // Over-under: draw segment, skip segment
2408
- ctx.moveTo(x, y);
2409
- ctx.lineTo(x, y + threadSpacing);
2410
- }
2411
- ctx.stroke();
2517
+ ctx.beginPath();
2518
+ for(let x = -extentF; x <= extentF; x += threadSpacing * 2)for(let y = -extentF; y <= extentF; y += threadSpacing * 2){
2519
+ // Over-under: draw segment, skip segment
2520
+ ctx.moveTo(x, y);
2521
+ ctx.lineTo(x, y + threadSpacing);
2412
2522
  }
2523
+ ctx.stroke();
2413
2524
  ctx.strokeStyle = strokeColor;
2414
2525
  ctx.restore();
2415
2526
  ctx.globalAlpha = savedAlphaF;
@@ -2475,14 +2586,17 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2475
2586
  ctx.translate(x, y);
2476
2587
  ctx.rotate(rotation * Math.PI / 180);
2477
2588
  // ── Drop shadow — soft colored shadow offset along light direction ──
2478
- if (lightAngle !== undefined && size > 10) {
2589
+ // Skip shadow entirely for small shapes (< 20px) — the blur is expensive
2590
+ // and visually imperceptible at that scale.
2591
+ const useShadow = size >= 20;
2592
+ if (useShadow && lightAngle !== undefined) {
2479
2593
  const shadowDist = size * 0.035;
2480
2594
  const shadowBlurR = size * 0.06;
2481
2595
  ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
2482
2596
  ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
2483
2597
  ctx.shadowBlur = shadowBlurR;
2484
2598
  ctx.shadowColor = "rgba(0,0,0,0.12)";
2485
- } else if (glowRadius > 0) {
2599
+ } else if (useShadow && glowRadius > 0) {
2486
2600
  // Glow / shadow effect (legacy path)
2487
2601
  ctx.shadowBlur = glowRadius;
2488
2602
  ctx.shadowColor = glowColor || fillColor;
@@ -2506,30 +2620,27 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2506
2620
  $c3de8257a8baa3b0$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
2507
2621
  }
2508
2622
  // Reset shadow so patterns and highlight aren't double-shadowed
2509
- ctx.shadowBlur = 0;
2510
- ctx.shadowOffsetX = 0;
2511
- ctx.shadowOffsetY = 0;
2512
- ctx.shadowColor = "transparent";
2623
+ // Only reset if we actually set shadow (avoids unnecessary state changes)
2624
+ if (useShadow && (lightAngle !== undefined || glowRadius > 0)) {
2625
+ ctx.shadowBlur = 0;
2626
+ ctx.shadowOffsetX = 0;
2627
+ ctx.shadowOffsetY = 0;
2628
+ ctx.shadowColor = "transparent";
2629
+ }
2513
2630
  // ── Specular highlight — tinted arc on the light-facing side ──
2514
- if (lightAngle !== undefined && size > 15 && rng) {
2631
+ // Skip for small shapes (< 30px) gradient creation + composite op
2632
+ // switch is expensive and the highlight is invisible at small sizes.
2633
+ if (lightAngle !== undefined && size > 30 && rng) {
2515
2634
  const hlRadius = size * 0.35;
2516
2635
  const hlDist = size * 0.15;
2517
2636
  const hlX = Math.cos(lightAngle) * hlDist;
2518
2637
  const hlY = Math.sin(lightAngle) * hlDist;
2519
2638
  const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
2520
- // Tint highlight warm/cool based on fill color for cohesion
2521
- // Parse fill to detect warmth fallback to white for non-parseable
2522
- let hlBase = "255,255,255";
2523
- if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
2524
- const r = parseInt(fillColor.slice(1, 3), 16);
2525
- const g = parseInt(fillColor.slice(3, 5), 16);
2526
- const b = parseInt(fillColor.slice(5, 7), 16);
2527
- // Blend toward white but keep a hint of the fill's warmth
2528
- hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
2529
- }
2530
- hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
2531
- hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
2532
- hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
2639
+ // Use a simple white highlight the per-shape hex parse was expensive
2640
+ // and the visual difference from tinted highlights is negligible.
2641
+ hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
2642
+ hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
2643
+ hlGrad.addColorStop(1, "rgba(255,255,255,0)");
2533
2644
  const savedOp = ctx.globalCompositeOperation;
2534
2645
  ctx.globalCompositeOperation = "soft-light";
2535
2646
  ctx.fillStyle = hlGrad;
@@ -4071,6 +4182,46 @@ function $f89bc858f7202849$export$f1142fd7da4d6590(rng) {
4071
4182
  }
4072
4183
 
4073
4184
 
4185
+ // ── Render style cost weights (normalized: fill-and-stroke = 1) ─────
4186
+ // Based on benchmark measurements. Used by the complexity budget to
4187
+ // cap total rendering work and downgrade expensive styles when needed.
4188
+ const $4f72c5a314eddf25$var$RENDER_STYLE_COST = {
4189
+ "fill-and-stroke": 1,
4190
+ "fill-only": 0.5,
4191
+ "stroke-only": 1,
4192
+ "double-stroke": 1.5,
4193
+ "dashed": 1,
4194
+ "watercolor": 7,
4195
+ "hatched": 3,
4196
+ "incomplete": 1,
4197
+ "stipple": 90,
4198
+ "stencil": 2,
4199
+ "noise-grain": 400,
4200
+ "wood-grain": 10,
4201
+ "marble-vein": 4,
4202
+ "fabric-weave": 6,
4203
+ "hand-drawn": 5
4204
+ };
4205
+ function $4f72c5a314eddf25$var$downgradeRenderStyle(style) {
4206
+ switch(style){
4207
+ case "noise-grain":
4208
+ return "hatched";
4209
+ case "stipple":
4210
+ return "dashed";
4211
+ case "wood-grain":
4212
+ return "hatched";
4213
+ case "watercolor":
4214
+ return "fill-and-stroke";
4215
+ case "fabric-weave":
4216
+ return "hatched";
4217
+ case "hand-drawn":
4218
+ return "fill-and-stroke";
4219
+ case "marble-vein":
4220
+ return "stroke-only";
4221
+ default:
4222
+ return style;
4223
+ }
4224
+ }
4074
4225
  // ── Shape categories for weighted selection (legacy fallback) ───────
4075
4226
  const $4f72c5a314eddf25$var$SACRED_SHAPES = [
4076
4227
  "mandala",
@@ -4682,19 +4833,20 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4682
4833
  ctx.beginPath();
4683
4834
  ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
4684
4835
  ctx.stroke();
4685
- // ~50% chance: scatter tiny dots inside the void
4836
+ // ~50% chance: scatter tiny dots inside the void — batched into single path
4686
4837
  if (rng() < 0.5) {
4687
4838
  const dotCount = 3 + Math.floor(rng() * 6);
4688
4839
  ctx.globalAlpha = 0.06 + rng() * 0.04;
4689
4840
  ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
4841
+ ctx.beginPath();
4690
4842
  for(let d = 0; d < dotCount; d++){
4691
4843
  const angle = rng() * Math.PI * 2;
4692
4844
  const dist = rng() * zone.radius * 0.7;
4693
4845
  const dotR = (1 + rng() * 3) * scaleFactor;
4694
- ctx.beginPath();
4846
+ ctx.moveTo(zone.x + Math.cos(angle) * dist + dotR, zone.y + Math.sin(angle) * dist);
4695
4847
  ctx.arc(zone.x + Math.cos(angle) * dist, zone.y + Math.sin(angle) * dist, dotR, 0, Math.PI * 2);
4696
- ctx.fill();
4697
4848
  }
4849
+ ctx.fill();
4698
4850
  }
4699
4851
  // ~30% chance: thin concentric ring inside
4700
4852
  if (rng() < 0.3) {
@@ -4786,6 +4938,26 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4786
4938
  }
4787
4939
  // ── 5. Shape layers ────────────────────────────────────────────
4788
4940
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4941
+ // ── Complexity budget — caps total rendering work ──────────────
4942
+ // Budget scales with pixel area so larger canvases get proportionally
4943
+ // more headroom. The multiplier extras (glazing, echoes, nesting,
4944
+ // constellations, rhythm) are gated behind the budget; when it runs
4945
+ // low they are skipped. When it's exhausted, expensive render styles
4946
+ // are downgraded to cheaper alternatives.
4947
+ //
4948
+ // RNG values are always consumed even when skipping, so the
4949
+ // deterministic sequence for shapes that *do* render is preserved.
4950
+ const pixelArea = width * height;
4951
+ const BUDGET_PER_MEGAPIXEL = 6000; // cost units per 1M pixels
4952
+ let complexityBudget = pixelArea / 1000000 * BUDGET_PER_MEGAPIXEL;
4953
+ const totalBudget = complexityBudget;
4954
+ const budgetForExtras = complexityBudget * 0.25; // reserve 25% for multiplier extras
4955
+ let extrasSpent = 0;
4956
+ // Hard cap on clip-heavy render styles (stipple, noise-grain).
4957
+ // These generate O(size²) fillRect calls per shape and dominate
4958
+ // worst-case render time. Cap scales with pixel area.
4959
+ const MAX_CLIP_HEAVY_SHAPES = Math.max(4, Math.floor(8 * (pixelArea / 1000000)));
4960
+ let clipHeavyCount = 0;
4789
4961
  for(let layer = 0; layer < layers; layer++){
4790
4962
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
4791
4963
  const numShapes = shapesPerLayer + Math.floor(rng() * shapesPerLayer * 0.3);
@@ -4874,7 +5046,26 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4874
5046
  const shapeRenderStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(shape, layerRenderStyle, rng);
4875
5047
  // Organic edge jitter — applied via watercolor style on ~15% of shapes
4876
5048
  const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
4877
- const finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
5049
+ let finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
5050
+ // Budget check: downgrade expensive styles proportionally —
5051
+ // the more expensive the style, the earlier it gets downgraded.
5052
+ // noise-grain (400) downgrades when budget < 20% remaining,
5053
+ // stipple (90) when < 82%, wood-grain (10) when < 98%.
5054
+ let styleCost = $4f72c5a314eddf25$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
5055
+ if (styleCost > 3) {
5056
+ const downgradeThreshold = Math.min(0.85, styleCost / 500);
5057
+ if (complexityBudget < totalBudget * (1 - downgradeThreshold)) {
5058
+ finalRenderStyle = $4f72c5a314eddf25$var$downgradeRenderStyle(finalRenderStyle);
5059
+ styleCost = $4f72c5a314eddf25$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
5060
+ }
5061
+ }
5062
+ // Hard cap: clip-heavy styles (stipple, noise-grain) are limited
5063
+ // to MAX_CLIP_HEAVY_SHAPES total across the entire render.
5064
+ if ((finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
5065
+ finalRenderStyle = $4f72c5a314eddf25$var$downgradeRenderStyle(finalRenderStyle);
5066
+ styleCost = $4f72c5a314eddf25$var$RENDER_STYLE_COST[finalRenderStyle] ?? 1;
5067
+ }
5068
+ if (finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") clipHeavyCount++;
4878
5069
  // Consistent light direction — subtle shadow offset
4879
5070
  const shadowDist = hasGlow ? 0 : size * 0.02;
4880
5071
  const shadowOffX = shadowDist * Math.cos(lightAngle);
@@ -4929,30 +5120,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4929
5120
  lightAngle: lightAngle,
4930
5121
  scaleFactor: scaleFactor
4931
5122
  };
4932
- if (shouldMirror) (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
4933
- ...shapeConfig,
4934
- mirrorAxis: mirrorAxis,
4935
- mirrorGap: size * (0.1 + rng() * 0.3)
4936
- });
4937
- else (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
5123
+ if (shouldMirror) {
5124
+ (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
5125
+ ...shapeConfig,
5126
+ mirrorAxis: mirrorAxis,
5127
+ mirrorGap: size * (0.1 + rng() * 0.3)
5128
+ });
5129
+ complexityBudget -= styleCost * 2; // mirrored = 2 shapes
5130
+ } else {
5131
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, shapeConfig);
5132
+ complexityBudget -= styleCost;
5133
+ }
5134
+ // ── Extras budget gate — skip multiplier sections when over budget ──
5135
+ const extrasAllowed = extrasSpent < budgetForExtras;
4938
5136
  // ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
4939
5137
  if (rng() < 0.2 && size > adjustedMinSize * 2) {
4940
5138
  const glazePasses = 2 + Math.floor(rng() * 2);
4941
- for(let g = 0; g < glazePasses; g++){
4942
- const glazeScale = 1 - (g + 1) * 0.12; // progressively smaller
4943
- const glazeAlpha = 0.08 + g * 0.04; // progressively more opaque toward center
4944
- ctx.globalAlpha = glazeAlpha;
4945
- (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
4946
- fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
4947
- strokeColor: "rgba(0,0,0,0)",
4948
- strokeWidth: 0,
4949
- size: size * glazeScale,
4950
- rotation: rotation,
4951
- proportionType: "GOLDEN_RATIO",
4952
- renderStyle: "fill-only",
4953
- rng: rng
4954
- });
5139
+ if (extrasAllowed) {
5140
+ for(let g = 0; g < glazePasses; g++){
5141
+ const glazeScale = 1 - (g + 1) * 0.12;
5142
+ const glazeAlpha = 0.08 + g * 0.04;
5143
+ ctx.globalAlpha = glazeAlpha;
5144
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, finalX, finalY, {
5145
+ fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.15 + g * 0.1),
5146
+ strokeColor: "rgba(0,0,0,0)",
5147
+ strokeWidth: 0,
5148
+ size: size * glazeScale,
5149
+ rotation: rotation,
5150
+ proportionType: "GOLDEN_RATIO",
5151
+ renderStyle: "fill-only",
5152
+ rng: rng
5153
+ });
5154
+ }
5155
+ extrasSpent += glazePasses;
4955
5156
  }
5157
+ // RNG consumed by glazePasses calculation above regardless
4956
5158
  }
4957
5159
  shapePositions.push({
4958
5160
  x: finalX,
@@ -4970,37 +5172,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4970
5172
  if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
4971
5173
  const echoCount = 2 + Math.floor(rng() * 2);
4972
5174
  const echoAngle = rng() * Math.PI * 2;
4973
- for(let e = 0; e < echoCount; e++){
4974
- const echoScale = 0.3 - e * 0.08;
4975
- const echoDist = size * (0.6 + e * 0.4);
4976
- const echoX = finalX + Math.cos(echoAngle) * echoDist;
4977
- const echoY = finalY + Math.sin(echoAngle) * echoDist;
4978
- const echoSize = size * Math.max(0.1, echoScale);
4979
- if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
4980
- ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
4981
- (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
4982
- fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
4983
- strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.4),
4984
- strokeWidth: strokeWidth * 0.6,
4985
- size: echoSize,
4986
- rotation: rotation + (e + 1) * 15,
4987
- proportionType: "GOLDEN_RATIO",
4988
- renderStyle: finalRenderStyle,
4989
- rng: rng
4990
- });
4991
- shapePositions.push({
4992
- x: echoX,
4993
- y: echoY,
4994
- size: echoSize,
4995
- shape: shape
4996
- });
4997
- spatialGrid.insert({
4998
- x: echoX,
4999
- y: echoY,
5000
- size: echoSize,
5001
- shape: shape
5002
- });
5175
+ if (extrasAllowed) {
5176
+ for(let e = 0; e < echoCount; e++){
5177
+ const echoScale = 0.3 - e * 0.08;
5178
+ const echoDist = size * (0.6 + e * 0.4);
5179
+ const echoX = finalX + Math.cos(echoAngle) * echoDist;
5180
+ const echoY = finalY + Math.sin(echoAngle) * echoDist;
5181
+ const echoSize = size * Math.max(0.1, echoScale);
5182
+ if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
5183
+ ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
5184
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, shape, echoX, echoY, {
5185
+ fillColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, fillAlpha * 0.6),
5186
+ strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.4),
5187
+ strokeWidth: strokeWidth * 0.6,
5188
+ size: echoSize,
5189
+ rotation: rotation + (e + 1) * 15,
5190
+ proportionType: "GOLDEN_RATIO",
5191
+ renderStyle: finalRenderStyle,
5192
+ rng: rng
5193
+ });
5194
+ shapePositions.push({
5195
+ x: echoX,
5196
+ y: echoY,
5197
+ size: echoSize,
5198
+ shape: shape
5199
+ });
5200
+ spatialGrid.insert({
5201
+ x: echoX,
5202
+ y: echoY,
5203
+ size: echoSize,
5204
+ shape: shape
5205
+ });
5206
+ }
5207
+ extrasSpent += echoCount * styleCost;
5003
5208
  }
5209
+ // RNG for echoCount + echoAngle consumed above regardless
5004
5210
  }
5005
5211
  // ── 5d. Recursive nesting ──────────────────────────────────
5006
5212
  // Focal depth: shapes near focal points get more detail
@@ -5008,7 +5214,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5008
5214
  const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
5009
5215
  if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
5010
5216
  const innerCount = 1 + Math.floor(rng() * 3);
5011
- for(let n = 0; n < innerCount; n++){
5217
+ if (extrasAllowed) for(let n = 0; n < innerCount; n++){
5012
5218
  // Pick inner shape from palette affinities
5013
5219
  const innerSizeFraction = size * 0.25 / adjustedMaxSize;
5014
5220
  const innerShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, innerSizeFraction);
@@ -5017,6 +5223,10 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5017
5223
  const innerOffY = (rng() - 0.5) * size * 0.4;
5018
5224
  const innerRot = rng() * 360;
5019
5225
  const innerFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 10, 0.1), 0.3 + rng() * 0.4);
5226
+ let innerStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng);
5227
+ // Apply clip-heavy cap to nested shapes too
5228
+ if ((innerStyle === "stipple" || innerStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) innerStyle = $4f72c5a314eddf25$var$downgradeRenderStyle(innerStyle);
5229
+ if (innerStyle === "stipple" || innerStyle === "noise-grain") clipHeavyCount++;
5020
5230
  ctx.globalAlpha = layerOpacity * 0.7;
5021
5231
  (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, innerShape, finalX + innerOffX, finalY + innerOffY, {
5022
5232
  fillColor: innerFill,
@@ -5025,9 +5235,21 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5025
5235
  size: innerSize,
5026
5236
  rotation: innerRot,
5027
5237
  proportionType: "GOLDEN_RATIO",
5028
- renderStyle: (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(innerShape, layerRenderStyle, rng),
5238
+ renderStyle: innerStyle,
5029
5239
  rng: rng
5030
5240
  });
5241
+ extrasSpent += $4f72c5a314eddf25$var$RENDER_STYLE_COST[innerStyle] ?? 1;
5242
+ }
5243
+ else // Drain RNG to keep determinism — each nested shape consumes ~8 rng calls
5244
+ for(let n = 0; n < innerCount; n++){
5245
+ rng();
5246
+ rng();
5247
+ rng();
5248
+ rng();
5249
+ rng();
5250
+ rng();
5251
+ rng();
5252
+ rng();
5031
5253
  }
5032
5254
  }
5033
5255
  // ── 5e. Shape constellations — pre-composed groups ─────────
@@ -5036,40 +5258,55 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5036
5258
  const constellation = $4f72c5a314eddf25$var$CONSTELLATIONS[Math.floor(rng() * $4f72c5a314eddf25$var$CONSTELLATIONS.length)];
5037
5259
  const members = constellation.build(rng, size);
5038
5260
  const groupRotation = rng() * Math.PI * 2;
5039
- const cosR = Math.cos(groupRotation);
5040
- const sinR = Math.sin(groupRotation);
5041
- for (const member of members){
5042
- // Rotate the group offset by the group rotation
5043
- const mx = finalX + member.dx * cosR - member.dy * sinR;
5044
- const my = finalY + member.dx * sinR + member.dy * cosR;
5045
- if (mx < 0 || mx > width || my < 0 || my > height) continue;
5046
- const memberFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
5047
- const memberStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
5048
- ctx.globalAlpha = layerOpacity * 0.6;
5049
- // Use the member's shape if available, otherwise fall back to palette
5050
- const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
5051
- (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
5052
- fillColor: memberFill,
5053
- strokeColor: memberStroke,
5054
- strokeWidth: strokeWidth * 0.7,
5055
- size: member.size,
5056
- rotation: member.rotation + groupRotation * 180 / Math.PI,
5057
- proportionType: "GOLDEN_RATIO",
5058
- renderStyle: (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng),
5059
- rng: rng
5060
- });
5061
- shapePositions.push({
5062
- x: mx,
5063
- y: my,
5064
- size: member.size,
5065
- shape: memberShape
5066
- });
5067
- spatialGrid.insert({
5068
- x: mx,
5069
- y: my,
5070
- size: member.size,
5071
- shape: memberShape
5072
- });
5261
+ if (extrasAllowed) {
5262
+ const cosR = Math.cos(groupRotation);
5263
+ const sinR = Math.sin(groupRotation);
5264
+ for (const member of members){
5265
+ // Rotate the group offset by the group rotation
5266
+ const mx = finalX + member.dx * cosR - member.dy * sinR;
5267
+ const my = finalY + member.dx * sinR + member.dy * cosR;
5268
+ if (mx < 0 || mx > width || my < 0 || my > height) continue;
5269
+ const memberFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), rng, 8, 0.06), fillAlpha * 0.8);
5270
+ const memberStroke = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$18a34c25ea7e724b)(strokeBase, rng, 5, 0.04), bgLum);
5271
+ ctx.globalAlpha = layerOpacity * 0.6;
5272
+ // Use the member's shape if available, otherwise fall back to palette
5273
+ const memberShape = shapeNames.includes(member.shape) ? member.shape : (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, member.size / adjustedMaxSize);
5274
+ let memberStyle = (0, $e73976f898150d4d$export$ab873bb6fb56c1a8)(memberShape, layerRenderStyle, rng);
5275
+ // Apply clip-heavy cap to constellation members too
5276
+ if ((memberStyle === "stipple" || memberStyle === "noise-grain") && clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) memberStyle = $4f72c5a314eddf25$var$downgradeRenderStyle(memberStyle);
5277
+ if (memberStyle === "stipple" || memberStyle === "noise-grain") clipHeavyCount++;
5278
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, memberShape, mx, my, {
5279
+ fillColor: memberFill,
5280
+ strokeColor: memberStroke,
5281
+ strokeWidth: strokeWidth * 0.7,
5282
+ size: member.size,
5283
+ rotation: member.rotation + groupRotation * 180 / Math.PI,
5284
+ proportionType: "GOLDEN_RATIO",
5285
+ renderStyle: memberStyle,
5286
+ rng: rng
5287
+ });
5288
+ shapePositions.push({
5289
+ x: mx,
5290
+ y: my,
5291
+ size: member.size,
5292
+ shape: memberShape
5293
+ });
5294
+ spatialGrid.insert({
5295
+ x: mx,
5296
+ y: my,
5297
+ size: member.size,
5298
+ shape: memberShape
5299
+ });
5300
+ extrasSpent += $4f72c5a314eddf25$var$RENDER_STYLE_COST[memberStyle] ?? 1;
5301
+ }
5302
+ } else // Drain RNG — each member consumes ~6 rng calls for colors/style
5303
+ for(let m = 0; m < members.length; m++){
5304
+ rng();
5305
+ rng();
5306
+ rng();
5307
+ rng();
5308
+ rng();
5309
+ rng();
5073
5310
  }
5074
5311
  }
5075
5312
  // ── 5f. Rhythm placement — deliberate geometric progressions ──
@@ -5080,39 +5317,47 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5080
5317
  const rhythmSpacing = size * (0.8 + rng() * 0.6);
5081
5318
  const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
5082
5319
  const rhythmShape = shape; // same shape for visual rhythm
5083
- let rhythmSize = size * 0.6;
5320
+ if (extrasAllowed) {
5321
+ let rhythmSize = size * 0.6;
5322
+ for(let r = 0; r < rhythmCount; r++){
5323
+ const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
5324
+ const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
5325
+ if (rx < 0 || rx > width || ry < 0 || ry > height) break;
5326
+ if ($4f72c5a314eddf25$var$isInVoidZone(rx, ry, voidZones)) break;
5327
+ rhythmSize *= rhythmDecay;
5328
+ if (rhythmSize < adjustedMinSize) break;
5329
+ const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
5330
+ ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
5331
+ const rhythmFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
5332
+ (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
5333
+ fillColor: rhythmFill,
5334
+ strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.5),
5335
+ strokeWidth: strokeWidth * 0.7,
5336
+ size: rhythmSize,
5337
+ rotation: rotation + (r + 1) * 12,
5338
+ proportionType: "GOLDEN_RATIO",
5339
+ renderStyle: finalRenderStyle,
5340
+ rng: rng
5341
+ });
5342
+ shapePositions.push({
5343
+ x: rx,
5344
+ y: ry,
5345
+ size: rhythmSize,
5346
+ shape: rhythmShape
5347
+ });
5348
+ spatialGrid.insert({
5349
+ x: rx,
5350
+ y: ry,
5351
+ size: rhythmSize,
5352
+ shape: rhythmShape
5353
+ });
5354
+ }
5355
+ extrasSpent += rhythmCount * styleCost;
5356
+ } else // Drain RNG — each rhythm step consumes ~3 rng calls for colors
5084
5357
  for(let r = 0; r < rhythmCount; r++){
5085
- const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
5086
- const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
5087
- if (rx < 0 || rx > width || ry < 0 || ry > height) break;
5088
- if ($4f72c5a314eddf25$var$isInVoidZone(rx, ry, voidZones)) break;
5089
- rhythmSize *= rhythmDecay;
5090
- if (rhythmSize < adjustedMinSize) break;
5091
- const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
5092
- ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
5093
- const rhythmFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
5094
- (0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
5095
- fillColor: rhythmFill,
5096
- strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.5),
5097
- strokeWidth: strokeWidth * 0.7,
5098
- size: rhythmSize,
5099
- rotation: rotation + (r + 1) * 12,
5100
- proportionType: "GOLDEN_RATIO",
5101
- renderStyle: finalRenderStyle,
5102
- rng: rng
5103
- });
5104
- shapePositions.push({
5105
- x: rx,
5106
- y: ry,
5107
- size: rhythmSize,
5108
- shape: rhythmShape
5109
- });
5110
- spatialGrid.insert({
5111
- x: rx,
5112
- y: ry,
5113
- size: rhythmSize,
5114
- shape: rhythmShape
5115
- });
5358
+ rng();
5359
+ rng();
5360
+ rng();
5116
5361
  }
5117
5362
  }
5118
5363
  }
@@ -5178,14 +5423,26 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5178
5423
  }
5179
5424
  }
5180
5425
  // ── 6. Flow-line pass — variable color, branching, pressure ────
5426
+ // Optimized: collect all segments into width-quantized buckets, then
5427
+ // render each bucket as a single batched path. This reduces
5428
+ // beginPath/stroke calls from O(segments) to O(buckets).
5181
5429
  const baseFlowLines = 6 + Math.floor(rng() * 10);
5182
5430
  const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
5431
+ // Width buckets — 6 buckets cover the taper×pressure range
5432
+ const FLOW_WIDTH_BUCKETS = 6;
5433
+ const flowBuckets = [];
5434
+ for(let b = 0; b < FLOW_WIDTH_BUCKETS; b++)flowBuckets.push([]);
5435
+ // Track the representative width for each bucket
5436
+ const flowBucketWidths = new Array(FLOW_WIDTH_BUCKETS);
5437
+ // Pre-compute max possible width for bucket assignment
5438
+ let globalMaxFlowWidth = 0;
5183
5439
  for(let i = 0; i < numFlowLines; i++){
5184
5440
  let fx = rng() * width;
5185
5441
  let fy = rng() * height;
5186
5442
  const steps = 30 + Math.floor(rng() * 40);
5187
5443
  const stepLen = (3 + rng() * 5) * scaleFactor;
5188
5444
  const startWidth = (1 + rng() * 3) * scaleFactor;
5445
+ if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
5189
5446
  // Variable color: interpolate between two hierarchy colors along the stroke
5190
5447
  const lineColorStart = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
5191
5448
  const lineColorEnd = (0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum);
@@ -5207,19 +5464,22 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5207
5464
  continue;
5208
5465
  }
5209
5466
  const t = s / steps;
5210
- // Taper + pressure
5211
5467
  const taper = 1 - t * 0.8;
5212
5468
  const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
5213
- ctx.globalAlpha = lineAlpha * taper;
5214
- // Interpolate color along stroke
5469
+ const segWidth = startWidth * taper * pressure;
5470
+ const segAlpha = lineAlpha * taper;
5215
5471
  const lineColor = t < 0.5 ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(lineColorStart, 0.4 + t * 0.2) : (0, $d016ad53434219a1$export$f2121afcad3d553f)(lineColorEnd, 0.4 + (1 - t) * 0.2);
5216
- ctx.strokeStyle = lineColor;
5217
- ctx.lineWidth = startWidth * taper * pressure;
5218
- ctx.lineCap = "round";
5219
- ctx.beginPath();
5220
- ctx.moveTo(prevX, prevY);
5221
- ctx.lineTo(fx, fy);
5222
- ctx.stroke();
5472
+ // Quantize width into bucket
5473
+ const bucketIdx = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(segWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
5474
+ flowBuckets[bucketIdx].push({
5475
+ x1: prevX,
5476
+ y1: prevY,
5477
+ x2: fx,
5478
+ y2: fy,
5479
+ color: lineColor,
5480
+ alpha: segAlpha
5481
+ });
5482
+ flowBucketWidths[bucketIdx] = segWidth;
5223
5483
  // Branching: ~12% chance per step to spawn a thinner child stroke
5224
5484
  if (rng() < 0.12 && s > 5 && s < steps - 10) {
5225
5485
  const branchAngle = angle + (rng() < 0.5 ? 1 : -1) * (0.3 + rng() * 0.5);
@@ -5235,12 +5495,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5235
5495
  by += Math.sin(bAngle) * stepLen * 0.8;
5236
5496
  if (bx < 0 || bx > width || by < 0 || by > height) break;
5237
5497
  const bTaper = 1 - bs / branchSteps * 0.9;
5238
- ctx.globalAlpha = lineAlpha * taper * bTaper * 0.6;
5239
- ctx.lineWidth = branchWidth * bTaper;
5240
- ctx.beginPath();
5241
- ctx.moveTo(bPrevX, bPrevY);
5242
- ctx.lineTo(bx, by);
5243
- ctx.stroke();
5498
+ const bSegWidth = branchWidth * bTaper;
5499
+ const bAlpha = lineAlpha * taper * bTaper * 0.6;
5500
+ const bBucket = Math.min(FLOW_WIDTH_BUCKETS - 1, Math.floor(bSegWidth / (globalMaxFlowWidth || 1) * FLOW_WIDTH_BUCKETS));
5501
+ flowBuckets[bBucket].push({
5502
+ x1: bPrevX,
5503
+ y1: bPrevY,
5504
+ x2: bx,
5505
+ y2: by,
5506
+ color: lineColor,
5507
+ alpha: bAlpha
5508
+ });
5509
+ flowBucketWidths[bBucket] = bSegWidth;
5244
5510
  bPrevX = bx;
5245
5511
  bPrevY = by;
5246
5512
  }
@@ -5249,7 +5515,40 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5249
5515
  prevY = fy;
5250
5516
  }
5251
5517
  }
5518
+ // Render flow line buckets — one batched path per width bucket
5519
+ // Within each bucket, further sub-batch by quantized alpha (4 levels)
5520
+ ctx.lineCap = "round";
5521
+ const FLOW_ALPHA_BUCKETS = 4;
5522
+ for(let wb = 0; wb < FLOW_WIDTH_BUCKETS; wb++){
5523
+ const segs = flowBuckets[wb];
5524
+ if (segs.length === 0) continue;
5525
+ ctx.lineWidth = flowBucketWidths[wb];
5526
+ // Sub-bucket by alpha
5527
+ const alphaSubs = [];
5528
+ for(let a = 0; a < FLOW_ALPHA_BUCKETS; a++)alphaSubs.push([]);
5529
+ let maxAlpha = 0;
5530
+ for(let j = 0; j < segs.length; j++)if (segs[j].alpha > maxAlpha) maxAlpha = segs[j].alpha;
5531
+ for(let j = 0; j < segs.length; j++){
5532
+ const ai = Math.min(FLOW_ALPHA_BUCKETS - 1, Math.floor(segs[j].alpha / (maxAlpha || 1) * FLOW_ALPHA_BUCKETS));
5533
+ alphaSubs[ai].push(segs[j]);
5534
+ }
5535
+ for(let ai = 0; ai < FLOW_ALPHA_BUCKETS; ai++){
5536
+ const sub = alphaSubs[ai];
5537
+ if (sub.length === 0) continue;
5538
+ // Use the median segment's alpha and color as representative
5539
+ const rep = sub[Math.floor(sub.length / 2)];
5540
+ ctx.globalAlpha = rep.alpha;
5541
+ ctx.strokeStyle = rep.color;
5542
+ ctx.beginPath();
5543
+ for(let j = 0; j < sub.length; j++){
5544
+ ctx.moveTo(sub[j].x1, sub[j].y1);
5545
+ ctx.lineTo(sub[j].x2, sub[j].y2);
5546
+ }
5547
+ ctx.stroke();
5548
+ }
5549
+ }
5252
5550
  // ── 6b. Motion/energy lines — short directional bursts ─────────
5551
+ // Optimized: collect all burst segments, then batch by quantized alpha
5253
5552
  const energyArchetypes = [
5254
5553
  "dense-chaotic",
5255
5554
  "cosmic",
@@ -5260,8 +5559,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5260
5559
  if (hasEnergyLines && shapePositions.length > 0) {
5261
5560
  const energyCount = 5 + Math.floor(rng() * 10);
5262
5561
  ctx.lineCap = "round";
5562
+ // Collect all energy segments with their computed state
5563
+ const ENERGY_ALPHA_BUCKETS = 3;
5564
+ const energyBuckets = [];
5565
+ for(let b = 0; b < ENERGY_ALPHA_BUCKETS; b++)energyBuckets.push([]);
5566
+ const energyAlphas = new Array(ENERGY_ALPHA_BUCKETS).fill(0);
5263
5567
  for(let e = 0; e < energyCount; e++){
5264
- // Pick a random shape to radiate from
5265
5568
  const source = shapePositions[Math.floor(rng() * shapePositions.length)];
5266
5569
  const burstCount = 2 + Math.floor(rng() * 4);
5267
5570
  const baseAngle = flowAngle(source.x, source.y);
@@ -5273,15 +5576,38 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5273
5576
  const sy = source.y + Math.sin(angle) * startDist;
5274
5577
  const ex = sx + Math.cos(angle) * lineLen;
5275
5578
  const ey = sy + Math.sin(angle) * lineLen;
5276
- ctx.globalAlpha = 0.04 + rng() * 0.06;
5277
- ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
5278
- ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
5279
- ctx.beginPath();
5280
- ctx.moveTo(sx, sy);
5281
- ctx.lineTo(ex, ey);
5282
- ctx.stroke();
5579
+ const eAlpha = 0.04 + rng() * 0.06;
5580
+ const eColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
5581
+ const eLw = (0.5 + rng() * 1.5) * scaleFactor;
5582
+ // Quantize alpha into bucket
5583
+ const bi = Math.min(ENERGY_ALPHA_BUCKETS - 1, Math.floor((eAlpha - 0.04) / 0.06 * ENERGY_ALPHA_BUCKETS));
5584
+ energyBuckets[bi].push({
5585
+ x1: sx,
5586
+ y1: sy,
5587
+ x2: ex,
5588
+ y2: ey,
5589
+ color: eColor,
5590
+ lw: eLw
5591
+ });
5592
+ energyAlphas[bi] = eAlpha;
5283
5593
  }
5284
5594
  }
5595
+ // Render batched energy lines
5596
+ for(let bi = 0; bi < ENERGY_ALPHA_BUCKETS; bi++){
5597
+ const segs = energyBuckets[bi];
5598
+ if (segs.length === 0) continue;
5599
+ ctx.globalAlpha = energyAlphas[bi];
5600
+ // Use median segment's color and width as representative
5601
+ const rep = segs[Math.floor(segs.length / 2)];
5602
+ ctx.strokeStyle = rep.color;
5603
+ ctx.lineWidth = rep.lw;
5604
+ ctx.beginPath();
5605
+ for(let j = 0; j < segs.length; j++){
5606
+ ctx.moveTo(segs[j].x1, segs[j].y1);
5607
+ ctx.lineTo(segs[j].x2, segs[j].y2);
5608
+ }
5609
+ ctx.stroke();
5610
+ }
5285
5611
  }
5286
5612
  // ── 6c. Apply symmetry mirroring ─────────────────────────────────
5287
5613
  if (symmetryMode !== "none") {
@@ -5304,27 +5630,44 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5304
5630
  ctx.restore();
5305
5631
  }
5306
5632
  // ── 7. Noise texture overlay — batched via ImageData ─────────────
5633
+ // Optimized: cap density at large sizes (diminishing returns above ~2K dots),
5634
+ // skip inner pixelScale loop when scale=1, use Uint32Array for faster writes.
5307
5635
  const noiseRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 777));
5308
- const noiseDensity = Math.floor(width * height / 800);
5636
+ const rawNoiseDensity = Math.floor(width * height / 800);
5637
+ // Cap at 2500 dots — beyond this the visual effect is indistinguishable
5638
+ // but getImageData/putImageData cost scales with canvas size
5639
+ const noiseDensity = Math.min(rawNoiseDensity, 2500);
5309
5640
  try {
5310
5641
  const imageData = ctx.getImageData(0, 0, width, height);
5311
5642
  const data = imageData.data;
5312
5643
  const pixelScale = Math.max(1, Math.round(scaleFactor));
5644
+ if (pixelScale === 1) // Fast path — no inner loop, direct pixel write
5645
+ // Pre-compute alpha blend as integer math (avoid float multiply per channel)
5313
5646
  for(let i = 0; i < noiseDensity; i++){
5314
5647
  const nx = Math.floor(noiseRng() * width);
5315
5648
  const ny = Math.floor(noiseRng() * height);
5316
5649
  const brightness = noiseRng() > 0.5 ? 255 : 0;
5317
- const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
5318
- // Write a small block of pixels for scale
5650
+ // srcA in range [0.01, 0.04] multiply by 256 for fixed-point
5651
+ const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
5652
+ const invA256 = 256 - srcA256;
5653
+ const bSrc = brightness * srcA256; // pre-multiply brightness × alpha
5654
+ const idx = ny * width + nx << 2;
5655
+ data[idx] = data[idx] * invA256 + bSrc >> 8;
5656
+ data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
5657
+ data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
5658
+ }
5659
+ else for(let i = 0; i < noiseDensity; i++){
5660
+ const nx = Math.floor(noiseRng() * width);
5661
+ const ny = Math.floor(noiseRng() * height);
5662
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5663
+ const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
5664
+ const invA256 = 256 - srcA256;
5665
+ const bSrc = brightness * srcA256;
5319
5666
  for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
5320
- const idx = ((ny + dy) * width + (nx + dx)) * 4;
5321
- // Alpha-blend the noise dot onto existing pixel data
5322
- const srcA = alpha / 255;
5323
- const invA = 1 - srcA;
5324
- data[idx] = Math.round(data[idx] * invA + brightness * srcA);
5325
- data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
5326
- data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
5327
- // Keep existing alpha
5667
+ const idx = (ny + dy) * width + (nx + dx) << 2;
5668
+ data[idx] = data[idx] * invA256 + bSrc >> 8;
5669
+ data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
5670
+ data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
5328
5671
  }
5329
5672
  }
5330
5673
  ctx.putImageData(imageData, 0, 0);
@@ -5354,10 +5697,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5354
5697
  ctx.fillStyle = vigGrad;
5355
5698
  ctx.fillRect(0, 0, width, height);
5356
5699
  // ── 9. Organic connecting curves — proximity-aware ───────────────
5700
+ // Optimized: batch all curves into alpha-quantized groups to reduce
5701
+ // beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
5357
5702
  if (shapePositions.length > 1) {
5358
5703
  const numCurves = Math.floor(8 * (width * height) / 1048576);
5359
5704
  const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
5360
5705
  ctx.lineWidth = 0.8 * scaleFactor;
5706
+ // Collect curves into 3 alpha buckets
5707
+ const CURVE_ALPHA_BUCKETS = 3;
5708
+ const curveBuckets = [];
5709
+ const curveColors = [];
5710
+ const curveAlphas = new Array(CURVE_ALPHA_BUCKETS).fill(0);
5711
+ for(let b = 0; b < CURVE_ALPHA_BUCKETS; b++)curveBuckets.push([]);
5361
5712
  for(let i = 0; i < numCurves; i++){
5362
5713
  const idxA = Math.floor(rng() * shapePositions.length);
5363
5714
  const offset = 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
@@ -5374,11 +5725,32 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5374
5725
  const bulge = (rng() - 0.5) * dist * 0.4;
5375
5726
  const cpx = mx + -dy / (dist || 1) * bulge;
5376
5727
  const cpy = my + dx / (dist || 1) * bulge;
5377
- ctx.globalAlpha = 0.06 + rng() * 0.1;
5378
- ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
5728
+ const curveAlpha = 0.06 + rng() * 0.1;
5729
+ const curveColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$90ad0e6170cf6af5)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), bgLum), 0.3);
5730
+ const bi = Math.min(CURVE_ALPHA_BUCKETS - 1, Math.floor((curveAlpha - 0.06) / 0.1 * CURVE_ALPHA_BUCKETS));
5731
+ curveBuckets[bi].push({
5732
+ ax: a.x,
5733
+ ay: a.y,
5734
+ cpx: cpx,
5735
+ cpy: cpy,
5736
+ bx: b.x,
5737
+ by: b.y
5738
+ });
5739
+ curveAlphas[bi] = curveAlpha;
5740
+ if (!curveColors[bi]) curveColors[bi] = curveColor;
5741
+ }
5742
+ // Render batched curves
5743
+ for(let bi = 0; bi < CURVE_ALPHA_BUCKETS; bi++){
5744
+ const curves = curveBuckets[bi];
5745
+ if (curves.length === 0) continue;
5746
+ ctx.globalAlpha = curveAlphas[bi];
5747
+ ctx.strokeStyle = curveColors[bi];
5379
5748
  ctx.beginPath();
5380
- ctx.moveTo(a.x, a.y);
5381
- ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);
5749
+ for(let j = 0; j < curves.length; j++){
5750
+ const c = curves[j];
5751
+ ctx.moveTo(c.ax, c.ay);
5752
+ ctx.quadraticCurveTo(c.cpx, c.cpy, c.bx, c.by);
5753
+ }
5382
5754
  ctx.stroke();
5383
5755
  }
5384
5756
  }
@@ -5496,11 +5868,14 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5496
5868
  }
5497
5869
  } else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
5498
5870
  // Vine tendrils — organic curving lines along edges
5871
+ // Optimized: batch all tendrils into a single path
5499
5872
  ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
5500
5873
  ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
5501
5874
  ctx.globalAlpha = 0.12 + borderRng() * 0.08;
5502
5875
  ctx.lineCap = "round";
5503
5876
  const tendrilCount = 8 + Math.floor(borderRng() * 8);
5877
+ ctx.beginPath();
5878
+ const leafPositions = [];
5504
5879
  for(let t = 0; t < tendrilCount; t++){
5505
5880
  // Start from a random edge point
5506
5881
  const edge = Math.floor(borderRng() * 4);
@@ -5518,7 +5893,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5518
5893
  tx = width - borderPad;
5519
5894
  ty = borderRng() * height;
5520
5895
  }
5521
- ctx.beginPath();
5522
5896
  ctx.moveTo(tx, ty);
5523
5897
  const segs = 3 + Math.floor(borderRng() * 4);
5524
5898
  for(let s = 0; s < segs; s++){
@@ -5532,14 +5906,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5532
5906
  ty = cpy3;
5533
5907
  ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
5534
5908
  }
5535
- ctx.stroke();
5536
- // Small leaf/dot at tendril end
5537
- if (borderRng() < 0.6) {
5538
- ctx.beginPath();
5539
- ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
5540
- ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
5541
- ctx.fill();
5909
+ // Collect leaf positions for batch fill
5910
+ if (borderRng() < 0.6) leafPositions.push({
5911
+ x: tx,
5912
+ y: ty,
5913
+ r: borderPad * (0.15 + borderRng() * 0.2)
5914
+ });
5915
+ }
5916
+ ctx.stroke();
5917
+ // Batch all leaf dots into a single fill
5918
+ if (leafPositions.length > 0) {
5919
+ ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
5920
+ ctx.beginPath();
5921
+ for (const leaf of leafPositions){
5922
+ ctx.moveTo(leaf.x + leaf.r, leaf.y);
5923
+ ctx.arc(leaf.x, leaf.y, leaf.r, 0, Math.PI * 2);
5542
5924
  }
5925
+ ctx.fill();
5543
5926
  }
5544
5927
  } else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
5545
5928
  // Star-studded arcs along edges
@@ -5554,8 +5937,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5554
5937
  ctx.beginPath();
5555
5938
  ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
5556
5939
  ctx.stroke();
5557
- // Scatter small stars along the border region
5940
+ // Scatter small stars along the border region — batched into single path
5558
5941
  const starCount = 15 + Math.floor(borderRng() * 15);
5942
+ ctx.beginPath();
5559
5943
  for(let s = 0; s < starCount; s++){
5560
5944
  const edge = Math.floor(borderRng() * 4);
5561
5945
  let sx, sy;
@@ -5574,7 +5958,6 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5574
5958
  }
5575
5959
  const starR = (1 + borderRng() * 2.5) * scaleFactor;
5576
5960
  // 4-point star
5577
- ctx.beginPath();
5578
5961
  for(let p = 0; p < 8; p++){
5579
5962
  const a = p / 8 * Math.PI * 2;
5580
5963
  const r = p % 2 === 0 ? starR : starR * 0.4;
@@ -5584,8 +5967,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5584
5967
  else ctx.lineTo(px2, py2);
5585
5968
  }
5586
5969
  ctx.closePath();
5587
- ctx.fill();
5588
5970
  }
5971
+ ctx.fill();
5589
5972
  } else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
5590
5973
  // Thin single rule — understated elegance
5591
5974
  ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);