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 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 drawFunction = (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[shape];
2082
+ const registry = activeShapes ?? (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5);
2083
+ const drawFunction = registry[shape];
2083
2084
  if (drawFunction) {
2084
2085
  drawFunction(ctx, size);
2085
2086
  ctx.fill();
@@ -2589,7 +2590,8 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
2589
2590
  } else ctx.fillStyle = fillColor;
2590
2591
  ctx.strokeStyle = strokeColor;
2591
2592
  ctx.lineWidth = strokeWidth;
2592
- const drawFunction = (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[shape];
2593
+ const registry = config.activeShapes ?? (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5);
2594
+ const drawFunction = registry[shape];
2593
2595
  if (drawFunction) {
2594
2596
  drawFunction(ctx, size, {
2595
2597
  rng: rng
@@ -3453,6 +3455,26 @@ const $8286059160ee2e04$export$4343b39fe47bd82c = {
3453
3455
  ]
3454
3456
  }
3455
3457
  };
3458
+ function $8286059160ee2e04$export$90912290d628650f(name, partial) {
3459
+ $8286059160ee2e04$export$4343b39fe47bd82c[name] = {
3460
+ tier: partial?.tier ?? 2,
3461
+ minSizeFraction: partial?.minSizeFraction ?? 0.05,
3462
+ maxSizeFraction: partial?.maxSizeFraction ?? 1.0,
3463
+ affinities: partial?.affinities ?? [
3464
+ "circle",
3465
+ "square"
3466
+ ],
3467
+ category: "procedural",
3468
+ heroCandidate: partial?.heroCandidate ?? false,
3469
+ bestStyles: partial?.bestStyles ?? [
3470
+ "fill-and-stroke",
3471
+ "watercolor"
3472
+ ]
3473
+ };
3474
+ }
3475
+ function $8286059160ee2e04$export$f4ca68bd046f15ae(name) {
3476
+ delete $8286059160ee2e04$export$4343b39fe47bd82c[name];
3477
+ }
3456
3478
  function $8286059160ee2e04$export$4a95df8944b5033b(rng, shapeNames, archetypeName) {
3457
3479
  const available = shapeNames.filter((s)=>$8286059160ee2e04$export$4343b39fe47bd82c[s]);
3458
3480
  // Pick a seed shape — tier 1 shapes that are hero candidates
@@ -3642,7 +3664,14 @@ function $8286059160ee2e04$export$ab873bb6fb56c1a8(shapeName, layerStyle, rng) {
3642
3664
 
3643
3665
 
3644
3666
  /**
3645
- * Configuration options for image generation.
3667
+ * Draw function signature for custom shapes.
3668
+ * The function should build a canvas path (moveTo/lineTo/arc/etc.)
3669
+ * centered at the origin. The pipeline handles translate, rotate,
3670
+ * fill, and stroke — your function just defines the geometry.
3671
+ *
3672
+ * @param ctx - Canvas 2D rendering context (already translated to shape center)
3673
+ * @param size - Bounding size in pixels
3674
+ * @param rng - Deterministic RNG seeded from the git hash — use this instead of Math.random()
3646
3675
  */ const $81c1b644006d48ec$export$c2f8e0cc249a8d8f = {
3647
3676
  width: 2048,
3648
3677
  height: 2048,
@@ -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
- const shapeNames = Object.keys((0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5));
4643
+ // Merge custom shapes into a combined registry
4644
+ const customShapeNames = [];
4645
+ let activeShapes;
4646
+ if (finalConfig.customShapes && Object.keys(finalConfig.customShapes).length > 0) {
4647
+ activeShapes = {
4648
+ ...(0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)
4649
+ };
4650
+ for (const [name, def] of Object.entries(finalConfig.customShapes)){
4651
+ // Wrap CustomDrawFunction (ctx, size, rng) into DrawFunction (ctx, size, config?)
4652
+ const customDraw = def.draw;
4653
+ activeShapes[name] = (ctx, size, config)=>{
4654
+ customDraw(ctx, size, config?.rng ?? Math.random);
4655
+ };
4656
+ // Register profile for affinity system (inlined to avoid ESM interop issues)
4657
+ (0, $8286059160ee2e04$export$4343b39fe47bd82c)[name] = {
4658
+ tier: def.profile?.tier ?? 2,
4659
+ minSizeFraction: def.profile?.minSizeFraction ?? 0.05,
4660
+ maxSizeFraction: def.profile?.maxSizeFraction ?? 1.0,
4661
+ affinities: def.profile?.affinities ?? [
4662
+ "circle",
4663
+ "square"
4664
+ ],
4665
+ category: "procedural",
4666
+ heroCandidate: def.profile?.heroCandidate ?? false,
4667
+ bestStyles: def.profile?.bestStyles ?? [
4668
+ "fill-and-stroke",
4669
+ "watercolor"
4670
+ ]
4671
+ };
4672
+ customShapeNames.push(name);
4673
+ }
4674
+ }
4675
+ const shapeNames = Object.keys(activeShapes ?? (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5));
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. Layered masking / cutout portals ───────────────────────
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