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/main.js CHANGED
@@ -2095,14 +2095,15 @@ function $c3de8257a8baa3b0$export$909ab0580e273f19(style) {
2095
2095
  }
2096
2096
  }
2097
2097
  function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
2098
- const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
2098
+ const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, activeShapes: activeShapes } = config;
2099
2099
  ctx.save();
2100
2100
  ctx.translate(x, y);
2101
2101
  ctx.rotate(rotation * Math.PI / 180);
2102
2102
  ctx.fillStyle = fillColor;
2103
2103
  ctx.strokeStyle = strokeColor;
2104
2104
  ctx.lineWidth = strokeWidth;
2105
- const drawFunction = (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[shape];
2105
+ const registry = activeShapes ?? (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5);
2106
+ const drawFunction = registry[shape];
2106
2107
  if (drawFunction) {
2107
2108
  drawFunction(ctx, size);
2108
2109
  ctx.fill();
@@ -2612,7 +2613,8 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2612
2613
  } else ctx.fillStyle = fillColor;
2613
2614
  ctx.strokeStyle = strokeColor;
2614
2615
  ctx.lineWidth = strokeWidth;
2615
- const drawFunction = (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[shape];
2616
+ const registry = config.activeShapes ?? (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5);
2617
+ const drawFunction = registry[shape];
2616
2618
  if (drawFunction) {
2617
2619
  drawFunction(ctx, size, {
2618
2620
  rng: rng
@@ -3476,6 +3478,26 @@ const $e73976f898150d4d$export$4343b39fe47bd82c = {
3476
3478
  ]
3477
3479
  }
3478
3480
  };
3481
+ function $e73976f898150d4d$export$90912290d628650f(name, partial) {
3482
+ $e73976f898150d4d$export$4343b39fe47bd82c[name] = {
3483
+ tier: partial?.tier ?? 2,
3484
+ minSizeFraction: partial?.minSizeFraction ?? 0.05,
3485
+ maxSizeFraction: partial?.maxSizeFraction ?? 1.0,
3486
+ affinities: partial?.affinities ?? [
3487
+ "circle",
3488
+ "square"
3489
+ ],
3490
+ category: "procedural",
3491
+ heroCandidate: partial?.heroCandidate ?? false,
3492
+ bestStyles: partial?.bestStyles ?? [
3493
+ "fill-and-stroke",
3494
+ "watercolor"
3495
+ ]
3496
+ };
3497
+ }
3498
+ function $e73976f898150d4d$export$f4ca68bd046f15ae(name) {
3499
+ delete $e73976f898150d4d$export$4343b39fe47bd82c[name];
3500
+ }
3479
3501
  function $e73976f898150d4d$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
3480
3502
  const available = shapeNames.filter((s)=>$e73976f898150d4d$export$4343b39fe47bd82c[s]);
3481
3503
  // Pick a seed shape — tier 1 shapes that are hero candidates
@@ -3665,7 +3687,14 @@ function $e73976f898150d4d$export$ab873bb6fb56c1a8(shapeName, layerStyle, rng) {
3665
3687
 
3666
3688
 
3667
3689
  /**
3668
- * Configuration options for image generation.
3690
+ * Draw function signature for custom shapes.
3691
+ * The function should build a canvas path (moveTo/lineTo/arc/etc.)
3692
+ * centered at the origin. The pipeline handles translate, rotate,
3693
+ * fill, and stroke — your function just defines the geometry.
3694
+ *
3695
+ * @param ctx - Canvas 2D rendering context (already translated to shape center)
3696
+ * @param size - Bounding size in pixels
3697
+ * @param rng - Deterministic RNG seeded from the git hash — use this instead of Math.random()
3669
3698
  */ const $93cf69256c93baa9$export$c2f8e0cc249a8d8f = {
3670
3699
  width: 2048,
3671
3700
  height: 2048,
@@ -4607,6 +4636,15 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4607
4636
  ...(0, $93cf69256c93baa9$export$c2f8e0cc249a8d8f),
4608
4637
  ...config
4609
4638
  };
4639
+ const _dt = finalConfig._debugTiming;
4640
+ const _t = _dt ? ()=>performance.now() : undefined;
4641
+ let _p = _t ? _t() : 0;
4642
+ function _mark(name) {
4643
+ if (!_dt || !_t) return;
4644
+ const now = _t();
4645
+ _dt.phases[name] = now - _p;
4646
+ _p = now;
4647
+ }
4610
4648
  const rng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash));
4611
4649
  // ── 0. Select archetype — fundamentally different visual personality ──
4612
4650
  const archetype = (0, $f89bc858f7202849$export$f1142fd7da4d6590)(rng);
@@ -4627,7 +4665,39 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4627
4665
  // ── 0b. Color hierarchy — dominant/secondary/accent weighting ──
4628
4666
  const colorHierarchy = (0, $d016ad53434219a1$export$fabac4600b87056)(colors, rng);
4629
4667
  // ── 0c. Shape palette — curated shapes that work well together ──
4630
- const shapeNames = Object.keys((0, $9c828bde2acaae64$export$4ff7fc6f1af248b5));
4668
+ // Merge custom shapes into a combined registry
4669
+ const customShapeNames = [];
4670
+ let activeShapes;
4671
+ if (finalConfig.customShapes && Object.keys(finalConfig.customShapes).length > 0) {
4672
+ activeShapes = {
4673
+ ...(0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)
4674
+ };
4675
+ for (const [name, def] of Object.entries(finalConfig.customShapes)){
4676
+ // Wrap CustomDrawFunction (ctx, size, rng) into DrawFunction (ctx, size, config?)
4677
+ const customDraw = def.draw;
4678
+ activeShapes[name] = (ctx, size, config)=>{
4679
+ customDraw(ctx, size, config?.rng ?? Math.random);
4680
+ };
4681
+ // Register profile for affinity system (inlined to avoid ESM interop issues)
4682
+ (0, $e73976f898150d4d$export$4343b39fe47bd82c)[name] = {
4683
+ tier: def.profile?.tier ?? 2,
4684
+ minSizeFraction: def.profile?.minSizeFraction ?? 0.05,
4685
+ maxSizeFraction: def.profile?.maxSizeFraction ?? 1.0,
4686
+ affinities: def.profile?.affinities ?? [
4687
+ "circle",
4688
+ "square"
4689
+ ],
4690
+ category: "procedural",
4691
+ heroCandidate: def.profile?.heroCandidate ?? false,
4692
+ bestStyles: def.profile?.bestStyles ?? [
4693
+ "fill-and-stroke",
4694
+ "watercolor"
4695
+ ]
4696
+ };
4697
+ customShapeNames.push(name);
4698
+ }
4699
+ }
4700
+ const shapeNames = Object.keys(activeShapes ?? (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5));
4631
4701
  const shapePalette = (0, $e73976f898150d4d$export$4a95df8944b5033b)(rng, shapeNames, archetype.name);
4632
4702
  // ── 0d. Color grading — unified tone for the whole image ───────
4633
4703
  const colorGrade = (0, $d016ad53434219a1$export$6d1620b367f86f7a)(rng);
@@ -4640,12 +4710,14 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4640
4710
  const adjustedMaxSize = maxShapeSize * scaleFactor;
4641
4711
  const cx = width / 2;
4642
4712
  const cy = height / 2;
4713
+ _mark("0_setup");
4643
4714
  // ── 1. Background ──────────────────────────────────────────────
4644
4715
  const bgRadius = Math.hypot(cx, cy);
4645
4716
  $4f72c5a314eddf25$var$drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
4646
4717
  // Gradient mesh overlay — 3-4 color control points for richer backgrounds
4718
+ // Use source-over instead of soft-light for cheaper compositing
4647
4719
  const meshPoints = 3 + Math.floor(rng() * 2);
4648
- ctx.globalCompositeOperation = "soft-light";
4720
+ ctx.globalAlpha = 1;
4649
4721
  for(let i = 0; i < meshPoints; i++){
4650
4722
  const mx = rng() * width;
4651
4723
  const my = rng() * height;
@@ -4654,95 +4726,103 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4654
4726
  const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
4655
4727
  grad.addColorStop(0, (0, $d016ad53434219a1$export$f2121afcad3d553f)(mColor, 0.08 + rng() * 0.06));
4656
4728
  grad.addColorStop(1, "rgba(0,0,0,0)");
4657
- ctx.globalAlpha = 1;
4658
4729
  ctx.fillStyle = grad;
4659
- ctx.fillRect(0, 0, width, height);
4730
+ // Clip to gradient bounding box — avoids blending transparent pixels
4731
+ const gx = Math.max(0, mx - mRadius);
4732
+ const gy = Math.max(0, my - mRadius);
4733
+ const gw = Math.min(width, mx + mRadius) - gx;
4734
+ const gh = Math.min(height, my + mRadius) - gy;
4735
+ ctx.fillRect(gx, gy, gw, gh);
4660
4736
  }
4661
- ctx.globalCompositeOperation = "source-over";
4662
4737
  // Compute average background luminance for contrast enforcement
4663
4738
  const bgLum = ((0, $d016ad53434219a1$export$5c6e3c2b59b7fbbe)(bgStart) + (0, $d016ad53434219a1$export$5c6e3c2b59b7fbbe)(bgEnd)) / 2;
4664
4739
  // ── 1b. Layered background — archetype-coherent shapes ─────────
4740
+ // Use source-over with pre-multiplied alpha instead of soft-light
4741
+ // for much cheaper compositing (soft-light requires per-pixel blend)
4665
4742
  const bgShapeCount = 3 + Math.floor(rng() * 4);
4666
- ctx.globalCompositeOperation = "soft-light";
4667
4743
  for(let i = 0; i < bgShapeCount; i++){
4668
4744
  const bx = rng() * width;
4669
4745
  const by = rng() * height;
4670
4746
  const bSize = width * 0.3 + rng() * width * 0.5;
4671
4747
  const bColor = (0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng);
4672
- ctx.globalAlpha = 0.03 + rng() * 0.05;
4748
+ ctx.globalAlpha = (0.03 + rng() * 0.05) * 0.5; // halved to compensate for source-over vs soft-light
4673
4749
  ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(bColor, 0.15);
4674
4750
  ctx.beginPath();
4675
4751
  // Use archetype-appropriate background shapes
4676
- if (archetype.name === "geometric-precision" || archetype.name === "op-art") // Rectangular shapes for geometric archetypes
4677
- ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
4752
+ if (archetype.name === "geometric-precision" || archetype.name === "op-art") ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
4678
4753
  else ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
4679
4754
  ctx.fill();
4680
4755
  }
4681
- // Subtle concentric rings from center
4756
+ // Subtle concentric rings from center — batched into single stroke
4682
4757
  const ringCount = 2 + Math.floor(rng() * 3);
4683
4758
  ctx.globalAlpha = 0.02 + rng() * 0.03;
4684
4759
  ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
4685
4760
  ctx.lineWidth = 1 * scaleFactor;
4761
+ ctx.beginPath();
4686
4762
  for(let i = 1; i <= ringCount; i++){
4687
4763
  const r = Math.min(width, height) * 0.15 * i;
4688
- ctx.beginPath();
4764
+ ctx.moveTo(cx + r, cy);
4689
4765
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
4690
- ctx.stroke();
4691
4766
  }
4692
- ctx.globalCompositeOperation = "source-over";
4767
+ ctx.stroke();
4693
4768
  // ── 1c. Background pattern layer — subtle textured paper ───────
4694
4769
  const bgPatternRoll = rng();
4695
4770
  if (bgPatternRoll < 0.6) {
4696
4771
  ctx.save();
4697
- ctx.globalCompositeOperation = "soft-light";
4698
4772
  const patternOpacity = 0.02 + rng() * 0.04;
4699
4773
  const patternColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
4700
4774
  if (bgPatternRoll < 0.2) {
4701
- // Dot grid — batched into a single path
4702
- const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
4703
- const dotR = dotSpacing * 0.08;
4775
+ // Dot grid — use fillRect instead of arcs (much cheaper, no path building)
4776
+ const dotSpacing = Math.max(12, Math.min(width, height) * (0.015 + rng() * 0.015));
4777
+ const dotDiam = Math.max(1, Math.round(dotSpacing * 0.16));
4704
4778
  ctx.globalAlpha = patternOpacity;
4705
4779
  ctx.fillStyle = patternColor;
4706
- ctx.beginPath();
4707
- for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
4708
- ctx.moveTo(px + dotR, py);
4709
- ctx.arc(px, py, dotR, 0, Math.PI * 2);
4780
+ let dotCount = 0;
4781
+ for(let px = 0; px < width && dotCount < 2000; px += dotSpacing)for(let py = 0; py < height && dotCount < 2000; py += dotSpacing){
4782
+ ctx.fillRect(px, py, dotDiam, dotDiam);
4783
+ dotCount++;
4710
4784
  }
4711
- ctx.fill();
4712
4785
  } else if (bgPatternRoll < 0.4) {
4713
- // Diagonal lines — batched into a single path
4714
- const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
4786
+ // Diagonal lines — batched into a single path, capped at 300 lines
4787
+ const lineSpacing = Math.max(10, Math.min(width, height) * (0.02 + rng() * 0.02));
4715
4788
  ctx.globalAlpha = patternOpacity;
4716
4789
  ctx.strokeStyle = patternColor;
4717
4790
  ctx.lineWidth = 0.5 * scaleFactor;
4718
4791
  const diag = Math.hypot(width, height);
4719
4792
  ctx.beginPath();
4720
- for(let d = -diag; d < diag; d += lineSpacing){
4793
+ let lineCount = 0;
4794
+ for(let d = -diag; d < diag && lineCount < 300; d += lineSpacing){
4721
4795
  ctx.moveTo(d, 0);
4722
4796
  ctx.lineTo(d + height, height);
4797
+ lineCount++;
4723
4798
  }
4724
4799
  ctx.stroke();
4725
4800
  } else {
4726
- // Tessellation — hexagonal grid, batched into a single path
4727
- const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
4801
+ // Tessellation — hexagonal grid, capped at 500 hexagons
4802
+ const tessSize = Math.max(15, Math.min(width, height) * (0.025 + rng() * 0.02));
4728
4803
  const tessH = tessSize * Math.sqrt(3);
4729
4804
  ctx.globalAlpha = patternOpacity * 0.7;
4730
4805
  ctx.strokeStyle = patternColor;
4731
4806
  ctx.lineWidth = 0.4 * scaleFactor;
4807
+ // Pre-compute hex vertex offsets (avoid trig per vertex)
4808
+ const hexVx = [];
4809
+ const hexVy = [];
4810
+ for(let s = 0; s < 6; s++){
4811
+ const angle = Math.PI / 3 * s - Math.PI / 6;
4812
+ hexVx.push(Math.cos(angle) * tessSize * 0.5);
4813
+ hexVy.push(Math.sin(angle) * tessSize * 0.5);
4814
+ }
4732
4815
  ctx.beginPath();
4733
- for(let row = 0; row * tessH < height + tessH; row++){
4816
+ let hexCount = 0;
4817
+ for(let row = 0; row * tessH < height + tessH && hexCount < 500; row++){
4734
4818
  const offsetX = row % 2 * tessSize * 0.75;
4735
- for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
4819
+ for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5 && hexCount < 500; col++){
4736
4820
  const hx = col * tessSize * 1.5 + offsetX;
4737
4821
  const hy = row * tessH;
4738
- for(let s = 0; s < 6; s++){
4739
- const angle = Math.PI / 3 * s - Math.PI / 6;
4740
- const vx = hx + Math.cos(angle) * tessSize * 0.5;
4741
- const vy = hy + Math.sin(angle) * tessSize * 0.5;
4742
- if (s === 0) ctx.moveTo(vx, vy);
4743
- else ctx.lineTo(vx, vy);
4744
- }
4822
+ ctx.moveTo(hx + hexVx[0], hy + hexVy[0]);
4823
+ for(let s = 1; s < 6; s++)ctx.lineTo(hx + hexVx[s], hy + hexVy[s]);
4745
4824
  ctx.closePath();
4825
+ hexCount++;
4746
4826
  }
4747
4827
  }
4748
4828
  ctx.stroke();
@@ -4750,6 +4830,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4750
4830
  ctx.restore();
4751
4831
  }
4752
4832
  ctx.globalCompositeOperation = "source-over";
4833
+ _mark("1_background");
4753
4834
  // ── 2. Composition mode — archetype-aware selection ──────────────
4754
4835
  const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES.length)];
4755
4836
  const symRoll = rng();
@@ -4860,6 +4941,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4860
4941
  }
4861
4942
  }
4862
4943
  ctx.globalAlpha = 1;
4944
+ _mark("2_3_composition_focal");
4863
4945
  // ── 4. Flow field — simplex noise for organic variation ─────────
4864
4946
  // Create a seeded simplex noise field (unique per hash)
4865
4947
  const noiseFieldRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 333));
@@ -4916,7 +4998,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4916
4998
  renderStyle: heroStyle,
4917
4999
  rng: rng,
4918
5000
  lightAngle: lightAngle,
4919
- scaleFactor: scaleFactor
5001
+ scaleFactor: scaleFactor,
5002
+ activeShapes: activeShapes
4920
5003
  });
4921
5004
  heroCenter = {
4922
5005
  x: heroFocal.x,
@@ -4936,6 +5019,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
4936
5019
  shape: heroShape
4937
5020
  });
4938
5021
  }
5022
+ _mark("4_flowfield_hero");
4939
5023
  // ── 5. Shape layers ────────────────────────────────────────────
4940
5024
  const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
4941
5025
  // ── Complexity budget — caps total rendering work ──────────────
@@ -5118,7 +5202,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5118
5202
  renderStyle: finalRenderStyle,
5119
5203
  rng: rng,
5120
5204
  lightAngle: lightAngle,
5121
- scaleFactor: scaleFactor
5205
+ scaleFactor: scaleFactor,
5206
+ activeShapes: activeShapes
5122
5207
  };
5123
5208
  if (shouldMirror) {
5124
5209
  (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
@@ -5149,7 +5234,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5149
5234
  rotation: rotation,
5150
5235
  proportionType: "GOLDEN_RATIO",
5151
5236
  renderStyle: "fill-only",
5152
- rng: rng
5237
+ rng: rng,
5238
+ activeShapes: activeShapes
5153
5239
  });
5154
5240
  }
5155
5241
  extrasSpent += glazePasses;
@@ -5189,7 +5275,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5189
5275
  rotation: rotation + (e + 1) * 15,
5190
5276
  proportionType: "GOLDEN_RATIO",
5191
5277
  renderStyle: finalRenderStyle,
5192
- rng: rng
5278
+ rng: rng,
5279
+ activeShapes: activeShapes
5193
5280
  });
5194
5281
  shapePositions.push({
5195
5282
  x: echoX,
@@ -5236,7 +5323,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5236
5323
  rotation: innerRot,
5237
5324
  proportionType: "GOLDEN_RATIO",
5238
5325
  renderStyle: innerStyle,
5239
- rng: rng
5326
+ rng: rng,
5327
+ activeShapes: activeShapes
5240
5328
  });
5241
5329
  extrasSpent += $4f72c5a314eddf25$var$RENDER_STYLE_COST[innerStyle] ?? 1;
5242
5330
  }
@@ -5283,7 +5371,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5283
5371
  rotation: member.rotation + groupRotation * 180 / Math.PI,
5284
5372
  proportionType: "GOLDEN_RATIO",
5285
5373
  renderStyle: memberStyle,
5286
- rng: rng
5374
+ rng: rng,
5375
+ activeShapes: activeShapes
5287
5376
  });
5288
5377
  shapePositions.push({
5289
5378
  x: mx,
@@ -5337,7 +5426,8 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5337
5426
  rotation: rotation + (r + 1) * 12,
5338
5427
  proportionType: "GOLDEN_RATIO",
5339
5428
  renderStyle: finalRenderStyle,
5340
- rng: rng
5429
+ rng: rng,
5430
+ activeShapes: activeShapes
5341
5431
  });
5342
5432
  shapePositions.push({
5343
5433
  x: rx,
@@ -5364,64 +5454,13 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5364
5454
  }
5365
5455
  // Reset blend mode for post-processing passes
5366
5456
  ctx.globalCompositeOperation = "source-over";
5367
- // ── 5g. Layered masking / cutout portals ───────────────────────
5368
- // ~18% of images get 1-3 portal windows that paint over foreground
5369
- // with a tinted background wash, creating a "peek through" effect.
5370
- if (rng() < 0.18 && shapePositions.length > 3) {
5371
- const portalCount = 1 + Math.floor(rng() * 2);
5372
- for(let p = 0; p < portalCount; p++){
5373
- // Pick a position biased toward placed shapes
5374
- const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
5375
- const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
5376
- const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
5377
- const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
5378
- // Pick a portal shape from the palette
5379
- const portalShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
5380
- const portalRotation = rng() * 360;
5381
- const portalAlpha = 0.6 + rng() * 0.35;
5382
- ctx.save();
5383
- ctx.translate(portalX, portalY);
5384
- ctx.rotate(portalRotation * Math.PI / 180);
5385
- // Step 1: Clip to the portal shape and fill with background wash
5386
- ctx.beginPath();
5387
- (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
5388
- ctx.clip();
5389
- // Fill the clipped region with a radial gradient from background colors
5390
- const portalColor = (0, $d016ad53434219a1$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
5391
- const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
5392
- portalGrad.addColorStop(0, portalColor);
5393
- portalGrad.addColorStop(1, bgEnd);
5394
- ctx.globalAlpha = portalAlpha;
5395
- ctx.fillStyle = portalGrad;
5396
- ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
5397
- // Optional: subtle inner texture — a few tiny dots inside the portal
5398
- if (rng() < 0.5) {
5399
- const dotCount = 3 + Math.floor(rng() * 5);
5400
- ctx.globalAlpha = portalAlpha * 0.3;
5401
- ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
5402
- for(let d = 0; d < dotCount; d++){
5403
- const dx = (rng() - 0.5) * portalSize * 1.4;
5404
- const dy = (rng() - 0.5) * portalSize * 1.4;
5405
- const dr = (1 + rng() * 3) * scaleFactor;
5406
- ctx.beginPath();
5407
- ctx.arc(dx, dy, dr, 0, Math.PI * 2);
5408
- ctx.fill();
5409
- }
5410
- }
5411
- ctx.restore();
5412
- // Step 2: Draw a border ring around the portal (outside the clip)
5413
- ctx.save();
5414
- ctx.translate(portalX, portalY);
5415
- ctx.rotate(portalRotation * Math.PI / 180);
5416
- ctx.globalAlpha = 0.15 + rng() * 0.2;
5417
- ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
5418
- ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
5419
- ctx.beginPath();
5420
- (0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
5421
- ctx.stroke();
5422
- ctx.restore();
5423
- }
5457
+ if (_dt) {
5458
+ _dt.shapeCount = shapePositions.length;
5459
+ _dt.extraCount = extrasSpent;
5424
5460
  }
5461
+ _mark("5_shape_layers");
5462
+ // ── 5g. (Portal/cutout feature removed — replaced by custom shapes API) ──
5463
+ _mark("5g_portals");
5425
5464
  // ── 6. Flow-line pass — variable color, branching, pressure ────
5426
5465
  // Optimized: collect all segments into width-quantized buckets, then
5427
5466
  // render each bucket as a single batched path. This reduces
@@ -5547,6 +5586,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5547
5586
  ctx.stroke();
5548
5587
  }
5549
5588
  }
5589
+ _mark("6_flow_lines");
5550
5590
  // ── 6b. Motion/energy lines — short directional bursts ─────────
5551
5591
  // Optimized: collect all burst segments, then batch by quantized alpha
5552
5592
  const energyArchetypes = [
@@ -5609,6 +5649,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5609
5649
  ctx.stroke();
5610
5650
  }
5611
5651
  }
5652
+ _mark("6b_energy_lines");
5612
5653
  // ── 6c. Apply symmetry mirroring ─────────────────────────────────
5613
5654
  if (symmetryMode !== "none") {
5614
5655
  const canvas = ctx.canvas;
@@ -5629,60 +5670,25 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5629
5670
  }
5630
5671
  ctx.restore();
5631
5672
  }
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.
5673
+ _mark("6c_symmetry");
5674
+ // ── 7. Noise texture overlay ─────────────────────────────────────
5675
+ // With density capped at 2500 dots, direct fillRect calls are far cheaper
5676
+ // than the getImageData/putImageData round-trip which copies the entire
5677
+ // pixel buffer (4 × width × height bytes) twice.
5635
5678
  const noiseRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 777));
5636
5679
  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
5680
  const noiseDensity = Math.min(rawNoiseDensity, 2500);
5640
- try {
5641
- const imageData = ctx.getImageData(0, 0, width, height);
5642
- const data = imageData.data;
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)
5646
- for(let i = 0; i < noiseDensity; i++){
5647
- const nx = Math.floor(noiseRng() * width);
5648
- const ny = Math.floor(noiseRng() * height);
5649
- const brightness = noiseRng() > 0.5 ? 255 : 0;
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;
5666
- for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
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;
5671
- }
5672
- }
5673
- ctx.putImageData(imageData, 0, 0);
5674
- } catch {
5675
- // Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
5676
- for(let i = 0; i < noiseDensity; i++){
5677
- const nx = noiseRng() * width;
5678
- const ny = noiseRng() * height;
5679
- const brightness = noiseRng() > 0.5 ? 255 : 0;
5680
- const alpha = 0.01 + noiseRng() * 0.03;
5681
- ctx.globalAlpha = alpha;
5682
- ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
5683
- ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
5684
- }
5681
+ const pixelScale = Math.max(1, Math.round(scaleFactor));
5682
+ for(let i = 0; i < noiseDensity; i++){
5683
+ const nx = noiseRng() * width;
5684
+ const ny = noiseRng() * height;
5685
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
5686
+ const alpha = 0.01 + noiseRng() * 0.03;
5687
+ ctx.globalAlpha = alpha;
5688
+ ctx.fillStyle = `rgb(${brightness},${brightness},${brightness})`;
5689
+ ctx.fillRect(nx, ny, pixelScale, pixelScale);
5685
5690
  }
5691
+ _mark("7_noise_texture");
5686
5692
  // ── 8. Vignette — darken edges to draw the eye inward ───────────
5687
5693
  ctx.globalAlpha = 1;
5688
5694
  const vignetteStrength = 0.25 + rng() * 0.2;
@@ -5696,6 +5702,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5696
5702
  vigGrad.addColorStop(1, vignetteColor);
5697
5703
  ctx.fillStyle = vigGrad;
5698
5704
  ctx.fillRect(0, 0, width, height);
5705
+ _mark("8_vignette");
5699
5706
  // ── 9. Organic connecting curves — proximity-aware ───────────────
5700
5707
  // Optimized: batch all curves into alpha-quantized groups to reduce
5701
5708
  // beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
@@ -5754,6 +5761,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5754
5761
  ctx.stroke();
5755
5762
  }
5756
5763
  }
5764
+ _mark("9_connecting_curves");
5757
5765
  // ── 10. Post-processing ────────────────────────────────────────
5758
5766
  // 10a. Color grading — unified tone across the whole image
5759
5767
  // Apply as a semi-transparent overlay in the grade hue
@@ -5813,6 +5821,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5813
5821
  ctx.fillRect(0, 0, width, height);
5814
5822
  ctx.globalCompositeOperation = "source-over";
5815
5823
  }
5824
+ _mark("10_post_processing");
5816
5825
  // ── 10e. Generative borders — archetype-driven decorative frames ──
5817
5826
  {
5818
5827
  ctx.save();
@@ -5979,6 +5988,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
5979
5988
  // Other archetypes: no border (intentional — not every image needs one)
5980
5989
  ctx.restore();
5981
5990
  }
5991
+ _mark("10e_borders");
5982
5992
  // ── 11. Signature mark — placed in the least-dense corner ──────
5983
5993
  {
5984
5994
  const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));
@@ -6046,6 +6056,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
6046
6056
  ctx.restore();
6047
6057
  }
6048
6058
  ctx.globalAlpha = 1;
6059
+ _mark("11_signature");
6060
+ // Clean up custom shape profiles to avoid leaking into subsequent renders
6061
+ for (const name of customShapeNames)delete (0, $e73976f898150d4d$export$4343b39fe47bd82c)[name];
6049
6062
  }
6050
6063
 
6051
6064