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 +16 -0
- package/dist/browser.js +167 -154
- package/dist/browser.js.map +1 -1
- package/dist/main.js +167 -154
- package/dist/main.js.map +1 -1
- package/dist/module.js +167 -154
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/custom-shapes.test.ts +182 -0
- package/src/__tests__/phase-breakdown.test.ts +44 -0
- package/src/browser.ts +1 -1
- package/src/index.ts +1 -1
- package/src/lib/canvas/draw.ts +8 -4
- package/src/lib/canvas/shapes/affinity.ts +33 -0
- package/src/lib/render.ts +133 -155
- package/src/types.ts +52 -0
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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")
|
|
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.
|
|
4739
|
+
ctx.moveTo(cx + r, cy);
|
|
4664
4740
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
4665
|
-
ctx.stroke();
|
|
4666
4741
|
}
|
|
4667
|
-
ctx.
|
|
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 —
|
|
4677
|
-
const dotSpacing = Math.max(
|
|
4678
|
-
const
|
|
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
|
-
|
|
4682
|
-
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4683
|
-
ctx.
|
|
4684
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
4702
|
-
const tessSize = Math.max(
|
|
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
|
-
|
|
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
|
-
|
|
4714
|
-
|
|
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
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
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
|
-
|
|
5608
|
-
//
|
|
5609
|
-
//
|
|
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
|
-
|
|
5616
|
-
|
|
5617
|
-
const
|
|
5618
|
-
const
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
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
|
|