git-hash-art 0.12.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/CHANGELOG.md CHANGED
@@ -4,11 +4,19 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [0.13.0](https://github.com/gfargo/git-hash-art/compare/0.12.0...0.13.0)
8
+
9
+ - perf: 33-111× pipeline speedup via phase-level profiling [`#23`](https://github.com/gfargo/git-hash-art/pull/23)
10
+ - perf: 33-111× speedup via phase-level profiling and targeted optimizations [`90ed5f5`](https://github.com/gfargo/git-hash-art/commit/90ed5f567bb57a507f11b836156bf8828a946013)
11
+
7
12
  #### [0.12.0](https://github.com/gfargo/git-hash-art/compare/0.11.0...0.12.0)
8
13
 
14
+ > 19 March 2026
15
+
9
16
  - perf: cross-env rendering optimizations round 2 [`#22`](https://github.com/gfargo/git-hash-art/pull/22)
10
17
  - perf: optimize rendering pipeline — batch flow lines, cap clip-heavy styles, cache color parsing [`#21`](https://github.com/gfargo/git-hash-art/pull/21)
11
18
  - docs: update ALGORITHM.md to reflect rendering pipeline changes [`2631e0c`](https://github.com/gfargo/git-hash-art/commit/2631e0c10b2b6bfb0a98c6a31d71d7ddaa8e7511)
19
+ - chore: release v0.12.0 [`501a71c`](https://github.com/gfargo/git-hash-art/commit/501a71c9ca8251d67141fa69b9ecaa62ae5f96c1)
12
20
 
13
21
  #### [0.11.0](https://github.com/gfargo/git-hash-art/compare/0.10.1...0.11.0)
14
22
 
package/dist/browser.js CHANGED
@@ -4582,6 +4582,15 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4582
4582
  ...(0, $81c1b644006d48ec$export$c2f8e0cc249a8d8f),
4583
4583
  ...config
4584
4584
  };
4585
+ const _dt = finalConfig._debugTiming;
4586
+ const _t = _dt ? ()=>performance.now() : undefined;
4587
+ let _p = _t ? _t() : 0;
4588
+ function _mark(name) {
4589
+ if (!_dt || !_t) return;
4590
+ const now = _t();
4591
+ _dt.phases[name] = now - _p;
4592
+ _p = now;
4593
+ }
4585
4594
  const rng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash));
4586
4595
  // ── 0. Select archetype — fundamentally different visual personality ──
4587
4596
  const archetype = (0, $68a238ccd77f2bcd$export$f1142fd7da4d6590)(rng);
@@ -4615,12 +4624,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4615
4624
  const adjustedMaxSize = maxShapeSize * scaleFactor;
4616
4625
  const cx = width / 2;
4617
4626
  const cy = height / 2;
4627
+ _mark("0_setup");
4618
4628
  // ── 1. Background ──────────────────────────────────────────────
4619
4629
  const bgRadius = Math.hypot(cx, cy);
4620
4630
  $1f63dc64b5593c73$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
4621
4631
  // Gradient mesh overlay — 3-4 color control points for richer backgrounds
4632
+ // Use source-over instead of soft-light for cheaper compositing
4622
4633
  const meshPoints = 3 + Math.floor(rng() * 2);
4623
- ctx.globalCompositeOperation = "soft-light";
4634
+ ctx.globalAlpha = 1;
4624
4635
  for(let i = 0; i < meshPoints; i++){
4625
4636
  const mx = rng() * width;
4626
4637
  const my = rng() * height;
@@ -4629,95 +4640,103 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4629
4640
  const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
4630
4641
  grad.addColorStop(0, (0, $b5a262d09b87e373$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
4631
4642
  grad.addColorStop(1, "rgba(0,0,0,0)");
4632
- ctx.globalAlpha = 1;
4633
4643
  ctx.fillStyle = grad;
4634
- ctx.fillRect(0, 0, width, height);
4644
+ // Clip to gradient bounding box — avoids blending transparent pixels
4645
+ const gx = Math.max(0, mx - mRadius);
4646
+ const gy = Math.max(0, my - mRadius);
4647
+ const gw = Math.min(width, mx + mRadius) - gx;
4648
+ const gh = Math.min(height, my + mRadius) - gy;
4649
+ ctx.fillRect(gx, gy, gw, gh);
4635
4650
  }
4636
- ctx.globalCompositeOperation = "source-over";
4637
4651
  // Compute average background luminance for contrast enforcement
4638
4652
  const bgLum = ((0, $b5a262d09b87e373$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $b5a262d09b87e373$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
4639
4653
  // ── 1b. Layered background — archetype-coherent shapes ─────────
4654
+ // Use source-over with pre-multiplied alpha instead of soft-light
4655
+ // for much cheaper compositing (soft-light requires per-pixel blend)
4640
4656
  const bgShapeCount = 3 + Math.floor(rng() * 4);
4641
- ctx.globalCompositeOperation = "soft-light";
4642
4657
  for(let i = 0; i < bgShapeCount; i++){
4643
4658
  const bx = rng() * width;
4644
4659
  const by = rng() * height;
4645
4660
  const bSize = width * 0.3 + rng() * width * 0.5;
4646
4661
  const bColor = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4647
- ctx.globalAlpha = 0.03 + rng() * 0.05;
4662
+ ctx.globalAlpha = (0.03 + rng() * 0.05) * 0.5; // halved to compensate for source-over vs soft-light
4648
4663
  ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(bColor, 0.15);
4649
4664
  ctx.beginPath();
4650
4665
  // Use archetype-appropriate background shapes
4651
- if (archetype.name === "geometric-precision" || archetype.name === "op-art") // Rectangular shapes for geometric archetypes
4652
- ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
4666
+ if (archetype.name === "geometric-precision" || archetype.name === "op-art") ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
4653
4667
  else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
4654
4668
  ctx.fill();
4655
4669
  }
4656
- // Subtle concentric rings from center
4670
+ // Subtle concentric rings from center — batched into single stroke
4657
4671
  const ringCount = 2 + Math.floor(rng() * 3);
4658
4672
  ctx.globalAlpha = 0.02 + rng() * 0.03;
4659
4673
  ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4660
4674
  ctx.lineWidth = 1 * scaleFactor;
4675
+ ctx.beginPath();
4661
4676
  for(let i = 1; i <= ringCount; i++){
4662
4677
  const r = Math.min(width, height) * 0.15 * i;
4663
- ctx.beginPath();
4678
+ ctx.moveTo(cx + r, cy);
4664
4679
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
4665
- ctx.stroke();
4666
4680
  }
4667
- ctx.globalCompositeOperation = "source-over";
4681
+ ctx.stroke();
4668
4682
  // ── 1c. Background pattern layer — subtle textured paper ───────
4669
4683
  const bgPatternRoll = rng();
4670
4684
  if (bgPatternRoll < 0.6) {
4671
4685
  ctx.save();
4672
- ctx.globalCompositeOperation = "soft-light";
4673
4686
  const patternOpacity = 0.02 + rng() * 0.04;
4674
4687
  const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4675
4688
  if (bgPatternRoll < 0.2) {
4676
- // Dot grid — batched into a single path
4677
- const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4678
- const dotR = dotSpacing * 0.08;
4689
+ // Dot grid — use fillRect instead of arcs (much cheaper, no path building)
4690
+ const dotSpacing = Math.max(12, Math.min(width, height) * (0.015 + rng() * 0.015));
4691
+ const dotDiam = Math.max(1, Math.round(dotSpacing * 0.16));
4679
4692
  ctx.globalAlpha = patternOpacity;
4680
4693
  ctx.fillStyle = patternColor;
4681
- ctx.beginPath();
4682
- for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4683
- ctx.moveTo(px + dotR, py);
4684
- ctx.arc(px, py, dotR, 0, Math.PI * 2);
4694
+ let dotCount = 0;
4695
+ for(let px = 0; px < width && dotCount < 2000; px += dotSpacing)for(let py = 0; py < height && dotCount < 2000; py += dotSpacing){
4696
+ ctx.fillRect(px, py, dotDiam, dotDiam);
4697
+ dotCount++;
4685
4698
  }
4686
- ctx.fill();
4687
4699
  } else if (bgPatternRoll < 0.4) {
4688
- // Diagonal lines — batched into a single path
4689
- const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4700
+ // Diagonal lines — batched into a single path, capped at 300 lines
4701
+ const lineSpacing = Math.max(10, Math.min(width, height) * (0.02 + rng() * 0.02));
4690
4702
  ctx.globalAlpha = patternOpacity;
4691
4703
  ctx.strokeStyle = patternColor;
4692
4704
  ctx.lineWidth = 0.5 * scaleFactor;
4693
4705
  const diag = Math.hypot(width, height);
4694
4706
  ctx.beginPath();
4695
- for(let d = -diag; d < diag; d += lineSpacing){
4707
+ let lineCount = 0;
4708
+ for(let d = -diag; d < diag && lineCount < 300; d += lineSpacing){
4696
4709
  ctx.moveTo(d, 0);
4697
4710
  ctx.lineTo(d + height, height);
4711
+ lineCount++;
4698
4712
  }
4699
4713
  ctx.stroke();
4700
4714
  } else {
4701
- // Tessellation — hexagonal grid, batched into a single path
4702
- const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4715
+ // Tessellation — hexagonal grid, capped at 500 hexagons
4716
+ const tessSize = Math.max(15, Math.min(width, height) * (0.025 + rng() * 0.02));
4703
4717
  const tessH = tessSize * Math.sqrt(3);
4704
4718
  ctx.globalAlpha = patternOpacity * 0.7;
4705
4719
  ctx.strokeStyle = patternColor;
4706
4720
  ctx.lineWidth = 0.4 * scaleFactor;
4721
+ // Pre-compute hex vertex offsets (avoid trig per vertex)
4722
+ const hexVx = [];
4723
+ const hexVy = [];
4724
+ for(let s = 0; s < 6; s++){
4725
+ const angle = Math.PI / 3 * s - Math.PI / 6;
4726
+ hexVx.push(Math.cos(angle) * tessSize * 0.5);
4727
+ hexVy.push(Math.sin(angle) * tessSize * 0.5);
4728
+ }
4707
4729
  ctx.beginPath();
4708
- for(let row = 0; row * tessH < height + tessH; row++){
4730
+ let hexCount = 0;
4731
+ for(let row = 0; row * tessH < height + tessH && hexCount < 500; row++){
4709
4732
  const offsetX = row % 2 * tessSize * 0.75;
4710
- for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4733
+ for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5 && hexCount < 500; col++){
4711
4734
  const hx = col * tessSize * 1.5 + offsetX;
4712
4735
  const hy = row * tessH;
4713
- for(let s = 0; s < 6; s++){
4714
- const angle = Math.PI / 3 * s - Math.PI / 6;
4715
- const vx = hx + Math.cos(angle) * tessSize * 0.5;
4716
- const vy = hy + Math.sin(angle) * tessSize * 0.5;
4717
- if (s === 0) ctx.moveTo(vx, vy);
4718
- else ctx.lineTo(vx, vy);
4719
- }
4736
+ ctx.moveTo(hx + hexVx[0], hy + hexVy[0]);
4737
+ for(let s = 1; s < 6; s++)ctx.lineTo(hx + hexVx[s], hy + hexVy[s]);
4720
4738
  ctx.closePath();
4739
+ hexCount++;
4721
4740
  }
4722
4741
  }
4723
4742
  ctx.stroke();
@@ -4725,6 +4744,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4725
4744
  ctx.restore();
4726
4745
  }
4727
4746
  ctx.globalCompositeOperation = "source-over";
4747
+ _mark("1_background");
4728
4748
  // ── 2. Composition mode — archetype-aware selection ──────────────
4729
4749
  const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES.length)];
4730
4750
  const symRoll = rng();
@@ -4835,6 +4855,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4835
4855
  }
4836
4856
  }
4837
4857
  ctx.globalAlpha = 1;
4858
+ _mark("2_3_composition_focal");
4838
4859
  // ── 4. Flow field — simplex noise for organic variation ─────────
4839
4860
  // Create a seeded simplex noise field (unique per hash)
4840
4861
  const noiseFieldRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 333));
@@ -4911,6 +4932,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4911
4932
  shape: heroShape
4912
4933
  });
4913
4934
  }
4935
+ _mark("4_flowfield_hero");
4914
4936
  // ── 5. Shape layers ────────────────────────────────────────────
4915
4937
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4916
4938
  // ── Complexity budget — caps total rendering work ──────────────
@@ -5339,6 +5361,11 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5339
5361
  }
5340
5362
  // Reset blend mode for post-processing passes
5341
5363
  ctx.globalCompositeOperation = "source-over";
5364
+ if (_dt) {
5365
+ _dt.shapeCount = shapePositions.length;
5366
+ _dt.extraCount = extrasSpent;
5367
+ }
5368
+ _mark("5_shape_layers");
5342
5369
  // ── 5g. Layered masking / cutout portals ───────────────────────
5343
5370
  // ~18% of images get 1-3 portal windows that paint over foreground
5344
5371
  // with a tinted background wash, creating a "peek through" effect.
@@ -5397,6 +5424,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5397
5424
  ctx.restore();
5398
5425
  }
5399
5426
  }
5427
+ _mark("5g_portals");
5400
5428
  // ── 6. Flow-line pass — variable color, branching, pressure ────
5401
5429
  // Optimized: collect all segments into width-quantized buckets, then
5402
5430
  // render each bucket as a single batched path. This reduces
@@ -5522,6 +5550,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5522
5550
  ctx.stroke();
5523
5551
  }
5524
5552
  }
5553
+ _mark("6_flow_lines");
5525
5554
  // ── 6b. Motion/energy lines — short directional bursts ─────────
5526
5555
  // Optimized: collect all burst segments, then batch by quantized alpha
5527
5556
  const energyArchetypes = [
@@ -5584,6 +5613,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5584
5613
  ctx.stroke();
5585
5614
  }
5586
5615
  }
5616
+ _mark("6b_energy_lines");
5587
5617
  // ── 6c. Apply symmetry mirroring ─────────────────────────────────
5588
5618
  if (symmetryMode !== "none") {
5589
5619
  const canvas = ctx.canvas;
@@ -5604,60 +5634,25 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5604
5634
  }
5605
5635
  ctx.restore();
5606
5636
  }
5607
- // ── 7. Noise texture overlay — batched via ImageData ─────────────
5608
- // Optimized: cap density at large sizes (diminishing returns above ~2K dots),
5609
- // skip inner pixelScale loop when scale=1, use Uint32Array for faster writes.
5637
+ _mark("6c_symmetry");
5638
+ // ── 7. Noise texture overlay ─────────────────────────────────────
5639
+ // With density capped at 2500 dots, direct fillRect calls are far cheaper
5640
+ // than the getImageData/putImageData round-trip which copies the entire
5641
+ // pixel buffer (4 × width × height bytes) twice.
5610
5642
  const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
5611
5643
  const rawNoiseDensity = Math.floor(width * height / 800);
5612
- // Cap at 2500 dots — beyond this the visual effect is indistinguishable
5613
- // but getImageData/putImageData cost scales with canvas size
5614
5644
  const noiseDensity = Math.min(rawNoiseDensity, 2500);
5615
- try {
5616
- const imageData = ctx.getImageData(0, 0, width, height);
5617
- const data = imageData.data;
5618
- const pixelScale = Math.max(1, Math.round(scaleFactor));
5619
- if (pixelScale === 1) // Fast path no inner loop, direct pixel write
5620
- // Pre-compute alpha blend as integer math (avoid float multiply per channel)
5621
- for(let i = 0; i < noiseDensity; i++){
5622
- const nx = Math.floor(noiseRng() * width);
5623
- const ny = Math.floor(noiseRng() * height);
5624
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5625
- // srcA in range [0.01, 0.04] — multiply by 256 for fixed-point
5626
- const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
5627
- const invA256 = 256 - srcA256;
5628
- const bSrc = brightness * srcA256; // pre-multiply brightness × alpha
5629
- const idx = ny * width + nx << 2;
5630
- data[idx] = data[idx] * invA256 + bSrc >> 8;
5631
- data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
5632
- data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
5633
- }
5634
- else for(let i = 0; i < noiseDensity; i++){
5635
- const nx = Math.floor(noiseRng() * width);
5636
- const ny = Math.floor(noiseRng() * height);
5637
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5638
- const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
5639
- const invA256 = 256 - srcA256;
5640
- const bSrc = brightness * srcA256;
5641
- for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
5642
- const idx = (ny + dy) * width + (nx + dx) << 2;
5643
- data[idx] = data[idx] * invA256 + bSrc >> 8;
5644
- data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
5645
- data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
5646
- }
5647
- }
5648
- ctx.putImageData(imageData, 0, 0);
5649
- } catch {
5650
- // Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
5651
- for(let i = 0; i < noiseDensity; i++){
5652
- const nx = noiseRng() * width;
5653
- const ny = noiseRng() * height;
5654
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5655
- const alpha = 0.01 + noiseRng() * 0.03;
5656
- ctx.globalAlpha = alpha;
5657
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5658
- ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5659
- }
5645
+ const pixelScale = Math.max(1, Math.round(scaleFactor));
5646
+ for(let i = 0; i < noiseDensity; i++){
5647
+ const nx = noiseRng() * width;
5648
+ const ny = noiseRng() * height;
5649
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5650
+ const alpha = 0.01 + noiseRng() * 0.03;
5651
+ ctx.globalAlpha = alpha;
5652
+ ctx.fillStyle = `rgb(${brightness},${brightness},${brightness})`;
5653
+ ctx.fillRect(nx, ny, pixelScale, pixelScale);
5660
5654
  }
5655
+ _mark("7_noise_texture");
5661
5656
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
5662
5657
  ctx.globalAlpha = 1;
5663
5658
  const vignetteStrength = 0.25 + rng() * 0.2;
@@ -5671,6 +5666,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5671
5666
  vigGrad.addColorStop(1, vignetteColor);
5672
5667
  ctx.fillStyle = vigGrad;
5673
5668
  ctx.fillRect(0, 0, width, height);
5669
+ _mark("8_vignette");
5674
5670
  // ── 9. Organic connecting curves — proximity-aware ───────────────
5675
5671
  // Optimized: batch all curves into alpha-quantized groups to reduce
5676
5672
  // beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
@@ -5729,6 +5725,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5729
5725
  ctx.stroke();
5730
5726
  }
5731
5727
  }
5728
+ _mark("9_connecting_curves");
5732
5729
  // ── 10. Post-processing ────────────────────────────────────────
5733
5730
  // 10a. Color grading — unified tone across the whole image
5734
5731
  // Apply as a semi-transparent overlay in the grade hue
@@ -5788,6 +5785,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5788
5785
  ctx.fillRect(0, 0, width, height);
5789
5786
  ctx.globalCompositeOperation = "source-over";
5790
5787
  }
5788
+ _mark("10_post_processing");
5791
5789
  // ── 10e. Generative borders — archetype-driven decorative frames ──
5792
5790
  {
5793
5791
  ctx.save();
@@ -5954,6 +5952,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5954
5952
  // Other archetypes: no border (intentional — not every image needs one)
5955
5953
  ctx.restore();
5956
5954
  }
5955
+ _mark("10e_borders");
5957
5956
  // ── 11. Signature mark — placed in the least-dense corner ──────
5958
5957
  {
5959
5958
  const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
@@ -6021,6 +6020,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
6021
6020
  ctx.restore();
6022
6021
  }
6023
6022
  ctx.globalAlpha = 1;
6023
+ _mark("11_signature");
6024
6024
  }
6025
6025
 
6026
6026