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/dist/module.js CHANGED
@@ -2081,14 +2081,15 @@ function $9beb8f41637c29fd$export$909ab0580e273f19(style) {
2081
2081
  }
2082
2082
  }
2083
2083
  function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
2084
- const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
2084
+ const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, activeShapes: activeShapes } = config;
2085
2085
  ctx.save();
2086
2086
  ctx.translate(x, y);
2087
2087
  ctx.rotate(rotation * Math.PI / 180);
2088
2088
  ctx.fillStyle = fillColor;
2089
2089
  ctx.strokeStyle = strokeColor;
2090
2090
  ctx.lineWidth = strokeWidth;
2091
- const drawFunction = (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[shape];
2091
+ const registry = activeShapes ?? (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5);
2092
+ const drawFunction = registry[shape];
2092
2093
  if (drawFunction) {
2093
2094
  drawFunction(ctx, size);
2094
2095
  ctx.fill();
@@ -2598,7 +2599,8 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2598
2599
  } else ctx.fillStyle = fillColor;
2599
2600
  ctx.strokeStyle = strokeColor;
2600
2601
  ctx.lineWidth = strokeWidth;
2601
- const drawFunction = (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[shape];
2602
+ const registry = config.activeShapes ?? (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5);
2603
+ const drawFunction = registry[shape];
2602
2604
  if (drawFunction) {
2603
2605
  drawFunction(ctx, size, {
2604
2606
  rng: rng
@@ -3462,6 +3464,26 @@ const $24064302523652b1$export$4343b39fe47bd82c = {
3462
3464
  ]
3463
3465
  }
3464
3466
  };
3467
+ function $24064302523652b1$export$90912290d628650f(name, partial) {
3468
+ $24064302523652b1$export$4343b39fe47bd82c[name] = {
3469
+ tier: partial?.tier ?? 2,
3470
+ minSizeFraction: partial?.minSizeFraction ?? 0.05,
3471
+ maxSizeFraction: partial?.maxSizeFraction ?? 1.0,
3472
+ affinities: partial?.affinities ?? [
3473
+ "circle",
3474
+ "square"
3475
+ ],
3476
+ category: "procedural",
3477
+ heroCandidate: partial?.heroCandidate ?? false,
3478
+ bestStyles: partial?.bestStyles ?? [
3479
+ "fill-and-stroke",
3480
+ "watercolor"
3481
+ ]
3482
+ };
3483
+ }
3484
+ function $24064302523652b1$export$f4ca68bd046f15ae(name) {
3485
+ delete $24064302523652b1$export$4343b39fe47bd82c[name];
3486
+ }
3465
3487
  function $24064302523652b1$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
3466
3488
  const available = shapeNames.filter((s)=>$24064302523652b1$export$4343b39fe47bd82c[s]);
3467
3489
  // Pick a seed shape — tier 1 shapes that are hero candidates
@@ -3651,7 +3673,14 @@ function $24064302523652b1$export$ab873bb6fb56c1a8(shapeName, layerStyle, rng) {
3651
3673
 
3652
3674
 
3653
3675
  /**
3654
- * Configuration options for image generation.
3676
+ * Draw function signature for custom shapes.
3677
+ * The function should build a canvas path (moveTo/lineTo/arc/etc.)
3678
+ * centered at the origin. The pipeline handles translate, rotate,
3679
+ * fill, and stroke — your function just defines the geometry.
3680
+ *
3681
+ * @param ctx - Canvas 2D rendering context (already translated to shape center)
3682
+ * @param size - Bounding size in pixels
3683
+ * @param rng - Deterministic RNG seeded from the git hash — use this instead of Math.random()
3655
3684
  */ const $2bfb6a1ccb7a82ae$export$c2f8e0cc249a8d8f = {
3656
3685
  width: 2048,
3657
3686
  height: 2048,
@@ -4593,6 +4622,15 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4593
4622
  ...(0, $2bfb6a1ccb7a82ae$export$c2f8e0cc249a8d8f),
4594
4623
  ...config
4595
4624
  };
4625
+ const _dt = finalConfig._debugTiming;
4626
+ const _t = _dt ? ()=>performance.now() : undefined;
4627
+ let _p = _t ? _t() : 0;
4628
+ function _mark(name) {
4629
+ if (!_dt || !_t) return;
4630
+ const now = _t();
4631
+ _dt.phases[name] = now - _p;
4632
+ _p = now;
4633
+ }
4596
4634
  const rng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash));
4597
4635
  // ── 0. Select archetype — fundamentally different visual personality ──
4598
4636
  const archetype = (0, $3faa2521b78398cf$export$f1142fd7da4d6590)(rng);
@@ -4613,7 +4651,39 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4613
4651
  // ── 0b. Color hierarchy — dominant/secondary/accent weighting ──
4614
4652
  const colorHierarchy = (0, $9d614e7d77fc2947$export$fabac4600b87056)(colors, rng);
4615
4653
  // ── 0c. Shape palette — curated shapes that work well together ──
4616
- const shapeNames = Object.keys((0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5));
4654
+ // Merge custom shapes into a combined registry
4655
+ const customShapeNames = [];
4656
+ let activeShapes;
4657
+ if (finalConfig.customShapes && Object.keys(finalConfig.customShapes).length > 0) {
4658
+ activeShapes = {
4659
+ ...(0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)
4660
+ };
4661
+ for (const [name, def] of Object.entries(finalConfig.customShapes)){
4662
+ // Wrap CustomDrawFunction (ctx, size, rng) into DrawFunction (ctx, size, config?)
4663
+ const customDraw = def.draw;
4664
+ activeShapes[name] = (ctx, size, config)=>{
4665
+ customDraw(ctx, size, config?.rng ?? Math.random);
4666
+ };
4667
+ // Register profile for affinity system (inlined to avoid ESM interop issues)
4668
+ (0, $24064302523652b1$export$4343b39fe47bd82c)[name] = {
4669
+ tier: def.profile?.tier ?? 2,
4670
+ minSizeFraction: def.profile?.minSizeFraction ?? 0.05,
4671
+ maxSizeFraction: def.profile?.maxSizeFraction ?? 1.0,
4672
+ affinities: def.profile?.affinities ?? [
4673
+ "circle",
4674
+ "square"
4675
+ ],
4676
+ category: "procedural",
4677
+ heroCandidate: def.profile?.heroCandidate ?? false,
4678
+ bestStyles: def.profile?.bestStyles ?? [
4679
+ "fill-and-stroke",
4680
+ "watercolor"
4681
+ ]
4682
+ };
4683
+ customShapeNames.push(name);
4684
+ }
4685
+ }
4686
+ const shapeNames = Object.keys(activeShapes ?? (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5));
4617
4687
  const shapePalette = (0, $24064302523652b1$export$4a95df8944b5033b)(rng, shapeNames, archetype.name);
4618
4688
  // ── 0d. Color grading — unified tone for the whole image ───────
4619
4689
  const colorGrade = (0, $9d614e7d77fc2947$export$6d1620b367f86f7a)(rng);
@@ -4626,12 +4696,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4626
4696
  const adjustedMaxSize = maxShapeSize * scaleFactor;
4627
4697
  const cx = width / 2;
4628
4698
  const cy = height / 2;
4699
+ _mark("0_setup");
4629
4700
  // ── 1. Background ──────────────────────────────────────────────
4630
4701
  const bgRadius = Math.hypot(cx, cy);
4631
4702
  $b623126c6e9cbb71$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
4632
4703
  // Gradient mesh overlay — 3-4 color control points for richer backgrounds
4704
+ // Use source-over instead of soft-light for cheaper compositing
4633
4705
  const meshPoints = 3 + Math.floor(rng() * 2);
4634
- ctx.globalCompositeOperation = "soft-light";
4706
+ ctx.globalAlpha = 1;
4635
4707
  for(let i = 0; i < meshPoints; i++){
4636
4708
  const mx = rng() * width;
4637
4709
  const my = rng() * height;
@@ -4640,95 +4712,103 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4640
4712
  const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
4641
4713
  grad.addColorStop(0, (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
4642
4714
  grad.addColorStop(1, "rgba(0,0,0,0)");
4643
- ctx.globalAlpha = 1;
4644
4715
  ctx.fillStyle = grad;
4645
- ctx.fillRect(0, 0, width, height);
4716
+ // Clip to gradient bounding box — avoids blending transparent pixels
4717
+ const gx = Math.max(0, mx - mRadius);
4718
+ const gy = Math.max(0, my - mRadius);
4719
+ const gw = Math.min(width, mx + mRadius) - gx;
4720
+ const gh = Math.min(height, my + mRadius) - gy;
4721
+ ctx.fillRect(gx, gy, gw, gh);
4646
4722
  }
4647
- ctx.globalCompositeOperation = "source-over";
4648
4723
  // Compute average background luminance for contrast enforcement
4649
4724
  const bgLum = ((0, $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $9d614e7d77fc2947$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
4650
4725
  // ── 1b. Layered background — archetype-coherent shapes ─────────
4726
+ // Use source-over with pre-multiplied alpha instead of soft-light
4727
+ // for much cheaper compositing (soft-light requires per-pixel blend)
4651
4728
  const bgShapeCount = 3 + Math.floor(rng() * 4);
4652
- ctx.globalCompositeOperation = "soft-light";
4653
4729
  for(let i = 0; i < bgShapeCount; i++){
4654
4730
  const bx = rng() * width;
4655
4731
  const by = rng() * height;
4656
4732
  const bSize = width * 0.3 + rng() * width * 0.5;
4657
4733
  const bColor = (0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4658
- ctx.globalAlpha = 0.03 + rng() * 0.05;
4734
+ ctx.globalAlpha = (0.03 + rng() * 0.05) * 0.5; // halved to compensate for source-over vs soft-light
4659
4735
  ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(bColor, 0.15);
4660
4736
  ctx.beginPath();
4661
4737
  // Use archetype-appropriate background shapes
4662
- if (archetype.name === "geometric-precision" || archetype.name === "op-art") // Rectangular shapes for geometric archetypes
4663
- ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
4738
+ if (archetype.name === "geometric-precision" || archetype.name === "op-art") ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
4664
4739
  else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
4665
4740
  ctx.fill();
4666
4741
  }
4667
- // Subtle concentric rings from center
4742
+ // Subtle concentric rings from center — batched into single stroke
4668
4743
  const ringCount = 2 + Math.floor(rng() * 3);
4669
4744
  ctx.globalAlpha = 0.02 + rng() * 0.03;
4670
4745
  ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4671
4746
  ctx.lineWidth = 1 * scaleFactor;
4747
+ ctx.beginPath();
4672
4748
  for(let i = 1; i <= ringCount; i++){
4673
4749
  const r = Math.min(width, height) * 0.15 * i;
4674
- ctx.beginPath();
4750
+ ctx.moveTo(cx + r, cy);
4675
4751
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
4676
- ctx.stroke();
4677
4752
  }
4678
- ctx.globalCompositeOperation = "source-over";
4753
+ ctx.stroke();
4679
4754
  // ── 1c. Background pattern layer — subtle textured paper ───────
4680
4755
  const bgPatternRoll = rng();
4681
4756
  if (bgPatternRoll < 0.6) {
4682
4757
  ctx.save();
4683
- ctx.globalCompositeOperation = "soft-light";
4684
4758
  const patternOpacity = 0.02 + rng() * 0.04;
4685
4759
  const patternColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4686
4760
  if (bgPatternRoll < 0.2) {
4687
- // Dot grid — batched into a single path
4688
- const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4689
- const dotR = dotSpacing * 0.08;
4761
+ // Dot grid — use fillRect instead of arcs (much cheaper, no path building)
4762
+ const dotSpacing = Math.max(12, Math.min(width, height) * (0.015 + rng() * 0.015));
4763
+ const dotDiam = Math.max(1, Math.round(dotSpacing * 0.16));
4690
4764
  ctx.globalAlpha = patternOpacity;
4691
4765
  ctx.fillStyle = patternColor;
4692
- ctx.beginPath();
4693
- for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4694
- ctx.moveTo(px + dotR, py);
4695
- ctx.arc(px, py, dotR, 0, Math.PI * 2);
4766
+ let dotCount = 0;
4767
+ for(let px = 0; px < width && dotCount < 2000; px += dotSpacing)for(let py = 0; py < height && dotCount < 2000; py += dotSpacing){
4768
+ ctx.fillRect(px, py, dotDiam, dotDiam);
4769
+ dotCount++;
4696
4770
  }
4697
- ctx.fill();
4698
4771
  } else if (bgPatternRoll < 0.4) {
4699
- // Diagonal lines — batched into a single path
4700
- const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4772
+ // Diagonal lines — batched into a single path, capped at 300 lines
4773
+ const lineSpacing = Math.max(10, Math.min(width, height) * (0.02 + rng() * 0.02));
4701
4774
  ctx.globalAlpha = patternOpacity;
4702
4775
  ctx.strokeStyle = patternColor;
4703
4776
  ctx.lineWidth = 0.5 * scaleFactor;
4704
4777
  const diag = Math.hypot(width, height);
4705
4778
  ctx.beginPath();
4706
- for(let d = -diag; d < diag; d += lineSpacing){
4779
+ let lineCount = 0;
4780
+ for(let d = -diag; d < diag && lineCount < 300; d += lineSpacing){
4707
4781
  ctx.moveTo(d, 0);
4708
4782
  ctx.lineTo(d + height, height);
4783
+ lineCount++;
4709
4784
  }
4710
4785
  ctx.stroke();
4711
4786
  } else {
4712
- // Tessellation — hexagonal grid, batched into a single path
4713
- const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4787
+ // Tessellation — hexagonal grid, capped at 500 hexagons
4788
+ const tessSize = Math.max(15, Math.min(width, height) * (0.025 + rng() * 0.02));
4714
4789
  const tessH = tessSize * Math.sqrt(3);
4715
4790
  ctx.globalAlpha = patternOpacity * 0.7;
4716
4791
  ctx.strokeStyle = patternColor;
4717
4792
  ctx.lineWidth = 0.4 * scaleFactor;
4793
+ // Pre-compute hex vertex offsets (avoid trig per vertex)
4794
+ const hexVx = [];
4795
+ const hexVy = [];
4796
+ for(let s = 0; s < 6; s++){
4797
+ const angle = Math.PI / 3 * s - Math.PI / 6;
4798
+ hexVx.push(Math.cos(angle) * tessSize * 0.5);
4799
+ hexVy.push(Math.sin(angle) * tessSize * 0.5);
4800
+ }
4718
4801
  ctx.beginPath();
4719
- for(let row = 0; row * tessH < height + tessH; row++){
4802
+ let hexCount = 0;
4803
+ for(let row = 0; row * tessH < height + tessH && hexCount < 500; row++){
4720
4804
  const offsetX = row % 2 * tessSize * 0.75;
4721
- for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4805
+ for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5 && hexCount < 500; col++){
4722
4806
  const hx = col * tessSize * 1.5 + offsetX;
4723
4807
  const hy = row * tessH;
4724
- for(let s = 0; s < 6; s++){
4725
- const angle = Math.PI / 3 * s - Math.PI / 6;
4726
- const vx = hx + Math.cos(angle) * tessSize * 0.5;
4727
- const vy = hy + Math.sin(angle) * tessSize * 0.5;
4728
- if (s === 0) ctx.moveTo(vx, vy);
4729
- else ctx.lineTo(vx, vy);
4730
- }
4808
+ ctx.moveTo(hx + hexVx[0], hy + hexVy[0]);
4809
+ for(let s = 1; s < 6; s++)ctx.lineTo(hx + hexVx[s], hy + hexVy[s]);
4731
4810
  ctx.closePath();
4811
+ hexCount++;
4732
4812
  }
4733
4813
  }
4734
4814
  ctx.stroke();
@@ -4736,6 +4816,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4736
4816
  ctx.restore();
4737
4817
  }
4738
4818
  ctx.globalCompositeOperation = "source-over";
4819
+ _mark("1_background");
4739
4820
  // ── 2. Composition mode — archetype-aware selection ──────────────
4740
4821
  const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES.length)];
4741
4822
  const symRoll = rng();
@@ -4846,6 +4927,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4846
4927
  }
4847
4928
  }
4848
4929
  ctx.globalAlpha = 1;
4930
+ _mark("2_3_composition_focal");
4849
4931
  // ── 4. Flow field — simplex noise for organic variation ─────────
4850
4932
  // Create a seeded simplex noise field (unique per hash)
4851
4933
  const noiseFieldRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 333));
@@ -4902,7 +4984,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4902
4984
  renderStyle: heroStyle,
4903
4985
  rng: rng,
4904
4986
  lightAngle: lightAngle,
4905
- scaleFactor: scaleFactor
4987
+ scaleFactor: scaleFactor,
4988
+ activeShapes: activeShapes
4906
4989
  });
4907
4990
  heroCenter = {
4908
4991
  x: heroFocal.x,
@@ -4922,6 +5005,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
4922
5005
  shape: heroShape
4923
5006
  });
4924
5007
  }
5008
+ _mark("4_flowfield_hero");
4925
5009
  // ── 5. Shape layers ────────────────────────────────────────────
4926
5010
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4927
5011
  // ── Complexity budget — caps total rendering work ──────────────
@@ -5104,7 +5188,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5104
5188
  renderStyle: finalRenderStyle,
5105
5189
  rng: rng,
5106
5190
  lightAngle: lightAngle,
5107
- scaleFactor: scaleFactor
5191
+ scaleFactor: scaleFactor,
5192
+ activeShapes: activeShapes
5108
5193
  };
5109
5194
  if (shouldMirror) {
5110
5195
  (0, $9beb8f41637c29fd$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
@@ -5135,7 +5220,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5135
5220
  rotation: rotation,
5136
5221
  proportionType: "GOLDEN_RATIO",
5137
5222
  renderStyle: "fill-only",
5138
- rng: rng
5223
+ rng: rng,
5224
+ activeShapes: activeShapes
5139
5225
  });
5140
5226
  }
5141
5227
  extrasSpent += glazePasses;
@@ -5175,7 +5261,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5175
5261
  rotation: rotation + (e + 1) * 15,
5176
5262
  proportionType: "GOLDEN_RATIO",
5177
5263
  renderStyle: finalRenderStyle,
5178
- rng: rng
5264
+ rng: rng,
5265
+ activeShapes: activeShapes
5179
5266
  });
5180
5267
  shapePositions.push({
5181
5268
  x: echoX,
@@ -5222,7 +5309,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5222
5309
  rotation: innerRot,
5223
5310
  proportionType: "GOLDEN_RATIO",
5224
5311
  renderStyle: innerStyle,
5225
- rng: rng
5312
+ rng: rng,
5313
+ activeShapes: activeShapes
5226
5314
  });
5227
5315
  extrasSpent += $b623126c6e9cbb71$var$RENDER_STYLE_COST[innerStyle] ?? 1;
5228
5316
  }
@@ -5269,7 +5357,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5269
5357
  rotation: member.rotation + groupRotation * 180 / Math.PI,
5270
5358
  proportionType: "GOLDEN_RATIO",
5271
5359
  renderStyle: memberStyle,
5272
- rng: rng
5360
+ rng: rng,
5361
+ activeShapes: activeShapes
5273
5362
  });
5274
5363
  shapePositions.push({
5275
5364
  x: mx,
@@ -5323,7 +5412,8 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5323
5412
  rotation: rotation + (r + 1) * 12,
5324
5413
  proportionType: "GOLDEN_RATIO",
5325
5414
  renderStyle: finalRenderStyle,
5326
- rng: rng
5415
+ rng: rng,
5416
+ activeShapes: activeShapes
5327
5417
  });
5328
5418
  shapePositions.push({
5329
5419
  x: rx,
@@ -5350,64 +5440,13 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5350
5440
  }
5351
5441
  // Reset blend mode for post-processing passes
5352
5442
  ctx.globalCompositeOperation = "source-over";
5353
- // ── 5g. Layered masking / cutout portals ───────────────────────
5354
- // ~18% of images get 1-3 portal windows that paint over foreground
5355
- // with a tinted background wash, creating a "peek through" effect.
5356
- if (rng() < 0.18 && shapePositions.length > 3) {
5357
- const portalCount = 1 + Math.floor(rng() * 2);
5358
- for(let p = 0; p < portalCount; p++){
5359
- // Pick a position biased toward placed shapes
5360
- const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
5361
- const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
5362
- const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
5363
- const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
5364
- // Pick a portal shape from the palette
5365
- const portalShape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
5366
- const portalRotation = rng() * 360;
5367
- const portalAlpha = 0.6 + rng() * 0.35;
5368
- ctx.save();
5369
- ctx.translate(portalX, portalY);
5370
- ctx.rotate(portalRotation * Math.PI / 180);
5371
- // Step 1: Clip to the portal shape and fill with background wash
5372
- ctx.beginPath();
5373
- (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
5374
- ctx.clip();
5375
- // Fill the clipped region with a radial gradient from background colors
5376
- const portalColor = (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
5377
- const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
5378
- portalGrad.addColorStop(0, portalColor);
5379
- portalGrad.addColorStop(1, bgEnd);
5380
- ctx.globalAlpha = portalAlpha;
5381
- ctx.fillStyle = portalGrad;
5382
- ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
5383
- // Optional: subtle inner texture — a few tiny dots inside the portal
5384
- if (rng() < 0.5) {
5385
- const dotCount = 3 + Math.floor(rng() * 5);
5386
- ctx.globalAlpha = portalAlpha * 0.3;
5387
- ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
5388
- for(let d = 0; d < dotCount; d++){
5389
- const dx = (rng() - 0.5) * portalSize * 1.4;
5390
- const dy = (rng() - 0.5) * portalSize * 1.4;
5391
- const dr = (1 + rng() * 3) * scaleFactor;
5392
- ctx.beginPath();
5393
- ctx.arc(dx, dy, dr, 0, Math.PI * 2);
5394
- ctx.fill();
5395
- }
5396
- }
5397
- ctx.restore();
5398
- // Step 2: Draw a border ring around the portal (outside the clip)
5399
- ctx.save();
5400
- ctx.translate(portalX, portalY);
5401
- ctx.rotate(portalRotation * Math.PI / 180);
5402
- ctx.globalAlpha = 0.15 + rng() * 0.2;
5403
- ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
5404
- ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
5405
- ctx.beginPath();
5406
- (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
5407
- ctx.stroke();
5408
- ctx.restore();
5409
- }
5443
+ if (_dt) {
5444
+ _dt.shapeCount = shapePositions.length;
5445
+ _dt.extraCount = extrasSpent;
5410
5446
  }
5447
+ _mark("5_shape_layers");
5448
+ // ── 5g. (Portal/cutout feature removed — replaced by custom shapes API) ──
5449
+ _mark("5g_portals");
5411
5450
  // ── 6. Flow-line pass — variable color, branching, pressure ────
5412
5451
  // Optimized: collect all segments into width-quantized buckets, then
5413
5452
  // render each bucket as a single batched path. This reduces
@@ -5533,6 +5572,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5533
5572
  ctx.stroke();
5534
5573
  }
5535
5574
  }
5575
+ _mark("6_flow_lines");
5536
5576
  // ── 6b. Motion/energy lines — short directional bursts ─────────
5537
5577
  // Optimized: collect all burst segments, then batch by quantized alpha
5538
5578
  const energyArchetypes = [
@@ -5595,6 +5635,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5595
5635
  ctx.stroke();
5596
5636
  }
5597
5637
  }
5638
+ _mark("6b_energy_lines");
5598
5639
  // ── 6c. Apply symmetry mirroring ─────────────────────────────────
5599
5640
  if (symmetryMode !== "none") {
5600
5641
  const canvas = ctx.canvas;
@@ -5615,60 +5656,25 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5615
5656
  }
5616
5657
  ctx.restore();
5617
5658
  }
5618
- // ── 7. Noise texture overlay — batched via ImageData ─────────────
5619
- // Optimized: cap density at large sizes (diminishing returns above ~2K dots),
5620
- // skip inner pixelScale loop when scale=1, use Uint32Array for faster writes.
5659
+ _mark("6c_symmetry");
5660
+ // ── 7. Noise texture overlay ─────────────────────────────────────
5661
+ // With density capped at 2500 dots, direct fillRect calls are far cheaper
5662
+ // than the getImageData/putImageData round-trip which copies the entire
5663
+ // pixel buffer (4 × width × height bytes) twice.
5621
5664
  const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
5622
5665
  const rawNoiseDensity = Math.floor(width * height / 800);
5623
- // Cap at 2500 dots — beyond this the visual effect is indistinguishable
5624
- // but getImageData/putImageData cost scales with canvas size
5625
5666
  const noiseDensity = Math.min(rawNoiseDensity, 2500);
5626
- try {
5627
- const imageData = ctx.getImageData(0, 0, width, height);
5628
- const data = imageData.data;
5629
- const pixelScale = Math.max(1, Math.round(scaleFactor));
5630
- if (pixelScale === 1) // Fast path no inner loop, direct pixel write
5631
- // Pre-compute alpha blend as integer math (avoid float multiply per channel)
5632
- for(let i = 0; i < noiseDensity; i++){
5633
- const nx = Math.floor(noiseRng() * width);
5634
- const ny = Math.floor(noiseRng() * height);
5635
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5636
- // srcA in range [0.01, 0.04] — multiply by 256 for fixed-point
5637
- const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
5638
- const invA256 = 256 - srcA256;
5639
- const bSrc = brightness * srcA256; // pre-multiply brightness × alpha
5640
- const idx = ny * width + nx << 2;
5641
- data[idx] = data[idx] * invA256 + bSrc >> 8;
5642
- data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
5643
- data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
5644
- }
5645
- else for(let i = 0; i < noiseDensity; i++){
5646
- const nx = Math.floor(noiseRng() * width);
5647
- const ny = Math.floor(noiseRng() * height);
5648
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5649
- const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
5650
- const invA256 = 256 - srcA256;
5651
- const bSrc = brightness * srcA256;
5652
- for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
5653
- const idx = (ny + dy) * width + (nx + dx) << 2;
5654
- data[idx] = data[idx] * invA256 + bSrc >> 8;
5655
- data[idx + 1] = data[idx + 1] * invA256 + bSrc >> 8;
5656
- data[idx + 2] = data[idx + 2] * invA256 + bSrc >> 8;
5657
- }
5658
- }
5659
- ctx.putImageData(imageData, 0, 0);
5660
- } catch {
5661
- // Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
5662
- for(let i = 0; i < noiseDensity; i++){
5663
- const nx = noiseRng() * width;
5664
- const ny = noiseRng() * height;
5665
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5666
- const alpha = 0.01 + noiseRng() * 0.03;
5667
- ctx.globalAlpha = alpha;
5668
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5669
- ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5670
- }
5667
+ const pixelScale = Math.max(1, Math.round(scaleFactor));
5668
+ for(let i = 0; i < noiseDensity; i++){
5669
+ const nx = noiseRng() * width;
5670
+ const ny = noiseRng() * height;
5671
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5672
+ const alpha = 0.01 + noiseRng() * 0.03;
5673
+ ctx.globalAlpha = alpha;
5674
+ ctx.fillStyle = `rgb(${brightness},${brightness},${brightness})`;
5675
+ ctx.fillRect(nx, ny, pixelScale, pixelScale);
5671
5676
  }
5677
+ _mark("7_noise_texture");
5672
5678
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
5673
5679
  ctx.globalAlpha = 1;
5674
5680
  const vignetteStrength = 0.25 + rng() * 0.2;
@@ -5682,6 +5688,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5682
5688
  vigGrad.addColorStop(1, vignetteColor);
5683
5689
  ctx.fillStyle = vigGrad;
5684
5690
  ctx.fillRect(0, 0, width, height);
5691
+ _mark("8_vignette");
5685
5692
  // ── 9. Organic connecting curves — proximity-aware ───────────────
5686
5693
  // Optimized: batch all curves into alpha-quantized groups to reduce
5687
5694
  // beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
@@ -5740,6 +5747,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5740
5747
  ctx.stroke();
5741
5748
  }
5742
5749
  }
5750
+ _mark("9_connecting_curves");
5743
5751
  // ── 10. Post-processing ────────────────────────────────────────
5744
5752
  // 10a. Color grading — unified tone across the whole image
5745
5753
  // Apply as a semi-transparent overlay in the grade hue
@@ -5799,6 +5807,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5799
5807
  ctx.fillRect(0, 0, width, height);
5800
5808
  ctx.globalCompositeOperation = "source-over";
5801
5809
  }
5810
+ _mark("10_post_processing");
5802
5811
  // ── 10e. Generative borders — archetype-driven decorative frames ──
5803
5812
  {
5804
5813
  ctx.save();
@@ -5965,6 +5974,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
5965
5974
  // Other archetypes: no border (intentional — not every image needs one)
5966
5975
  ctx.restore();
5967
5976
  }
5977
+ _mark("10e_borders");
5968
5978
  // ── 11. Signature mark — placed in the least-dense corner ──────
5969
5979
  {
5970
5980
  const sigRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
@@ -6032,6 +6042,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
6032
6042
  ctx.restore();
6033
6043
  }
6034
6044
  ctx.globalAlpha = 1;
6045
+ _mark("11_signature");
6046
+ // Clean up custom shape profiles to avoid leaking into subsequent renders
6047
+ for (const name of customShapeNames)delete (0, $24064302523652b1$export$4343b39fe47bd82c)[name];
6035
6048
  }
6036
6049
 
6037
6050