git-hash-art 0.12.0 → 0.14.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,27 @@ 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.14.0](https://github.com/gfargo/git-hash-art/compare/0.13.0...0.14.0)
8
+
9
+ - feat: Custom shapes API + remove portal/cutout feature [`#24`](https://github.com/gfargo/git-hash-art/pull/24)
10
+ - feat: add custom shapes API, remove portal/cutout feature [`68d1d5e`](https://github.com/gfargo/git-hash-art/commit/68d1d5eb05c09e4459153b277cf07f325fe27b1a)
11
+
12
+ #### [0.13.0](https://github.com/gfargo/git-hash-art/compare/0.12.0...0.13.0)
13
+
14
+ > 20 March 2026
15
+
16
+ - perf: 33-111× pipeline speedup via phase-level profiling [`#23`](https://github.com/gfargo/git-hash-art/pull/23)
17
+ - perf: 33-111× speedup via phase-level profiling and targeted optimizations [`90ed5f5`](https://github.com/gfargo/git-hash-art/commit/90ed5f567bb57a507f11b836156bf8828a946013)
18
+ - chore: release v0.13.0 [`79ae33e`](https://github.com/gfargo/git-hash-art/commit/79ae33e6645ab25c144558dd1fe601bd33b96d32)
19
+
7
20
  #### [0.12.0](https://github.com/gfargo/git-hash-art/compare/0.11.0...0.12.0)
8
21
 
22
+ > 19 March 2026
23
+
9
24
  - perf: cross-env rendering optimizations round 2 [`#22`](https://github.com/gfargo/git-hash-art/pull/22)
10
25
  - 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
26
  - docs: update ALGORITHM.md to reflect rendering pipeline changes [`2631e0c`](https://github.com/gfargo/git-hash-art/commit/2631e0c10b2b6bfb0a98c6a31d71d7ddaa8e7511)
27
+ - chore: release v0.12.0 [`501a71c`](https://github.com/gfargo/git-hash-art/commit/501a71c9ca8251d67141fa69b9ecaa62ae5f96c1)
12
28
 
13
29
  #### [0.11.0](https://github.com/gfargo/git-hash-art/compare/0.10.1...0.11.0)
14
30
 
package/dist/browser.js CHANGED
@@ -2072,14 +2072,15 @@ function $e0f99502ff383dd8$export$909ab0580e273f19(style) {
2072
2072
  }
2073
2073
  }
2074
2074
  function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
2075
- const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
2075
+ const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, activeShapes: activeShapes } = config;
2076
2076
  ctx.save();
2077
2077
  ctx.translate(x, y);
2078
2078
  ctx.rotate(rotation * Math.PI / 180);
2079
2079
  ctx.fillStyle = fillColor;
2080
2080
  ctx.strokeStyle = strokeColor;
2081
2081
  ctx.lineWidth = strokeWidth;
2082
- const drawFunction = (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[shape];
2082
+ const registry = activeShapes ?? (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5);
2083
+ const drawFunction = registry[shape];
2083
2084
  if (drawFunction) {
2084
2085
  drawFunction(ctx, size);
2085
2086
  ctx.fill();
@@ -2589,7 +2590,8 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2589
2590
  } else ctx.fillStyle = fillColor;
2590
2591
  ctx.strokeStyle = strokeColor;
2591
2592
  ctx.lineWidth = strokeWidth;
2592
- const drawFunction = (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[shape];
2593
+ const registry = config.activeShapes ?? (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5);
2594
+ const drawFunction = registry[shape];
2593
2595
  if (drawFunction) {
2594
2596
  drawFunction(ctx, size, {
2595
2597
  rng: rng
@@ -3453,6 +3455,26 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
3453
3455
  ]
3454
3456
  }
3455
3457
  };
3458
+ function $8286059160ee2e04$export$90912290d628650f(name, partial) {
3459
+ $8286059160ee2e04$export$4343b39fe47bd82c[name] = {
3460
+ tier: partial?.tier ?? 2,
3461
+ minSizeFraction: partial?.minSizeFraction ?? 0.05,
3462
+ maxSizeFraction: partial?.maxSizeFraction ?? 1.0,
3463
+ affinities: partial?.affinities ?? [
3464
+ "circle",
3465
+ "square"
3466
+ ],
3467
+ category: "procedural",
3468
+ heroCandidate: partial?.heroCandidate ?? false,
3469
+ bestStyles: partial?.bestStyles ?? [
3470
+ "fill-and-stroke",
3471
+ "watercolor"
3472
+ ]
3473
+ };
3474
+ }
3475
+ function $8286059160ee2e04$export$f4ca68bd046f15ae(name) {
3476
+ delete $8286059160ee2e04$export$4343b39fe47bd82c[name];
3477
+ }
3456
3478
  function $8286059160ee2e04$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
3457
3479
  const available = shapeNames.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]);
3458
3480
  // Pick a seed shape — tier 1 shapes that are hero candidates
@@ -3642,7 +3664,14 @@ function $8286059160ee2e04$export$ab873bb6fb56c1a8(shapeName, layerStyle, rng) {
3642
3664
 
3643
3665
 
3644
3666
  /**
3645
- * Configuration options for image generation.
3667
+ * Draw function signature for custom shapes.
3668
+ * The function should build a canvas path (moveTo/lineTo/arc/etc.)
3669
+ * centered at the origin. The pipeline handles translate, rotate,
3670
+ * fill, and stroke — your function just defines the geometry.
3671
+ *
3672
+ * @param ctx - Canvas 2D rendering context (already translated to shape center)
3673
+ * @param size - Bounding size in pixels
3674
+ * @param rng - Deterministic RNG seeded from the git hash — use this instead of Math.random()
3646
3675
  */ const $81c1b644006d48ec$export$c2f8e0cc249a8d8f = {
3647
3676
  width: 2048,
3648
3677
  height: 2048,
@@ -4582,6 +4611,15 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4582
4611
  ...(0, $81c1b644006d48ec$export$c2f8e0cc249a8d8f),
4583
4612
  ...config
4584
4613
  };
4614
+ const _dt = finalConfig._debugTiming;
4615
+ const _t = _dt ? ()=>performance.now() : undefined;
4616
+ let _p = _t ? _t() : 0;
4617
+ function _mark(name) {
4618
+ if (!_dt || !_t) return;
4619
+ const now = _t();
4620
+ _dt.phases[name] = now - _p;
4621
+ _p = now;
4622
+ }
4585
4623
  const rng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash));
4586
4624
  // ── 0. Select archetype — fundamentally different visual personality ──
4587
4625
  const archetype = (0, $68a238ccd77f2bcd$export$f1142fd7da4d6590)(rng);
@@ -4602,7 +4640,39 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4602
4640
  // ── 0b. Color hierarchy — dominant/secondary/accent weighting ──
4603
4641
  const colorHierarchy = (0, $b5a262d09b87e373$export$fabac4600b87056)(colors, rng);
4604
4642
  // ── 0c. Shape palette — curated shapes that work well together ──
4605
- const shapeNames = Object.keys((0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5));
4643
+ // Merge custom shapes into a combined registry
4644
+ const customShapeNames = [];
4645
+ let activeShapes;
4646
+ if (finalConfig.customShapes && Object.keys(finalConfig.customShapes).length > 0) {
4647
+ activeShapes = {
4648
+ ...(0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)
4649
+ };
4650
+ for (const [name, def] of Object.entries(finalConfig.customShapes)){
4651
+ // Wrap CustomDrawFunction (ctx, size, rng) into DrawFunction (ctx, size, config?)
4652
+ const customDraw = def.draw;
4653
+ activeShapes[name] = (ctx, size, config)=>{
4654
+ customDraw(ctx, size, config?.rng ?? Math.random);
4655
+ };
4656
+ // Register profile for affinity system (inlined to avoid ESM interop issues)
4657
+ (0, $8286059160ee2e04$export$4343b39fe47bd82c)[name] = {
4658
+ tier: def.profile?.tier ?? 2,
4659
+ minSizeFraction: def.profile?.minSizeFraction ?? 0.05,
4660
+ maxSizeFraction: def.profile?.maxSizeFraction ?? 1.0,
4661
+ affinities: def.profile?.affinities ?? [
4662
+ "circle",
4663
+ "square"
4664
+ ],
4665
+ category: "procedural",
4666
+ heroCandidate: def.profile?.heroCandidate ?? false,
4667
+ bestStyles: def.profile?.bestStyles ?? [
4668
+ "fill-and-stroke",
4669
+ "watercolor"
4670
+ ]
4671
+ };
4672
+ customShapeNames.push(name);
4673
+ }
4674
+ }
4675
+ const shapeNames = Object.keys(activeShapes ?? (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5));
4606
4676
  const shapePalette = (0, $8286059160ee2e04$export$4a95df8944b5033b)(rng, shapeNames, archetype.name);
4607
4677
  // ── 0d. Color grading — unified tone for the whole image ───────
4608
4678
  const colorGrade = (0, $b5a262d09b87e373$export$6d1620b367f86f7a)(rng);
@@ -4615,12 +4685,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4615
4685
  const adjustedMaxSize = maxShapeSize * scaleFactor;
4616
4686
  const cx = width / 2;
4617
4687
  const cy = height / 2;
4688
+ _mark("0_setup");
4618
4689
  // ── 1. Background ──────────────────────────────────────────────
4619
4690
  const bgRadius = Math.hypot(cx, cy);
4620
4691
  $1f63dc64b5593c73$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
4621
4692
  // Gradient mesh overlay — 3-4 color control points for richer backgrounds
4693
+ // Use source-over instead of soft-light for cheaper compositing
4622
4694
  const meshPoints = 3 + Math.floor(rng() * 2);
4623
- ctx.globalCompositeOperation = "soft-light";
4695
+ ctx.globalAlpha = 1;
4624
4696
  for(let i = 0; i < meshPoints; i++){
4625
4697
  const mx = rng() * width;
4626
4698
  const my = rng() * height;
@@ -4629,95 +4701,103 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4629
4701
  const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
4630
4702
  grad.addColorStop(0, (0, $b5a262d09b87e373$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
4631
4703
  grad.addColorStop(1, "rgba(0,0,0,0)");
4632
- ctx.globalAlpha = 1;
4633
4704
  ctx.fillStyle = grad;
4634
- ctx.fillRect(0, 0, width, height);
4705
+ // Clip to gradient bounding box — avoids blending transparent pixels
4706
+ const gx = Math.max(0, mx - mRadius);
4707
+ const gy = Math.max(0, my - mRadius);
4708
+ const gw = Math.min(width, mx + mRadius) - gx;
4709
+ const gh = Math.min(height, my + mRadius) - gy;
4710
+ ctx.fillRect(gx, gy, gw, gh);
4635
4711
  }
4636
- ctx.globalCompositeOperation = "source-over";
4637
4712
  // Compute average background luminance for contrast enforcement
4638
4713
  const bgLum = ((0, $b5a262d09b87e373$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $b5a262d09b87e373$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
4639
4714
  // ── 1b. Layered background — archetype-coherent shapes ─────────
4715
+ // Use source-over with pre-multiplied alpha instead of soft-light
4716
+ // for much cheaper compositing (soft-light requires per-pixel blend)
4640
4717
  const bgShapeCount = 3 + Math.floor(rng() * 4);
4641
- ctx.globalCompositeOperation = "soft-light";
4642
4718
  for(let i = 0; i < bgShapeCount; i++){
4643
4719
  const bx = rng() * width;
4644
4720
  const by = rng() * height;
4645
4721
  const bSize = width * 0.3 + rng() * width * 0.5;
4646
4722
  const bColor = (0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4647
- ctx.globalAlpha = 0.03 + rng() * 0.05;
4723
+ ctx.globalAlpha = (0.03 + rng() * 0.05) * 0.5; // halved to compensate for source-over vs soft-light
4648
4724
  ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(bColor, 0.15);
4649
4725
  ctx.beginPath();
4650
4726
  // 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));
4727
+ 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
4728
  else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
4654
4729
  ctx.fill();
4655
4730
  }
4656
- // Subtle concentric rings from center
4731
+ // Subtle concentric rings from center — batched into single stroke
4657
4732
  const ringCount = 2 + Math.floor(rng() * 3);
4658
4733
  ctx.globalAlpha = 0.02 + rng() * 0.03;
4659
4734
  ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4660
4735
  ctx.lineWidth = 1 * scaleFactor;
4736
+ ctx.beginPath();
4661
4737
  for(let i = 1; i <= ringCount; i++){
4662
4738
  const r = Math.min(width, height) * 0.15 * i;
4663
- ctx.beginPath();
4739
+ ctx.moveTo(cx + r, cy);
4664
4740
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
4665
- ctx.stroke();
4666
4741
  }
4667
- ctx.globalCompositeOperation = "source-over";
4742
+ ctx.stroke();
4668
4743
  // ── 1c. Background pattern layer — subtle textured paper ───────
4669
4744
  const bgPatternRoll = rng();
4670
4745
  if (bgPatternRoll < 0.6) {
4671
4746
  ctx.save();
4672
- ctx.globalCompositeOperation = "soft-light";
4673
4747
  const patternOpacity = 0.02 + rng() * 0.04;
4674
4748
  const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4675
4749
  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;
4750
+ // Dot grid — use fillRect instead of arcs (much cheaper, no path building)
4751
+ const dotSpacing = Math.max(12, Math.min(width, height) * (0.015 + rng() * 0.015));
4752
+ const dotDiam = Math.max(1, Math.round(dotSpacing * 0.16));
4679
4753
  ctx.globalAlpha = patternOpacity;
4680
4754
  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);
4755
+ let dotCount = 0;
4756
+ for(let px = 0; px < width && dotCount < 2000; px += dotSpacing)for(let py = 0; py < height && dotCount < 2000; py += dotSpacing){
4757
+ ctx.fillRect(px, py, dotDiam, dotDiam);
4758
+ dotCount++;
4685
4759
  }
4686
- ctx.fill();
4687
4760
  } 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));
4761
+ // Diagonal lines — batched into a single path, capped at 300 lines
4762
+ const lineSpacing = Math.max(10, Math.min(width, height) * (0.02 + rng() * 0.02));
4690
4763
  ctx.globalAlpha = patternOpacity;
4691
4764
  ctx.strokeStyle = patternColor;
4692
4765
  ctx.lineWidth = 0.5 * scaleFactor;
4693
4766
  const diag = Math.hypot(width, height);
4694
4767
  ctx.beginPath();
4695
- for(let d = -diag; d < diag; d += lineSpacing){
4768
+ let lineCount = 0;
4769
+ for(let d = -diag; d < diag && lineCount < 300; d += lineSpacing){
4696
4770
  ctx.moveTo(d, 0);
4697
4771
  ctx.lineTo(d + height, height);
4772
+ lineCount++;
4698
4773
  }
4699
4774
  ctx.stroke();
4700
4775
  } 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));
4776
+ // Tessellation — hexagonal grid, capped at 500 hexagons
4777
+ const tessSize = Math.max(15, Math.min(width, height) * (0.025 + rng() * 0.02));
4703
4778
  const tessH = tessSize * Math.sqrt(3);
4704
4779
  ctx.globalAlpha = patternOpacity * 0.7;
4705
4780
  ctx.strokeStyle = patternColor;
4706
4781
  ctx.lineWidth = 0.4 * scaleFactor;
4782
+ // Pre-compute hex vertex offsets (avoid trig per vertex)
4783
+ const hexVx = [];
4784
+ const hexVy = [];
4785
+ for(let s = 0; s < 6; s++){
4786
+ const angle = Math.PI / 3 * s - Math.PI / 6;
4787
+ hexVx.push(Math.cos(angle) * tessSize * 0.5);
4788
+ hexVy.push(Math.sin(angle) * tessSize * 0.5);
4789
+ }
4707
4790
  ctx.beginPath();
4708
- for(let row = 0; row * tessH < height + tessH; row++){
4791
+ let hexCount = 0;
4792
+ for(let row = 0; row * tessH < height + tessH && hexCount < 500; row++){
4709
4793
  const offsetX = row % 2 * tessSize * 0.75;
4710
- for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4794
+ for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5 && hexCount < 500; col++){
4711
4795
  const hx = col * tessSize * 1.5 + offsetX;
4712
4796
  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
- }
4797
+ ctx.moveTo(hx + hexVx[0], hy + hexVy[0]);
4798
+ for(let s = 1; s < 6; s++)ctx.lineTo(hx + hexVx[s], hy + hexVy[s]);
4720
4799
  ctx.closePath();
4800
+ hexCount++;
4721
4801
  }
4722
4802
  }
4723
4803
  ctx.stroke();
@@ -4725,6 +4805,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4725
4805
  ctx.restore();
4726
4806
  }
4727
4807
  ctx.globalCompositeOperation = "source-over";
4808
+ _mark("1_background");
4728
4809
  // ── 2. Composition mode — archetype-aware selection ──────────────
4729
4810
  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
4811
  const symRoll = rng();
@@ -4835,6 +4916,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4835
4916
  }
4836
4917
  }
4837
4918
  ctx.globalAlpha = 1;
4919
+ _mark("2_3_composition_focal");
4838
4920
  // ── 4. Flow field — simplex noise for organic variation ─────────
4839
4921
  // Create a seeded simplex noise field (unique per hash)
4840
4922
  const noiseFieldRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 333));
@@ -4891,7 +4973,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4891
4973
  renderStyle: heroStyle,
4892
4974
  rng: rng,
4893
4975
  lightAngle: lightAngle,
4894
- scaleFactor: scaleFactor
4976
+ scaleFactor: scaleFactor,
4977
+ activeShapes: activeShapes
4895
4978
  });
4896
4979
  heroCenter = {
4897
4980
  x: heroFocal.x,
@@ -4911,6 +4994,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
4911
4994
  shape: heroShape
4912
4995
  });
4913
4996
  }
4997
+ _mark("4_flowfield_hero");
4914
4998
  // ── 5. Shape layers ────────────────────────────────────────────
4915
4999
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4916
5000
  // ── Complexity budget — caps total rendering work ──────────────
@@ -5093,7 +5177,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5093
5177
  renderStyle: finalRenderStyle,
5094
5178
  rng: rng,
5095
5179
  lightAngle: lightAngle,
5096
- scaleFactor: scaleFactor
5180
+ scaleFactor: scaleFactor,
5181
+ activeShapes: activeShapes
5097
5182
  };
5098
5183
  if (shouldMirror) {
5099
5184
  (0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
@@ -5124,7 +5209,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5124
5209
  rotation: rotation,
5125
5210
  proportionType: "GOLDEN_RATIO",
5126
5211
  renderStyle: "fill-only",
5127
- rng: rng
5212
+ rng: rng,
5213
+ activeShapes: activeShapes
5128
5214
  });
5129
5215
  }
5130
5216
  extrasSpent += glazePasses;
@@ -5164,7 +5250,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5164
5250
  rotation: rotation + (e + 1) * 15,
5165
5251
  proportionType: "GOLDEN_RATIO",
5166
5252
  renderStyle: finalRenderStyle,
5167
- rng: rng
5253
+ rng: rng,
5254
+ activeShapes: activeShapes
5168
5255
  });
5169
5256
  shapePositions.push({
5170
5257
  x: echoX,
@@ -5211,7 +5298,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5211
5298
  rotation: innerRot,
5212
5299
  proportionType: "GOLDEN_RATIO",
5213
5300
  renderStyle: innerStyle,
5214
- rng: rng
5301
+ rng: rng,
5302
+ activeShapes: activeShapes
5215
5303
  });
5216
5304
  extrasSpent += $1f63dc64b5593c73$var$RENDER_STYLE_COST[innerStyle] ?? 1;
5217
5305
  }
@@ -5258,7 +5346,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5258
5346
  rotation: member.rotation + groupRotation * 180 / Math.PI,
5259
5347
  proportionType: "GOLDEN_RATIO",
5260
5348
  renderStyle: memberStyle,
5261
- rng: rng
5349
+ rng: rng,
5350
+ activeShapes: activeShapes
5262
5351
  });
5263
5352
  shapePositions.push({
5264
5353
  x: mx,
@@ -5312,7 +5401,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5312
5401
  rotation: rotation + (r + 1) * 12,
5313
5402
  proportionType: "GOLDEN_RATIO",
5314
5403
  renderStyle: finalRenderStyle,
5315
- rng: rng
5404
+ rng: rng,
5405
+ activeShapes: activeShapes
5316
5406
  });
5317
5407
  shapePositions.push({
5318
5408
  x: rx,
@@ -5339,64 +5429,13 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5339
5429
  }
5340
5430
  // Reset blend mode for post-processing passes
5341
5431
  ctx.globalCompositeOperation = "source-over";
5342
- // ── 5g. Layered masking / cutout portals ───────────────────────
5343
- // ~18% of images get 1-3 portal windows that paint over foreground
5344
- // with a tinted background wash, creating a "peek through" effect.
5345
- if (rng() < 0.18 && shapePositions.length > 3) {
5346
- const portalCount = 1 + Math.floor(rng() * 2);
5347
- for(let p = 0; p < portalCount; p++){
5348
- // Pick a position biased toward placed shapes
5349
- const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
5350
- const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
5351
- const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
5352
- const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
5353
- // Pick a portal shape from the palette
5354
- const portalShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
5355
- const portalRotation = rng() * 360;
5356
- const portalAlpha = 0.6 + rng() * 0.35;
5357
- ctx.save();
5358
- ctx.translate(portalX, portalY);
5359
- ctx.rotate(portalRotation * Math.PI / 180);
5360
- // Step 1: Clip to the portal shape and fill with background wash
5361
- ctx.beginPath();
5362
- (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
5363
- ctx.clip();
5364
- // Fill the clipped region with a radial gradient from background colors
5365
- const portalColor = (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
5366
- const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
5367
- portalGrad.addColorStop(0, portalColor);
5368
- portalGrad.addColorStop(1, bgEnd);
5369
- ctx.globalAlpha = portalAlpha;
5370
- ctx.fillStyle = portalGrad;
5371
- ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
5372
- // Optional: subtle inner texture — a few tiny dots inside the portal
5373
- if (rng() < 0.5) {
5374
- const dotCount = 3 + Math.floor(rng() * 5);
5375
- ctx.globalAlpha = portalAlpha * 0.3;
5376
- ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
5377
- for(let d = 0; d < dotCount; d++){
5378
- const dx = (rng() - 0.5) * portalSize * 1.4;
5379
- const dy = (rng() - 0.5) * portalSize * 1.4;
5380
- const dr = (1 + rng() * 3) * scaleFactor;
5381
- ctx.beginPath();
5382
- ctx.arc(dx, dy, dr, 0, Math.PI * 2);
5383
- ctx.fill();
5384
- }
5385
- }
5386
- ctx.restore();
5387
- // Step 2: Draw a border ring around the portal (outside the clip)
5388
- ctx.save();
5389
- ctx.translate(portalX, portalY);
5390
- ctx.rotate(portalRotation * Math.PI / 180);
5391
- ctx.globalAlpha = 0.15 + rng() * 0.2;
5392
- ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
5393
- ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
5394
- ctx.beginPath();
5395
- (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
5396
- ctx.stroke();
5397
- ctx.restore();
5398
- }
5432
+ if (_dt) {
5433
+ _dt.shapeCount = shapePositions.length;
5434
+ _dt.extraCount = extrasSpent;
5399
5435
  }
5436
+ _mark("5_shape_layers");
5437
+ // ── 5g. (Portal/cutout feature removed — replaced by custom shapes API) ──
5438
+ _mark("5g_portals");
5400
5439
  // ── 6. Flow-line pass — variable color, branching, pressure ────
5401
5440
  // Optimized: collect all segments into width-quantized buckets, then
5402
5441
  // render each bucket as a single batched path. This reduces
@@ -5522,6 +5561,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5522
5561
  ctx.stroke();
5523
5562
  }
5524
5563
  }
5564
+ _mark("6_flow_lines");
5525
5565
  // ── 6b. Motion/energy lines — short directional bursts ─────────
5526
5566
  // Optimized: collect all burst segments, then batch by quantized alpha
5527
5567
  const energyArchetypes = [
@@ -5584,6 +5624,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5584
5624
  ctx.stroke();
5585
5625
  }
5586
5626
  }
5627
+ _mark("6b_energy_lines");
5587
5628
  // ── 6c. Apply symmetry mirroring ─────────────────────────────────
5588
5629
  if (symmetryMode !== "none") {
5589
5630
  const canvas = ctx.canvas;
@@ -5604,60 +5645,25 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5604
5645
  }
5605
5646
  ctx.restore();
5606
5647
  }
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.
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.
5610
5653
  const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
5611
5654
  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
5655
  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
- }
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);
5660
5665
  }
5666
+ _mark("7_noise_texture");
5661
5667
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
5662
5668
  ctx.globalAlpha = 1;
5663
5669
  const vignetteStrength = 0.25 + rng() * 0.2;
@@ -5671,6 +5677,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5671
5677
  vigGrad.addColorStop(1, vignetteColor);
5672
5678
  ctx.fillStyle = vigGrad;
5673
5679
  ctx.fillRect(0, 0, width, height);
5680
+ _mark("8_vignette");
5674
5681
  // ── 9. Organic connecting curves — proximity-aware ───────────────
5675
5682
  // Optimized: batch all curves into alpha-quantized groups to reduce
5676
5683
  // beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
@@ -5729,6 +5736,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5729
5736
  ctx.stroke();
5730
5737
  }
5731
5738
  }
5739
+ _mark("9_connecting_curves");
5732
5740
  // ── 10. Post-processing ────────────────────────────────────────
5733
5741
  // 10a. Color grading — unified tone across the whole image
5734
5742
  // Apply as a semi-transparent overlay in the grade hue
@@ -5788,6 +5796,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5788
5796
  ctx.fillRect(0, 0, width, height);
5789
5797
  ctx.globalCompositeOperation = "source-over";
5790
5798
  }
5799
+ _mark("10_post_processing");
5791
5800
  // ── 10e. Generative borders — archetype-driven decorative frames ──
5792
5801
  {
5793
5802
  ctx.save();
@@ -5954,6 +5963,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
5954
5963
  // Other archetypes: no border (intentional — not every image needs one)
5955
5964
  ctx.restore();
5956
5965
  }
5966
+ _mark("10e_borders");
5957
5967
  // ── 11. Signature mark — placed in the least-dense corner ──────
5958
5968
  {
5959
5969
  const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
@@ -6021,6 +6031,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
6021
6031
  ctx.restore();
6022
6032
  }
6023
6033
  ctx.globalAlpha = 1;
6034
+ _mark("11_signature");
6035
+ // Clean up custom shape profiles to avoid leaking into subsequent renders
6036
+ for (const name of customShapeNames)delete (0, $8286059160ee2e04$export$4343b39fe47bd82c)[name];
6024
6037
  }
6025
6038
 
6026
6039