git-hash-art 0.13.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 +8 -0
- package/dist/browser.js +83 -70
- package/dist/browser.js.map +1 -1
- package/dist/main.js +83 -70
- package/dist/main.js.map +1 -1
- package/dist/module.js +83 -70
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/custom-shapes.test.ts +182 -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 +38 -67
- package/src/types.ts +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,18 @@ 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
|
+
|
|
7
12
|
#### [0.13.0](https://github.com/gfargo/git-hash-art/compare/0.12.0...0.13.0)
|
|
8
13
|
|
|
14
|
+
> 20 March 2026
|
|
15
|
+
|
|
9
16
|
- perf: 33-111× pipeline speedup via phase-level profiling [`#23`](https://github.com/gfargo/git-hash-art/pull/23)
|
|
10
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)
|
|
11
19
|
|
|
12
20
|
#### [0.12.0](https://github.com/gfargo/git-hash-art/compare/0.11.0...0.12.0)
|
|
13
21
|
|
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,
|
|
@@ -4611,7 +4640,39 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4611
4640
|
// ── 0b. Color hierarchy — dominant/secondary/accent weighting ──
|
|
4612
4641
|
const colorHierarchy = (0, $b5a262d09b87e373$export$fabac4600b87056)(colors, rng);
|
|
4613
4642
|
// ── 0c. Shape palette — curated shapes that work well together ──
|
|
4614
|
-
|
|
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));
|
|
4615
4676
|
const shapePalette = (0, $8286059160ee2e04$export$4a95df8944b5033b)(rng, shapeNames, archetype.name);
|
|
4616
4677
|
// ── 0d. Color grading — unified tone for the whole image ───────
|
|
4617
4678
|
const colorGrade = (0, $b5a262d09b87e373$export$6d1620b367f86f7a)(rng);
|
|
@@ -4912,7 +4973,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4912
4973
|
renderStyle: heroStyle,
|
|
4913
4974
|
rng: rng,
|
|
4914
4975
|
lightAngle: lightAngle,
|
|
4915
|
-
scaleFactor: scaleFactor
|
|
4976
|
+
scaleFactor: scaleFactor,
|
|
4977
|
+
activeShapes: activeShapes
|
|
4916
4978
|
});
|
|
4917
4979
|
heroCenter = {
|
|
4918
4980
|
x: heroFocal.x,
|
|
@@ -5115,7 +5177,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5115
5177
|
renderStyle: finalRenderStyle,
|
|
5116
5178
|
rng: rng,
|
|
5117
5179
|
lightAngle: lightAngle,
|
|
5118
|
-
scaleFactor: scaleFactor
|
|
5180
|
+
scaleFactor: scaleFactor,
|
|
5181
|
+
activeShapes: activeShapes
|
|
5119
5182
|
};
|
|
5120
5183
|
if (shouldMirror) {
|
|
5121
5184
|
(0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
@@ -5146,7 +5209,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5146
5209
|
rotation: rotation,
|
|
5147
5210
|
proportionType: "GOLDEN_RATIO",
|
|
5148
5211
|
renderStyle: "fill-only",
|
|
5149
|
-
rng: rng
|
|
5212
|
+
rng: rng,
|
|
5213
|
+
activeShapes: activeShapes
|
|
5150
5214
|
});
|
|
5151
5215
|
}
|
|
5152
5216
|
extrasSpent += glazePasses;
|
|
@@ -5186,7 +5250,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5186
5250
|
rotation: rotation + (e + 1) * 15,
|
|
5187
5251
|
proportionType: "GOLDEN_RATIO",
|
|
5188
5252
|
renderStyle: finalRenderStyle,
|
|
5189
|
-
rng: rng
|
|
5253
|
+
rng: rng,
|
|
5254
|
+
activeShapes: activeShapes
|
|
5190
5255
|
});
|
|
5191
5256
|
shapePositions.push({
|
|
5192
5257
|
x: echoX,
|
|
@@ -5233,7 +5298,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5233
5298
|
rotation: innerRot,
|
|
5234
5299
|
proportionType: "GOLDEN_RATIO",
|
|
5235
5300
|
renderStyle: innerStyle,
|
|
5236
|
-
rng: rng
|
|
5301
|
+
rng: rng,
|
|
5302
|
+
activeShapes: activeShapes
|
|
5237
5303
|
});
|
|
5238
5304
|
extrasSpent += $1f63dc64b5593c73$var$RENDER_STYLE_COST[innerStyle] ?? 1;
|
|
5239
5305
|
}
|
|
@@ -5280,7 +5346,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5280
5346
|
rotation: member.rotation + groupRotation * 180 / Math.PI,
|
|
5281
5347
|
proportionType: "GOLDEN_RATIO",
|
|
5282
5348
|
renderStyle: memberStyle,
|
|
5283
|
-
rng: rng
|
|
5349
|
+
rng: rng,
|
|
5350
|
+
activeShapes: activeShapes
|
|
5284
5351
|
});
|
|
5285
5352
|
shapePositions.push({
|
|
5286
5353
|
x: mx,
|
|
@@ -5334,7 +5401,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5334
5401
|
rotation: rotation + (r + 1) * 12,
|
|
5335
5402
|
proportionType: "GOLDEN_RATIO",
|
|
5336
5403
|
renderStyle: finalRenderStyle,
|
|
5337
|
-
rng: rng
|
|
5404
|
+
rng: rng,
|
|
5405
|
+
activeShapes: activeShapes
|
|
5338
5406
|
});
|
|
5339
5407
|
shapePositions.push({
|
|
5340
5408
|
x: rx,
|
|
@@ -5366,64 +5434,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5366
5434
|
_dt.extraCount = extrasSpent;
|
|
5367
5435
|
}
|
|
5368
5436
|
_mark("5_shape_layers");
|
|
5369
|
-
// ── 5g.
|
|
5370
|
-
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
5371
|
-
// with a tinted background wash, creating a "peek through" effect.
|
|
5372
|
-
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
5373
|
-
const portalCount = 1 + Math.floor(rng() * 2);
|
|
5374
|
-
for(let p = 0; p < portalCount; p++){
|
|
5375
|
-
// Pick a position biased toward placed shapes
|
|
5376
|
-
const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5377
|
-
const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
5378
|
-
const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
5379
|
-
const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
|
|
5380
|
-
// Pick a portal shape from the palette
|
|
5381
|
-
const portalShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
|
|
5382
|
-
const portalRotation = rng() * 360;
|
|
5383
|
-
const portalAlpha = 0.6 + rng() * 0.35;
|
|
5384
|
-
ctx.save();
|
|
5385
|
-
ctx.translate(portalX, portalY);
|
|
5386
|
-
ctx.rotate(portalRotation * Math.PI / 180);
|
|
5387
|
-
// Step 1: Clip to the portal shape and fill with background wash
|
|
5388
|
-
ctx.beginPath();
|
|
5389
|
-
(0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
|
|
5390
|
-
ctx.clip();
|
|
5391
|
-
// Fill the clipped region with a radial gradient from background colors
|
|
5392
|
-
const portalColor = (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
|
|
5393
|
-
const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
|
|
5394
|
-
portalGrad.addColorStop(0, portalColor);
|
|
5395
|
-
portalGrad.addColorStop(1, bgEnd);
|
|
5396
|
-
ctx.globalAlpha = portalAlpha;
|
|
5397
|
-
ctx.fillStyle = portalGrad;
|
|
5398
|
-
ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
|
|
5399
|
-
// Optional: subtle inner texture — a few tiny dots inside the portal
|
|
5400
|
-
if (rng() < 0.5) {
|
|
5401
|
-
const dotCount = 3 + Math.floor(rng() * 5);
|
|
5402
|
-
ctx.globalAlpha = portalAlpha * 0.3;
|
|
5403
|
-
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
|
|
5404
|
-
for(let d = 0; d < dotCount; d++){
|
|
5405
|
-
const dx = (rng() - 0.5) * portalSize * 1.4;
|
|
5406
|
-
const dy = (rng() - 0.5) * portalSize * 1.4;
|
|
5407
|
-
const dr = (1 + rng() * 3) * scaleFactor;
|
|
5408
|
-
ctx.beginPath();
|
|
5409
|
-
ctx.arc(dx, dy, dr, 0, Math.PI * 2);
|
|
5410
|
-
ctx.fill();
|
|
5411
|
-
}
|
|
5412
|
-
}
|
|
5413
|
-
ctx.restore();
|
|
5414
|
-
// Step 2: Draw a border ring around the portal (outside the clip)
|
|
5415
|
-
ctx.save();
|
|
5416
|
-
ctx.translate(portalX, portalY);
|
|
5417
|
-
ctx.rotate(portalRotation * Math.PI / 180);
|
|
5418
|
-
ctx.globalAlpha = 0.15 + rng() * 0.2;
|
|
5419
|
-
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
|
|
5420
|
-
ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
|
|
5421
|
-
ctx.beginPath();
|
|
5422
|
-
(0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
|
|
5423
|
-
ctx.stroke();
|
|
5424
|
-
ctx.restore();
|
|
5425
|
-
}
|
|
5426
|
-
}
|
|
5437
|
+
// ── 5g. (Portal/cutout feature removed — replaced by custom shapes API) ──
|
|
5427
5438
|
_mark("5g_portals");
|
|
5428
5439
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
5429
5440
|
// Optimized: collect all segments into width-quantized buckets, then
|
|
@@ -6021,6 +6032,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
6021
6032
|
}
|
|
6022
6033
|
ctx.globalAlpha = 1;
|
|
6023
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
|
|