git-hash-art 0.5.0 → 0.7.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/ALGORITHM.md CHANGED
@@ -9,28 +9,33 @@ Hash String
9
9
 
10
10
  ├─► Seed (mulberry32 PRNG)
11
11
 
12
- ├─► Color Scheme (hash-driven variation + scheme type + temperature mode)
12
+ ├─► Archetype Selection (1 of 10 visual personalities)
13
13
 
14
- └─► Rendering Pipeline
14
+ ├─► Color Scheme (palette mode from archetype + temperature mode + contrast enforcement)
15
+
16
+ └─► Rendering Pipeline (parameters overridden by archetype)
15
17
 
16
- 1. Background Layer (radial gradient, temperature-shifted)
18
+ 0. Archetype Override (gridSize, layers, opacity, sizes, styles)
19
+ 1. Background Layer (7 styles: radial, linear, solid, multi-stop)
20
+ 1a. Background Luminance → contrast enforcement threshold
17
21
  1b. Layered Background (faint shapes + concentric rings)
18
22
  2. Composition Mode Selection
19
23
  2b. Symmetry Mode Selection (none / bilateral / quad)
20
24
  3. Focal Points (rule-of-thirds biased) + Void Zones
21
25
  4. Flow Field Initialization
22
- 4b. Hero Shape (large focal anchor, ~60% of images)
23
- 5. Shape Layers (× N layers)
26
+ 4b. Hero Shape (large focal anchor, archetype-controlled)
27
+ 5. Shape Layers (× N layers, archetype-tuned)
24
28
  │ ├─ Blend Mode (per-layer compositing)
25
- │ ├─ Render Style (fill+stroke, wireframe, dashed, watercolor, hatched, incomplete, etc.)
29
+ │ ├─ Render Style (archetype-preferred + random mix)
26
30
  │ ├─ Position (composition mode + focal bias + density check)
27
- │ ├─ Shape Selection (layer-weighted)
31
+ │ ├─ Shape Selection (4 categories: basic, complex, sacred, procedural)
32
+ │ ├─ Contrast Enforcement (ensure readability vs background)
28
33
  │ ├─ Atmospheric Depth (desaturation on later layers)
29
34
  │ ├─ Temperature Contrast (foreground opposite to background)
30
35
  │ ├─ Styling (transparency, glow, gradients, color jitter)
31
36
  │ ├─ Organic Edges (~15% watercolor bleed)
32
37
  │ └─ Recursive Nesting (~15% of large shapes)
33
- 6. Flow-Line Pass (tapered brush strokes)
38
+ 6. Flow-Line Pass (tapered brush strokes, archetype-scaled)
34
39
  6b. Symmetry Mirroring (bilateral-x, bilateral-y, or quad)
35
40
  7. Noise Texture Overlay
36
41
  8. Vignette (radial edge darkening)
@@ -48,7 +53,44 @@ rng() → float in [0, 1)
48
53
 
49
54
  The old approach extracted 2-char hex pairs from the hash (only ~20 unique values in a 40-char hash). Mulberry32 produces a full 32-bit uniform stream from any seed, eliminating correlation artifacts.
50
55
 
51
- ## 2. Color Scheme
56
+ ## 2. Archetype System
57
+
58
+ Before any rendering begins, the hash deterministically selects one of 10 **visual archetypes** — fundamentally different rendering personalities that override key parameters. This is the primary mechanism for visual diversity: two hashes that select different archetypes will look like they came from entirely different generators.
59
+
60
+ Each archetype controls:
61
+
62
+ | Parameter | Effect |
63
+ |-----------|--------|
64
+ | `gridSize` | Shape density (2 = sparse, 9 = packed) |
65
+ | `layers` | Rendering depth (2 = flat, 5 = deep) |
66
+ | `baseOpacity` / `opacityReduction` | Transparency character |
67
+ | `minShapeSize` / `maxShapeSize` | Scale range |
68
+ | `backgroundStyle` | One of 7 background rendering modes |
69
+ | `paletteMode` | One of 7 color palette strategies |
70
+ | `preferredStyles` | Weighted render style selection |
71
+ | `flowLineMultiplier` | Flow line density (0 = none, 4 = heavy) |
72
+ | `heroShape` | Whether to draw a dominant focal shape |
73
+ | `glowMultiplier` | Glow probability scaling (0 = none, 3 = heavy) |
74
+ | `sizePower` | Size distribution curve (0.5 = uniform, 2.5 = many tiny) |
75
+
76
+ ### The 10 Archetypes
77
+
78
+ | Archetype | Character | Background | Palette | Key Traits |
79
+ |-----------|-----------|------------|---------|------------|
80
+ | **dense-chaotic** | Packed, energetic | radial-dark | harmonious | 9×9 grid, 5 layers, heavy flow lines, low glow |
81
+ | **minimal-spacious** | Clean, deliberate | solid-light | duotone | 2×2 grid, 2 layers, large shapes, no glow |
82
+ | **organic-flow** | Natural, flowing | radial-dark | earth | Heavy flow lines (4×), watercolor style, no hero |
83
+ | **geometric-precision** | Technical, structured | solid-dark | high-contrast | Stroke-only/dashed/hatched styles, no flow lines |
84
+ | **ethereal** | Dreamy, luminous | radial-light | pastel-light | High glow (2×), watercolor, hero shape |
85
+ | **bold-graphic** | Poster-like, impactful | linear-diagonal | duotone | 2 layers, very large shapes, no flow lines |
86
+ | **neon-glow** | Electric, vibrant | solid-dark | neon | Heavy glow (3×), stroke-heavy styles, hero shape |
87
+ | **monochrome-ink** | Pen-and-ink, textural | solid-light | monochrome | Hatched/incomplete styles, no glow |
88
+ | **cosmic** | Deep space, vast | radial-dark | neon | 8×8 grid, 5 layers, heavy glow, many tiny shapes |
89
+ | **classic** | Balanced, familiar | radial-dark | harmonious | Preserves the original rendering look |
90
+
91
+ Archetype values serve as defaults — explicit user config always wins. The `classic` archetype preserves backward compatibility with the original rendering style.
92
+
93
+ ## 3. Color Scheme
52
94
 
53
95
  The `SacredColorScheme` class derives three harmonious palettes from the hash:
54
96
 
@@ -60,6 +102,22 @@ The `SacredColorScheme` class derives three harmonious palettes from the hash:
60
102
 
61
103
  These are merged and deduplicated into a single 6-8 color palette. Background colors are darkened variants (65% and 55% brightness) of the base scheme, with optional temperature shifting.
62
104
 
105
+ ### Palette Modes
106
+
107
+ The archetype's `paletteMode` reshapes the color palette to match the visual personality:
108
+
109
+ | Mode | Colors | Character |
110
+ |------|--------|-----------|
111
+ | **harmonious** | Full base + complementary + triadic | Rich, balanced (default) |
112
+ | **monochrome** | Single hue, 5 lightness steps | Elegant, focused |
113
+ | **duotone** | Two contrasting hues + tints | Bold, graphic |
114
+ | **neon** | 4 hues at full saturation | Electric, vivid |
115
+ | **pastel-light** | 4 hues at low saturation, high lightness | Soft, dreamy |
116
+ | **earth** | Warm muted naturals (browns, olives, sage) | Organic, grounded |
117
+ | **high-contrast** | Black + white + one accent | Technical, stark |
118
+
119
+ Each mode also provides matching background colors (e.g., neon gets near-black backgrounds, pastel-light gets warm off-whites).
120
+
63
121
  ### Temperature Contrast
64
122
 
65
123
  The hash deterministically selects a **temperature mode** that creates warm/cool tension across the image:
@@ -95,9 +153,21 @@ Scheme types also vary: `analogic`, `mono`, `contrast`, `triade`, `tetrade`. The
95
153
  - **`shiftTemperature(hex, target, amount)`** — shifts hue toward warm (orange) or cool (blue)
96
154
  - **Positional blending** — shape fill color is biased by canvas position, creating smooth color flow across the image
97
155
 
98
- ## 3. Background
156
+ ## 4. Background
157
+
158
+ The archetype's `backgroundStyle` selects one of 7 rendering modes:
159
+
160
+ | Style | Description |
161
+ |-------|-------------|
162
+ | **radial-dark** | Radial gradient from dark center to darker edges (original default) |
163
+ | **radial-light** | Light off-white center fading to the base palette |
164
+ | **linear-horizontal** | Left-to-right gradient between two palette colors |
165
+ | **linear-diagonal** | Corner-to-corner gradient with color reversal |
166
+ | **solid-dark** | Flat dark color fill |
167
+ | **solid-light** | Flat warm off-white fill |
168
+ | **multi-stop** | 3-4 color gradient with a darkened mid-palette accent |
99
169
 
100
- A radial gradient fills the canvas from center to corners using two darkened base-scheme colors. This creates depth before any shapes are drawn.
170
+ This single change has an outsized impact on visual diversity a solid-light background with monochrome shapes looks nothing like a radial-dark background with neon glow.
101
171
 
102
172
  ### Layered Background
103
173
 
@@ -108,7 +178,7 @@ After the gradient, a second pass adds visual texture to the background:
108
178
 
109
179
  This prevents the background from feeling flat and gives the image depth before the main shape layers begin.
110
180
 
111
- ## 4. Composition Modes
181
+ ## 5. Composition Modes
112
182
 
113
183
  The hash deterministically selects one of five composition strategies that control how shapes are positioned on the canvas:
114
184
 
@@ -135,7 +205,7 @@ Each mode produces fundamentally different visual character from the same shape
135
205
 
136
206
  Symmetry is applied after shape layers and flow lines but before post-processing (noise, vignette, connecting curves). This means the mirrored content gets the same noise texture and vignette as the original, maintaining visual consistency. Symmetrical generative art is disproportionately appealing to humans due to our innate preference for bilateral symmetry.
137
207
 
138
- ## 5. Focal Points & Negative Space
208
+ ## 6. Focal Points & Negative Space
139
209
 
140
210
  1-2 focal points are placed on the canvas. **70% of the time**, focal points snap to **rule-of-thirds intersection points** (with slight jitter to avoid a mechanical look), creating compositions that feel intentionally designed. The remaining 30% use free placement within the central 60% of the canvas. Every shape position is pulled toward the nearest focal point by a strength factor (30-70%).
141
211
 
@@ -156,7 +226,7 @@ Symmetry is applied after shape layers and flow lines but before post-processing
156
226
 
157
227
  Before placing each shape, the renderer checks how many shapes already exist nearby. If local density exceeds ~15% of the per-layer shape count, there's a 60% chance the shape is skipped. This prevents areas from becoming an opaque blob and creates natural visual rhythm.
158
228
 
159
- ## 6. Shape Layers
229
+ ## 7. Shape Layers
160
230
 
161
231
  The image is built in N layers (default: 4). Each layer has its own characteristics:
162
232
 
@@ -169,7 +239,7 @@ The image is built in N layers (default: 4). Each layer has its own characterist
169
239
  | Shape weights | Early layers favor basic shapes; later layers favor complex/sacred |
170
240
  | Per-shape opacity | Additional random jitter (50-100% of layer opacity) |
171
241
  | Blend mode | Each layer gets a hash-derived `globalCompositeOperation` (see below) |
172
- | Render style | Each layer has a dominant render style; 30% of shapes pick their own |
242
+ | Render style | Each layer has a dominant render style (60% from archetype preferences, 40% random); 30% of shapes pick their own |
173
243
  | Atmospheric depth | Later layers desaturate colors by up to 30%, simulating distance |
174
244
 
175
245
  ### Blend Modes (Per-Layer Compositing)
@@ -199,13 +269,25 @@ Later layers progressively desaturate their colors (0% on layer 0, up to 30% on
199
269
 
200
270
  ### Shape Selection (Layer-Weighted)
201
271
 
202
- Shapes are divided into three categories with weights that shift across layers:
272
+ Shapes are divided into four categories with weights that shift across layers:
203
273
 
204
274
  | Category | Shapes | Early layers | Late layers |
205
275
  |----------|--------|-------------|-------------|
206
276
  | **Basic** | circle, square, triangle, hexagon, diamond, cube | High weight | Low weight |
207
277
  | **Complex** | star, platonic solid, fibonacci spiral, islamic pattern, celtic knot, merkaba, fractal | Medium | Medium-high |
208
278
  | **Sacred** | mandala, flower of life, tree of life, Metatron's cube, Sri Yantra, seed of life, vesica piscis, torus, egg of life | Low | High |
279
+ | **Procedural** | blob, ngon, lissajous, superellipse, spirograph, waveRing, rose | Medium (always present) | Medium-high |
280
+
281
+ Procedural shapes are hash-derived — their geometry is generated from the RNG, so every hash produces unique shapes that don't exist in any other generation. See the Procedural Shapes section below for details.
282
+
283
+ ### Contrast Enforcement
284
+
285
+ After color selection, every foreground color (fills, strokes, flow lines, connecting curves) is checked against the average background luminance. If the luminance difference is below the minimum threshold (0.15), the color is adjusted:
286
+
287
+ - **Light backgrounds** — foreground colors are darkened and saturated
288
+ - **Dark backgrounds** — foreground colors are lightened and saturated
289
+
290
+ This prevents the white-on-white and dark-on-dark readability problems that occur when light palette modes (pastel-light, high-contrast) combine with light background styles (solid-light, radial-light).
209
291
 
210
292
  ### Size Distribution
211
293
 
@@ -230,7 +312,7 @@ Each shape receives:
230
312
  - Sized at 15-40% of the parent
231
313
  - More transparent than the parent layer
232
314
 
233
- ## 7. Flow-Line Pass (Tapered Brush Strokes)
315
+ ## 8. Flow-Line Pass (Tapered Brush Strokes)
234
316
 
235
317
  6-16 flowing curves are drawn across the canvas, following the hash-derived vector field:
236
318
 
@@ -246,7 +328,7 @@ The flow field is defined by:
246
328
  angle(x, y) = baseAngle + sin(x/w × freq × 2π) × π/2 + cos(y/h × freq × 2π) × π/2
247
329
  ```
248
330
 
249
- ## 8. Noise Texture Overlay
331
+ ## 9. Noise Texture Overlay
250
332
 
251
333
  A dedicated noise RNG (seeded separately from the main RNG to avoid affecting shape generation) renders thousands of 1px dots across the canvas:
252
334
 
@@ -255,7 +337,7 @@ A dedicated noise RNG (seeded separately from the main RNG to avoid affecting sh
255
337
  - Very low opacity (1-4%)
256
338
  - Creates subtle film-grain texture that adds organic depth
257
339
 
258
- ## 8b. Vignette
340
+ ## 9b. Vignette
259
341
 
260
342
  A radial gradient overlay darkens the edges of the canvas, drawing the viewer's eye toward the center:
261
343
 
@@ -264,7 +346,7 @@ A radial gradient overlay darkens the edges of the canvas, drawing the viewer's
264
346
  - Applied after noise but before connecting curves, so the curves remain visible at edges
265
347
  - Creates a natural "spotlight" effect that makes compositions feel more focused and photographic
266
348
 
267
- ## 9. Organic Connecting Curves
349
+ ## 10. Organic Connecting Curves
268
350
 
269
351
  Quadratic bezier curves connect nearby shapes:
270
352
 
@@ -298,6 +380,19 @@ Mathematically precise sacred geometry patterns:
298
380
  - **Torus** — 2D projection of a torus via line segments
299
381
  - **Egg of Life** — 7 circles in tight hexagonal packing
300
382
 
383
+ ### Procedural Shapes
384
+ Hash-derived shapes whose geometry is generated from the RNG. Every hash produces unique shapes that don't exist in any other generation:
385
+
386
+ | Shape | Algorithm | Hash Controls |
387
+ |-------|-----------|---------------|
388
+ | **Blob** | Smooth closed curve via quadratic bezier through 5-9 control points arranged around a circle | Number of lobes (5-9), radius jitter per lobe (50-100%) |
389
+ | **Ngon** | Irregular polygon with independent vertex displacement | Side count (3-12), vertex jitter amount (10-50%) |
390
+ | **Lissajous** | Parametric curve `x = sin(a*t + φ), y = sin(b*t)` | Frequency ratios a,b (1-5 each), phase offset φ |
391
+ | **Superellipse** | `|x|^n + |y|^n = 1` rendered parametrically | Exponent n: 0.3 (spiky astroid) → 2 (circle) → 5 (rounded rectangle) |
392
+ | **Spirograph** | Hypotrochoid curve `(R-r)cos(t) + d*cos((R-r)t/r)` | Inner radius ratio r (0.2-0.8), pen distance d (0.3-1.0) |
393
+ | **Wave Ring** | Concentric rings with sinusoidal radial displacement | Ring count (2-5), wave frequency (3-14), amplitude (5-20%) |
394
+ | **Rose** | Polar rose curve `r = cos(k*θ)` | Petal parameter k (2-7), producing k or 2k petals |
395
+
301
396
  ## Configuration
302
397
 
303
398
  All parameters are exposed via `GenerationConfig`:
package/CHANGELOG.md CHANGED
@@ -4,8 +4,25 @@ 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.7.0](https://github.com/gfargo/git-hash-art/compare/0.6.0...0.7.0)
8
+
9
+ - feat: procedural shape generators and contrast enforcement [`#13`](https://github.com/gfargo/git-hash-art/pull/13)
10
+ - feat: procedural shapes and contrast enforcement [`4f018ab`](https://github.com/gfargo/git-hash-art/commit/4f018abb4c89849954ccc941ee7e9ab468231f78)
11
+ - docs: update ALGORITHM.md with procedural shapes and contrast enforcement [`3e044fd`](https://github.com/gfargo/git-hash-art/commit/3e044fd940fbc0f62e3a9696fa815145cf315f67)
12
+
13
+ #### [0.6.0](https://github.com/gfargo/git-hash-art/compare/0.5.0...0.6.0)
14
+
15
+ > 19 March 2026
16
+
17
+ - feat: archetype system, palette modes, background variety, and CLI [`#12`](https://github.com/gfargo/git-hash-art/pull/12)
18
+ - feat: archetype system for dramatically different visual personalities [`2a1b919`](https://github.com/gfargo/git-hash-art/commit/2a1b919c51d3bfbd9d5c7a381ea5e104f81df2f6)
19
+ - feat: add CLI for generating art from terminal [`184372a`](https://github.com/gfargo/git-hash-art/commit/184372a584f68031bf44379f385f2e78691f98aa)
20
+ - docs: update ALGORITHM.md with archetype system, palette modes, and background styles [`4eccd07`](https://github.com/gfargo/git-hash-art/commit/4eccd077be9469c27caf60a0d8c463124ad7f9fa)
21
+
7
22
  #### [0.5.0](https://github.com/gfargo/git-hash-art/compare/0.4.1...0.5.0)
8
23
 
24
+ > 19 March 2026
25
+
9
26
  - feat: visual improvements — composition, color, and rendering upgrades [`#11`](https://github.com/gfargo/git-hash-art/pull/11)
10
27
  - feat: warm/cool temperature contrast in color system [`550bc6a`](https://github.com/gfargo/git-hash-art/commit/550bc6a87929d7ebafa973079845e688a00de929)
11
28
  - docs: update ALGORITHM.md with all new visual features [`a05de15`](https://github.com/gfargo/git-hash-art/commit/a05de1586684b91266f5a26ea7279fc601dd2202)
package/bin/cli.js ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require("child_process");
4
+ const { generateImageFromHash, saveImageToFile } = require("../dist/main.js");
5
+ const { PRESETS } = require("../dist/main.js");
6
+
7
+ const HELP = `
8
+ git-hash-art — Generate deterministic abstract art from git hashes.
9
+
10
+ Usage:
11
+ git-hash-art current [options] Generate art from HEAD commit
12
+ git-hash-art generate <hash> [options] Generate art from a specific hash
13
+
14
+ Options:
15
+ --width <n> Canvas width in pixels (default: 2048)
16
+ --height <n> Canvas height in pixels (default: 2048)
17
+ --output <dir> Output directory (default: ./output)
18
+ --preset <name> Use a built-in preset (overrides width/height)
19
+ --layers <n> Number of layers (default: 4)
20
+ --grid <n> Grid density (default: 5)
21
+ --list-presets List available presets
22
+
23
+ Examples:
24
+ npx git-hash-art current
25
+ npx git-hash-art current --preset instagram-square
26
+ npx git-hash-art generate abc123ff --width 1920 --height 1080
27
+ `;
28
+
29
+ function parseArgs(argv) {
30
+ const args = argv.slice(2);
31
+ const command = args[0];
32
+ const parsed = { command, hash: null, options: {} };
33
+
34
+ let i = 1;
35
+ if (command === "generate") {
36
+ parsed.hash = args[1];
37
+ i = 2;
38
+ }
39
+
40
+ while (i < args.length) {
41
+ const arg = args[i];
42
+ if (arg === "--width" && args[i + 1]) {
43
+ parsed.options.width = parseInt(args[i + 1], 10);
44
+ i += 2;
45
+ } else if (arg === "--height" && args[i + 1]) {
46
+ parsed.options.height = parseInt(args[i + 1], 10);
47
+ i += 2;
48
+ } else if (arg === "--output" && args[i + 1]) {
49
+ parsed.options.output = args[i + 1];
50
+ i += 2;
51
+ } else if (arg === "--preset" && args[i + 1]) {
52
+ parsed.options.preset = args[i + 1];
53
+ i += 2;
54
+ } else if (arg === "--layers" && args[i + 1]) {
55
+ parsed.options.layers = parseInt(args[i + 1], 10);
56
+ i += 2;
57
+ } else if (arg === "--grid" && args[i + 1]) {
58
+ parsed.options.gridSize = parseInt(args[i + 1], 10);
59
+ i += 2;
60
+ } else if (arg === "--list-presets") {
61
+ parsed.command = "list-presets";
62
+ i += 1;
63
+ } else if (arg === "--help" || arg === "-h") {
64
+ parsed.command = "help";
65
+ i += 1;
66
+ } else {
67
+ i += 1;
68
+ }
69
+ }
70
+
71
+ return parsed;
72
+ }
73
+
74
+ function getHeadHash() {
75
+ try {
76
+ return execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
77
+ } catch {
78
+ console.error("Error: Not a git repository or git is not installed.");
79
+ process.exit(1);
80
+ }
81
+ }
82
+
83
+ function listPresets() {
84
+ console.log("\nAvailable presets:\n");
85
+ for (const [name, config] of Object.entries(PRESETS)) {
86
+ console.log(` ${name.padEnd(20)} ${config.width}x${config.height}`);
87
+ }
88
+ console.log("");
89
+ }
90
+
91
+ function run() {
92
+ const { command, hash, options } = parseArgs(process.argv);
93
+
94
+ if (!command || command === "help") {
95
+ console.log(HELP);
96
+ process.exit(0);
97
+ }
98
+
99
+ if (command === "list-presets") {
100
+ listPresets();
101
+ process.exit(0);
102
+ }
103
+
104
+ if (command !== "current" && command !== "generate") {
105
+ console.error(`Unknown command: ${command}\n`);
106
+ console.log(HELP);
107
+ process.exit(1);
108
+ }
109
+
110
+ let gitHash;
111
+ if (command === "current") {
112
+ gitHash = getHeadHash();
113
+ console.log(`Using HEAD: ${gitHash}`);
114
+ } else {
115
+ if (!hash) {
116
+ console.error("Error: Please provide a hash.\n");
117
+ console.log("Usage: git-hash-art generate <hash> [options]");
118
+ process.exit(1);
119
+ }
120
+ gitHash = hash;
121
+ }
122
+
123
+ // Build config from preset + overrides
124
+ let config = {};
125
+ if (options.preset) {
126
+ const preset = PRESETS[options.preset];
127
+ if (!preset) {
128
+ console.error(`Unknown preset: ${options.preset}`);
129
+ console.log("Use --list-presets to see available presets.");
130
+ process.exit(1);
131
+ }
132
+ config = { ...preset };
133
+ delete config.hash;
134
+ }
135
+
136
+ if (options.width) config.width = options.width;
137
+ if (options.height) config.height = options.height;
138
+ if (options.layers) config.layers = options.layers;
139
+ if (options.gridSize) config.gridSize = options.gridSize;
140
+
141
+ const outputDir = options.output || "./output";
142
+
143
+ try {
144
+ const buffer = generateImageFromHash(gitHash, config);
145
+ const w = config.width || 2048;
146
+ const h = config.height || 2048;
147
+ const outputPath = saveImageToFile(buffer, outputDir, gitHash, "", w, h);
148
+ console.log(`Done! Image saved to ${outputPath}`);
149
+ } catch (error) {
150
+ console.error("Generation failed:", error.message);
151
+ process.exit(1);
152
+ }
153
+ }
154
+
155
+ run();