git-hash-art 0.0.4 → 0.2.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.
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createCanvas } from "@napi-rs/canvas";
3
+ import { renderHashArt } from "../lib/render";
4
+
5
+ const TEST_HASH = "46192e59d42f741c761cbea79462a8b3815dd905";
6
+
7
+ function createTestCtx(width = 128, height = 128) {
8
+ const canvas = createCanvas(width, height);
9
+ return canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
10
+ }
11
+
12
+ describe("renderHashArt (core renderer)", () => {
13
+ it("renders without error on a provided context", () => {
14
+ const ctx = createTestCtx();
15
+ expect(() =>
16
+ renderHashArt(ctx, TEST_HASH, { width: 128, height: 128 }),
17
+ ).not.toThrow();
18
+ });
19
+
20
+ it("is deterministic — same hash + config produces identical pixel data", () => {
21
+ const canvas1 = createCanvas(64, 64);
22
+ const canvas2 = createCanvas(64, 64);
23
+ const config = { width: 64, height: 64, gridSize: 3 };
24
+
25
+ renderHashArt(
26
+ canvas1.getContext("2d") as unknown as CanvasRenderingContext2D,
27
+ TEST_HASH,
28
+ config,
29
+ );
30
+ renderHashArt(
31
+ canvas2.getContext("2d") as unknown as CanvasRenderingContext2D,
32
+ TEST_HASH,
33
+ config,
34
+ );
35
+
36
+ const buf1 = canvas1.toBuffer("image/png");
37
+ const buf2 = canvas2.toBuffer("image/png");
38
+ expect(buf1.equals(buf2)).toBe(true);
39
+ });
40
+
41
+ it("different hashes produce different output", () => {
42
+ const canvas1 = createCanvas(64, 64);
43
+ const canvas2 = createCanvas(64, 64);
44
+ const config = { width: 64, height: 64, gridSize: 3 };
45
+
46
+ renderHashArt(
47
+ canvas1.getContext("2d") as unknown as CanvasRenderingContext2D,
48
+ TEST_HASH,
49
+ config,
50
+ );
51
+ renderHashArt(
52
+ canvas2.getContext("2d") as unknown as CanvasRenderingContext2D,
53
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
54
+ config,
55
+ );
56
+
57
+ const buf1 = canvas1.toBuffer("image/png");
58
+ const buf2 = canvas2.toBuffer("image/png");
59
+ expect(buf1.equals(buf2)).toBe(false);
60
+ });
61
+
62
+ it("uses defaults when no config is provided", () => {
63
+ // Large default canvas — just verify it doesn't throw
64
+ const canvas = createCanvas(2048, 2048);
65
+ const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
66
+ expect(() => renderHashArt(ctx, TEST_HASH)).not.toThrow();
67
+ });
68
+
69
+ it("respects custom config values", () => {
70
+ const ctx = createTestCtx(256, 128);
71
+ expect(() =>
72
+ renderHashArt(ctx, TEST_HASH, {
73
+ width: 256,
74
+ height: 128,
75
+ layers: 2,
76
+ gridSize: 3,
77
+ baseOpacity: 0.5,
78
+ }),
79
+ ).not.toThrow();
80
+ });
81
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createCanvas } from "@napi-rs/canvas";
3
+ import { shapes } from "../lib/canvas/shapes";
4
+ import { drawShape, enhanceShapeGeneration } from "../lib/canvas/draw";
5
+
6
+ const TEST_HASH = "46192e59d42f741c761cbea79462a8b3815dd905";
7
+
8
+ function createTestCtx(size = 256) {
9
+ const canvas = createCanvas(size, size);
10
+ return canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
11
+ }
12
+
13
+ describe("all shapes render without crashing", () => {
14
+ const shapeNames = Object.keys(shapes);
15
+
16
+ it.each(shapeNames)("shape '%s' draws without error", (name) => {
17
+ const ctx = createTestCtx();
18
+ expect(() => shapes[name](ctx, 100)).not.toThrow();
19
+ });
20
+
21
+ it.each(shapeNames)("shape '%s' works through drawShape()", (name) => {
22
+ const ctx = createTestCtx();
23
+ expect(() =>
24
+ drawShape(ctx, name, 128, 128, {
25
+ fillColor: "#ff0000",
26
+ strokeColor: "#000000",
27
+ strokeWidth: 2,
28
+ size: 80,
29
+ rotation: 45,
30
+ }),
31
+ ).not.toThrow();
32
+ });
33
+
34
+ it.each(shapeNames)(
35
+ "shape '%s' works through enhanceShapeGeneration()",
36
+ (name) => {
37
+ const ctx = createTestCtx();
38
+ expect(() =>
39
+ enhanceShapeGeneration(ctx, name, 128, 128, {
40
+ fillColor: "#ff0000",
41
+ strokeColor: "#000000",
42
+ strokeWidth: 2,
43
+ size: 80,
44
+ rotation: 45,
45
+ proportionType: "GOLDEN_RATIO",
46
+ }),
47
+ ).not.toThrow();
48
+ },
49
+ );
50
+ });
51
+
52
+ describe("platonicSolid handles missing config.type gracefully", () => {
53
+ it("renders with no config at all", () => {
54
+ const ctx = createTestCtx();
55
+ expect(() => shapes["platonicSolid"](ctx, 100)).not.toThrow();
56
+ });
57
+
58
+ it("renders with explicit type", () => {
59
+ const ctx = createTestCtx();
60
+ expect(() =>
61
+ shapes["platonicSolid"](ctx, 100, { type: "tetrahedron" }),
62
+ ).not.toThrow();
63
+ });
64
+
65
+ it("renders with invalid type (falls back)", () => {
66
+ const ctx = createTestCtx();
67
+ expect(() =>
68
+ shapes["platonicSolid"](ctx, 100, { type: "nonexistent" }),
69
+ ).not.toThrow();
70
+ });
71
+ });
package/src/browser.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Browser entry point for git-hash-art.
3
+ *
4
+ * This module has zero Node.js dependencies — it works with a standard
5
+ * HTMLCanvasElement or OffscreenCanvas and the native Canvas 2D API.
6
+ */
7
+ import { renderHashArt } from "./lib/render";
8
+ import type { GenerationConfig } from "./types";
9
+ import { DEFAULT_CONFIG } from "./types";
10
+
11
+ /**
12
+ * Render hash-derived art directly onto an HTMLCanvasElement.
13
+ *
14
+ * The canvas should already have the desired width/height set.
15
+ * Config width/height will be inferred from the canvas if not provided.
16
+ *
17
+ * @param canvas - An HTMLCanvasElement (or OffscreenCanvas)
18
+ * @param gitHash - Hex hash string used as the deterministic seed
19
+ * @param config - Partial generation config (merged with defaults)
20
+ */
21
+ function renderToCanvas(
22
+ canvas: HTMLCanvasElement | OffscreenCanvas,
23
+ gitHash: string,
24
+ config: Partial<GenerationConfig> = {},
25
+ ): void {
26
+ const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
27
+ if (!ctx) {
28
+ throw new Error("Failed to get 2D rendering context from canvas");
29
+ }
30
+
31
+ const finalConfig: Partial<GenerationConfig> = {
32
+ width: canvas.width,
33
+ height: canvas.height,
34
+ ...config,
35
+ };
36
+
37
+ renderHashArt(ctx, gitHash, finalConfig);
38
+ }
39
+
40
+ /**
41
+ * Render hash-derived art and return it as a Blob (browser-native).
42
+ *
43
+ * @param gitHash - Hex hash string used as the deterministic seed
44
+ * @param config - Partial generation config (merged with defaults)
45
+ * @returns A Promise that resolves to a PNG Blob
46
+ */
47
+ async function generateImageBlob(
48
+ gitHash: string,
49
+ config: Partial<GenerationConfig> = {},
50
+ ): Promise<Blob> {
51
+ const finalConfig: GenerationConfig = { ...DEFAULT_CONFIG, ...config };
52
+ const { width, height } = finalConfig;
53
+
54
+ const canvas = new OffscreenCanvas(width, height);
55
+ const ctx = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D;
56
+ if (!ctx) {
57
+ throw new Error("Failed to get 2D rendering context from OffscreenCanvas");
58
+ }
59
+
60
+ renderHashArt(
61
+ ctx as unknown as CanvasRenderingContext2D,
62
+ gitHash,
63
+ finalConfig,
64
+ );
65
+
66
+ return canvas.convertToBlob({ type: "image/png" });
67
+ }
68
+
69
+ /**
70
+ * Render hash-derived art and return it as a data URL string.
71
+ *
72
+ * @param gitHash - Hex hash string used as the deterministic seed
73
+ * @param config - Partial generation config (merged with defaults)
74
+ * @returns A data:image/png;base64,… string
75
+ */
76
+ function generateDataURL(
77
+ gitHash: string,
78
+ config: Partial<GenerationConfig> = {},
79
+ ): string {
80
+ const finalConfig: GenerationConfig = { ...DEFAULT_CONFIG, ...config };
81
+ const { width, height } = finalConfig;
82
+
83
+ const canvas = document.createElement("canvas");
84
+ canvas.width = width;
85
+ canvas.height = height;
86
+
87
+ const ctx = canvas.getContext("2d");
88
+ if (!ctx) {
89
+ throw new Error("Failed to get 2D rendering context");
90
+ }
91
+
92
+ renderHashArt(ctx, gitHash, finalConfig);
93
+
94
+ return canvas.toDataURL("image/png");
95
+ }
96
+
97
+ export {
98
+ renderToCanvas,
99
+ generateImageBlob,
100
+ generateDataURL,
101
+ renderHashArt,
102
+ };
103
+ export { PRESETS } from "./lib/constants";
104
+ export type { GenerationConfig } from "./types";
105
+ export { DEFAULT_CONFIG } from "./types";
package/src/index.ts CHANGED
@@ -1,208 +1,54 @@
1
- import { createCanvas } from "canvas";
2
- import fs from "fs";
3
- import path from "path";
4
- import { SacredColorScheme } from "./lib/canvas/colors";
5
- import { enhanceShapeGeneration } from "./lib/canvas/draw";
6
- import { shapes } from "./lib/canvas/shapes";
7
- import { getRandomFromHash } from "./lib/utils";
1
+ import { createCanvas } from "@napi-rs/canvas";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { renderHashArt } from "./lib/render";
5
+ import type { GenerationConfig } from "./types";
6
+ import { DEFAULT_CONFIG } from "./types";
8
7
 
9
8
  /**
10
- * Generate an abstract art image from a git hash with custom configuration
11
- * @param {string} gitHash - The git hash to use as a seed
12
- * @param {ArtConfig} [config={}] - Configuration options
13
- * @returns {Buffer} PNG buffer of the generated image
9
+ * Generate an abstract art PNG buffer from a git hash (Node.js only).
10
+ *
11
+ * Uses @napi-rs/canvas under the hood to create an off-screen canvas,
12
+ * renders the hash-derived art, and returns the result as a PNG Buffer.
13
+ *
14
+ * @param gitHash - Hex hash string used as the deterministic seed
15
+ * @param config - Partial generation config (merged with defaults)
16
+ * @returns PNG buffer of the generated image
14
17
  */
15
- function generateImageFromHash(gitHash: string, config = {}) {
16
- // Default configuration
17
- const defaultConfig = {
18
- width: 2048,
19
- height: 2048,
20
- gridSize: 12,
21
- layers: 2,
22
- minShapeSize: 20,
23
- maxShapeSize: 600,
24
- baseOpacity: 0.8,
25
- opacityReduction: 0.4,
26
- shapesPerLayer: 0,
27
- };
28
-
29
- // Merge provided config with defaults
30
- const finalConfig = { ...defaultConfig, ...config };
31
- const {
32
- width,
33
- height,
34
- gridSize,
35
- layers,
36
- minShapeSize,
37
- maxShapeSize,
38
- baseOpacity,
39
- opacityReduction,
40
- } = finalConfig;
41
-
42
- // Calculate shapes per layer based on grid size if not provided
43
- finalConfig.shapesPerLayer =
44
- finalConfig.shapesPerLayer || Math.floor(gridSize * gridSize * 1.5);
18
+ function generateImageFromHash(
19
+ gitHash: string,
20
+ config: Partial<GenerationConfig> = {},
21
+ ): Buffer {
22
+ const finalConfig: GenerationConfig = { ...DEFAULT_CONFIG, ...config };
23
+ const { width, height } = finalConfig;
45
24
 
46
25
  const canvas = createCanvas(width, height);
47
26
  const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
48
27
 
49
- const colorScheme = new SacredColorScheme(gitHash);
50
- const colors = colorScheme.getColorPalette("chakra");
51
-
52
- // Create a gradient background
53
- const gradient = ctx.createLinearGradient(0, 0, width, height);
54
- gradient.addColorStop(0, colorScheme.baseScheme[0]);
55
- gradient.addColorStop(1, colorScheme.baseScheme[1]);
56
- ctx.fillStyle = gradient;
57
- ctx.fillRect(0, 0, width, height);
58
-
59
- const shapeNames = Object.keys(shapes);
60
-
61
- const cellWidth = width / gridSize;
62
- const cellHeight = height / gridSize;
63
-
64
- // Scale shape sizes based on canvas dimensions
65
- const scaleFactor = Math.min(width, height) / 1024;
66
- const adjustedMinSize = minShapeSize * scaleFactor;
67
- const adjustedMaxSize = maxShapeSize * scaleFactor;
68
-
69
- for (let layer = 0; layer < layers; layer++) {
70
- const numShapes =
71
- finalConfig.shapesPerLayer +
72
- Math.floor(
73
- getRandomFromHash(gitHash, layer, 0, finalConfig.shapesPerLayer / 2),
74
- );
75
- const layerOpacity = baseOpacity - layer * opacityReduction;
76
-
77
- for (let i = 0; i < numShapes; i++) {
78
- const gridX = Math.floor(i / gridSize);
79
- const gridY = i % gridSize;
80
-
81
- const cellOffsetX = getRandomFromHash(
82
- gitHash,
83
- layer * numShapes + i * 2,
84
- 0,
85
- cellWidth,
86
- );
87
- const cellOffsetY = getRandomFromHash(
88
- gitHash,
89
- layer * numShapes + i * 2 + 1,
90
- 0,
91
- cellHeight,
92
- );
93
-
94
- const x = gridX * cellWidth + cellOffsetX;
95
- const y = gridY * cellHeight + cellOffsetY;
96
-
97
- const shape =
98
- shapeNames[
99
- Math.floor(
100
- getRandomFromHash(
101
- gitHash,
102
- layer * numShapes + i * 3,
103
- 0,
104
- shapeNames.length,
105
- ),
106
- )
107
- ];
108
- const size =
109
- adjustedMinSize +
110
- getRandomFromHash(
111
- gitHash,
112
- layer * numShapes + i * 4,
113
- 0,
114
- adjustedMaxSize - adjustedMinSize,
115
- );
116
- const rotation = getRandomFromHash(
117
- gitHash,
118
- layer * numShapes + i * 5,
119
- 0,
120
- 360,
121
- );
122
-
123
- const fillColorIndex = Math.floor(
124
- getRandomFromHash(
125
- gitHash,
126
- layer * numShapes + i * 6,
127
- 0,
128
- Object.keys(colors).length,
129
- ),
130
- );
131
- const strokeColorIndex = Math.floor(
132
- getRandomFromHash(
133
- gitHash,
134
- layer * numShapes + i * 7,
135
- 0,
136
- Object.keys(colors).length,
137
- ),
138
- );
139
-
140
- ctx.globalAlpha = layerOpacity;
141
- // drawShape(
142
- // ctx,
143
- // shape,
144
- // x,
145
- // y,
146
- // colors[fillColorIndex],
147
- // colors[strokeColorIndex],
148
- // 2 * scaleFactor,
149
- // size,
150
- // rotation
151
- // );
152
-
153
- enhanceShapeGeneration(ctx, shape, x, y, {
154
- fillColor: Object.values(colors)[fillColorIndex],
155
- strokeColor: Object.values(colors)[strokeColorIndex],
156
- strokeWidth: 1.5 * scaleFactor,
157
- size,
158
- rotation,
159
- // Optionally add pattern combinations
160
- // patterns:
161
- // Math.random() > 0.7 ? PatternPresets.cosmicTree(size) : [],
162
- proportionType: "GOLDEN_RATIO",
163
- });
164
- }
165
-
166
- // Add connecting lines scaled to canvas size
167
- ctx.globalAlpha = 0.2;
168
- ctx.strokeStyle = Object.values(colors)[Object.keys(colors).length - 1];
169
- ctx.lineWidth = 1 * scaleFactor;
170
-
171
- const numLines = Math.floor((15 * (width * height)) / (1024 * 1024));
172
- for (let i = 0; i < numLines; i++) {
173
- const x1 = getRandomFromHash(gitHash, i * 4, 0, width);
174
- const y1 = getRandomFromHash(gitHash, i * 4 + 1, 0, height);
175
- const x2 = getRandomFromHash(gitHash, i * 4 + 2, 0, width);
176
- const y2 = getRandomFromHash(gitHash, i * 4 + 3, 0, height);
177
-
178
- ctx.beginPath();
179
- ctx.moveTo(x1, y1);
180
- ctx.lineTo(x2, y2);
181
- ctx.stroke();
182
- }
183
- }
28
+ renderHashArt(ctx, gitHash, finalConfig);
184
29
 
185
30
  return canvas.toBuffer("image/png");
186
31
  }
187
32
 
188
33
  /**
189
- * Save the generated image to a file
190
- * @param {Buffer} imageBuffer - The PNG buffer of the generated image
191
- * @param {string} outputDir - The directory to save the image
192
- * @param {string} gitHash - The git hash used to generate the image
193
- * @param {string} [label=''] - Label for the output file
194
- * @param {number} width - The width of the generated image
195
- * @param {number} height - The height of the generated image
196
- * @returns {string} Path to the saved image
34
+ * Save the generated image to a file (Node.js only).
35
+ *
36
+ * @param imageBuffer - The PNG buffer of the generated image
37
+ * @param outputDir - The directory to save the image
38
+ * @param gitHash - The git hash used to generate the image
39
+ * @param label - Optional label for the output filename
40
+ * @param width - The width of the generated image
41
+ * @param height - The height of the generated image
42
+ * @returns Path to the saved image
197
43
  */
198
44
  function saveImageToFile(
199
- imageBuffer: string,
45
+ imageBuffer: Buffer,
200
46
  outputDir: string,
201
- gitHash: string | any[],
47
+ gitHash: string,
202
48
  label = "",
203
- width: any,
204
- height: any,
205
- ) {
49
+ width: number,
50
+ height: number,
51
+ ): string {
206
52
  if (!fs.existsSync(outputDir)) {
207
53
  fs.mkdirSync(outputDir, { recursive: true });
208
54
  }
@@ -218,14 +64,7 @@ function saveImageToFile(
218
64
  return outputPath;
219
65
  }
220
66
 
221
- export { generateImageFromHash, saveImageToFile };
222
-
223
- // Usage example:
224
- /*
225
- import { generateImageFromHash, saveImageToFile } from 'git-hash-art';
226
-
227
- const gitHash = '1234567890abcdef1234567890abcdef12345678';
228
- const imageBuffer = generateImageFromHash(gitHash, { width: 1024, height: 1024 });
229
- const savedImagePath = saveImageToFile(imageBuffer, './output', gitHash, 'example', 1024, 1024);
230
- console.log(`Image saved to: ${savedImagePath}`);
231
- */
67
+ export { generateImageFromHash, saveImageToFile, renderHashArt };
68
+ export { PRESETS } from "./lib/constants";
69
+ export type { GenerationConfig } from "./types";
70
+ export { DEFAULT_CONFIG } from "./types";