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.
@@ -5,9 +5,6 @@ import { gitHashToSeed } from "../utils";
5
5
 
6
6
  /**
7
7
  * Generates a color scheme based on a given Git hash.
8
- *
9
- * @param {string} gitHash - The Git hash used to generate the color scheme.
10
- * @returns {string[]} An array of hex color codes representing the generated color scheme.
11
8
  */
12
9
  export function generateColorScheme(gitHash: string): string[] {
13
10
  const seed = gitHashToSeed(gitHash);
@@ -34,53 +31,24 @@ interface MetallicColors {
34
31
  bronze: string;
35
32
  }
36
33
 
37
- interface SacredPalette {
38
- primary: string;
39
- secondary: string;
40
- accent: string;
41
- metallic: string;
42
- }
43
-
44
- interface ElementalPalette {
45
- earth: string;
46
- water: string;
47
- air: string;
48
- fire: string;
49
- }
50
-
51
- interface ChakraPalette {
52
- root: string;
53
- sacral: string;
54
- solar: string;
55
- heart: string;
56
- throat: string;
57
- third_eye: string;
58
- crown: string;
59
- }
60
-
61
- type ColorPalette = SacredPalette | ElementalPalette | ChakraPalette | string[];
62
-
63
34
  // Enhanced color scheme generation for sacred geometry
64
35
  export class SacredColorScheme {
65
36
  private seed: number;
66
37
  public baseScheme: string[];
67
38
  private complementaryScheme: string[];
39
+ private triadicScheme: string[];
68
40
  private metallic: MetallicColors;
69
41
 
70
42
  constructor(gitHash: string) {
71
- this.seed = this.gitHashToSeed(gitHash);
43
+ this.seed = gitHashToSeed(gitHash);
72
44
  this.baseScheme = this.generateBaseScheme();
73
45
  this.complementaryScheme = this.generateComplementaryScheme();
46
+ this.triadicScheme = this.generateTriadicScheme();
74
47
  this.metallic = this.generateMetallicColors();
75
48
  }
76
49
 
77
- private gitHashToSeed(hash: string): number {
78
- return parseInt(hash.slice(0, 8), 16);
79
- }
80
-
81
50
  private generateBaseScheme(): string[] {
82
51
  const scheme = new ColorScheme();
83
- scheme;
84
52
  return scheme
85
53
  .from_hue(this.seed % 360)
86
54
  .scheme("analogic")
@@ -100,6 +68,17 @@ export class SacredColorScheme {
100
68
  .map((hex: string) => `#${hex}`);
101
69
  }
102
70
 
71
+ private generateTriadicScheme(): string[] {
72
+ const triadicHue = (this.seed + 120) % 360;
73
+ const scheme = new ColorScheme();
74
+ return scheme
75
+ .from_hue(triadicHue)
76
+ .scheme("triade")
77
+ .variation("soft")
78
+ .colors()
79
+ .map((hex: string) => `#${hex}`);
80
+ }
81
+
103
82
  private generateMetallicColors(): MetallicColors {
104
83
  return {
105
84
  gold: "#FFD700",
@@ -109,36 +88,81 @@ export class SacredColorScheme {
109
88
  };
110
89
  }
111
90
 
112
- getColorPalette(
113
- type: "sacred" | "elemental" | "chakra" | "default" = "sacred",
114
- ): ColorPalette {
115
- switch (type) {
116
- case "sacred":
117
- return {
118
- primary: this.baseScheme[0],
119
- secondary: this.baseScheme[1],
120
- accent: this.complementaryScheme[0],
121
- metallic: this.metallic.gold,
122
- };
123
- case "elemental":
124
- return {
125
- earth: this.baseScheme[0],
126
- water: this.baseScheme[1],
127
- air: this.baseScheme[2],
128
- fire: this.complementaryScheme[0],
129
- };
130
- case "chakra":
131
- return {
132
- root: "#FF0000",
133
- sacral: "#FF7F00",
134
- solar: "#FFFF00",
135
- heart: "#00FF00",
136
- throat: "#0000FF",
137
- third_eye: "#4B0082",
138
- crown: "#8F00FF",
139
- };
140
- default:
141
- return this.baseScheme;
142
- }
91
+ /**
92
+ * Returns a flat array of hash-derived colors suitable for art generation.
93
+ * Combines base analogic, complementary, and triadic schemes for variety
94
+ * while maintaining color harmony.
95
+ */
96
+ getColors(): string[] {
97
+ // Deduplicate and return a rich palette
98
+ const all = [
99
+ ...this.baseScheme.slice(0, 4),
100
+ ...this.complementaryScheme.slice(0, 2),
101
+ ...this.triadicScheme.slice(0, 2),
102
+ ];
103
+ return [...new Set(all)];
143
104
  }
105
+
106
+ /**
107
+ * Returns two background colors derived from the hash — darker variants
108
+ * of the base scheme for gradient backgrounds.
109
+ */
110
+ getBackgroundColors(): [string, string] {
111
+ return [
112
+ this.darken(this.baseScheme[0], 0.65),
113
+ this.darken(this.baseScheme[1], 0.55),
114
+ ];
115
+ }
116
+
117
+ /**
118
+ * Simple hex color darkening by a factor (0 = black, 1 = unchanged).
119
+ */
120
+ private darken(hex: string, factor: number): string {
121
+ const c = hex.replace("#", "");
122
+ const r = Math.round(parseInt(c.substring(0, 2), 16) * factor);
123
+ const g = Math.round(parseInt(c.substring(2, 4), 16) * factor);
124
+ const b = Math.round(parseInt(c.substring(4, 6), 16) * factor);
125
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
126
+ }
127
+ }
128
+
129
+ // ── Standalone color utilities ──────────────────────────────────────
130
+
131
+ /** Parse a hex color (#RRGGBB) into [r, g, b] 0-255. */
132
+ function hexToRgb(hex: string): [number, number, number] {
133
+ const c = hex.replace("#", "");
134
+ return [
135
+ parseInt(c.substring(0, 2), 16),
136
+ parseInt(c.substring(2, 4), 16),
137
+ parseInt(c.substring(4, 6), 16),
138
+ ];
139
+ }
140
+
141
+ /** Format [r, g, b] back to #RRGGBB. */
142
+ function rgbToHex(r: number, g: number, b: number): string {
143
+ const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)));
144
+ return `#${clamp(r).toString(16).padStart(2, "0")}${clamp(g).toString(16).padStart(2, "0")}${clamp(b).toString(16).padStart(2, "0")}`;
145
+ }
146
+
147
+ /**
148
+ * Return a hex color with an alpha component as an rgba() CSS string.
149
+ * `alpha` is 0-1.
150
+ */
151
+ export function hexWithAlpha(hex: string, alpha: number): string {
152
+ const [r, g, b] = hexToRgb(hex);
153
+ return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
154
+ }
155
+
156
+ /**
157
+ * Apply slight hue/saturation/lightness jitter to a hex color.
158
+ * `rng` should return a float in [0,1). `amount` controls intensity (0-1, default 0.1).
159
+ */
160
+ export function jitterColor(
161
+ hex: string,
162
+ rng: () => number,
163
+ amount = 0.1,
164
+ ): string {
165
+ const [r, g, b] = hexToRgb(hex);
166
+ const jit = () => (rng() - 0.5) * 2 * amount * 255;
167
+ return rgbToHex(r + jit(), g + jit(), b + jit());
144
168
  }
@@ -14,6 +14,11 @@ interface EnhanceShapeConfig extends DrawShapeConfig {
14
14
  proportionType?: ProportionType;
15
15
  baseOpacity?: number;
16
16
  opacityReduction?: number;
17
+ /** If provided, applies a glow (shadowBlur) effect. */
18
+ glowRadius?: number;
19
+ glowColor?: string;
20
+ /** If provided, fills with a radial gradient between two colors. */
21
+ gradientFillEnd?: string;
17
22
  }
18
23
 
19
24
  export function drawShape(
@@ -34,14 +39,16 @@ export function drawShape(
34
39
  const drawFunction = shapes[shape];
35
40
  if (drawFunction) {
36
41
  drawFunction(ctx, size);
42
+ ctx.fill();
43
+ ctx.stroke();
37
44
  }
38
45
 
39
- ctx.fill();
40
- ctx.stroke();
41
46
  ctx.restore();
42
47
  }
43
48
 
44
- // Integration with existing generation logic
49
+ /**
50
+ * Enhanced shape drawing with glow, gradient fills, and pattern layering.
51
+ */
45
52
  export function enhanceShapeGeneration(
46
53
  ctx: CanvasRenderingContext2D,
47
54
  shape: string,
@@ -59,20 +66,46 @@ export function enhanceShapeGeneration(
59
66
  proportionType = "GOLDEN_RATIO",
60
67
  baseOpacity = 0.6,
61
68
  opacityReduction = 0.1,
69
+ glowRadius = 0,
70
+ glowColor,
71
+ gradientFillEnd,
62
72
  } = config;
63
73
 
64
74
  ctx.save();
65
75
  ctx.translate(x, y);
66
76
  ctx.rotate((rotation * Math.PI) / 180);
67
77
 
68
- // Draw base shape
69
- ctx.fillStyle = fillColor;
78
+ // Glow / shadow effect
79
+ if (glowRadius > 0) {
80
+ ctx.shadowBlur = glowRadius;
81
+ ctx.shadowColor = glowColor || fillColor;
82
+ ctx.shadowOffsetX = 0;
83
+ ctx.shadowOffsetY = 0;
84
+ }
85
+
86
+ // Gradient fill or flat fill
87
+ if (gradientFillEnd) {
88
+ const grad = ctx.createRadialGradient(0, 0, 0, 0, 0, size / 2);
89
+ grad.addColorStop(0, fillColor);
90
+ grad.addColorStop(1, gradientFillEnd);
91
+ ctx.fillStyle = grad;
92
+ } else {
93
+ ctx.fillStyle = fillColor;
94
+ }
95
+
70
96
  ctx.strokeStyle = strokeColor;
71
97
  ctx.lineWidth = strokeWidth;
72
98
 
73
99
  const drawFunction = shapes[shape];
74
100
  if (drawFunction) {
75
101
  drawFunction(ctx, size);
102
+ ctx.fill();
103
+ ctx.stroke();
104
+ }
105
+
106
+ // Reset shadow so patterns aren't double-glowed
107
+ if (glowRadius > 0) {
108
+ ctx.shadowBlur = 0;
76
109
  }
77
110
 
78
111
  // Layer additional patterns if specified
@@ -85,7 +118,5 @@ export function enhanceShapeGeneration(
85
118
  });
86
119
  }
87
120
 
88
- ctx.fill();
89
- ctx.stroke();
90
121
  ctx.restore();
91
122
  }
@@ -21,7 +21,7 @@ export const drawTriangle: DrawFunction = (ctx, size) => {
21
21
  export const drawHexagon: DrawFunction = (ctx, size) => {
22
22
  ctx.beginPath();
23
23
  for (let i = 0; i < 6; i++) {
24
- const angle = (Math.PI / 8) * i;
24
+ const angle = ((Math.PI * 2) / 6) * i;
25
25
  const x = (size / 2) * Math.cos(angle);
26
26
  const y = (size / 2) * Math.sin(angle);
27
27
  if (i === 0) ctx.moveTo(x, y);
@@ -54,10 +54,13 @@ export const drawPlatonicSolid: DrawFunction = (ctx, size, config = {}) => {
54
54
  const finalConfig = { ...defaultShapeConfig, ...config };
55
55
  applyTransforms(ctx, size, finalConfig);
56
56
 
57
+ const solidType = config.type as keyof typeof ShapeConfigs.platonic;
58
+ const solidConfig =
59
+ ShapeConfigs.platonic[solidType] ?? ShapeConfigs.platonic.icosahedron;
57
60
  const {
58
61
  vertices,
59
62
  // faces
60
- } = ShapeConfigs.platonic[config.type as keyof typeof ShapeConfigs.platonic];
63
+ } = solidConfig;
61
64
  const radius = size / 2;
62
65
 
63
66
  // Calculate vertices based on platonic solid type
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Pure rendering logic — environment-agnostic.
3
+ *
4
+ * This module only uses the standard CanvasRenderingContext2D API,
5
+ * so it works identically in Node (@napi-rs/canvas) and browsers
6
+ * (HTMLCanvasElement).
7
+ */
8
+ import { SacredColorScheme } from "./canvas/colors";
9
+ import { enhanceShapeGeneration } from "./canvas/draw";
10
+ import { shapes } from "./canvas/shapes";
11
+ import { createRng, seedFromHash } from "./utils";
12
+ import { DEFAULT_CONFIG, type GenerationConfig } from "../types";
13
+
14
+ /**
15
+ * Render hash-derived art onto an existing CanvasRenderingContext2D.
16
+ *
17
+ * This is the environment-agnostic core — it never creates a canvas or
18
+ * produces a buffer. Call it from Node or browser wrappers that supply
19
+ * the context.
20
+ *
21
+ * @param ctx - A 2D rendering context (browser or Node canvas)
22
+ * @param gitHash - Hex hash string used as the deterministic seed
23
+ * @param config - Partial generation config (merged with defaults)
24
+ */
25
+ export function renderHashArt(
26
+ ctx: CanvasRenderingContext2D,
27
+ gitHash: string,
28
+ config: Partial<GenerationConfig> = {},
29
+ ): void {
30
+ const finalConfig: GenerationConfig = { ...DEFAULT_CONFIG, ...config };
31
+ const {
32
+ width,
33
+ height,
34
+ gridSize,
35
+ layers,
36
+ minShapeSize,
37
+ maxShapeSize,
38
+ baseOpacity,
39
+ opacityReduction,
40
+ } = finalConfig;
41
+
42
+ finalConfig.shapesPerLayer =
43
+ finalConfig.shapesPerLayer || Math.floor(gridSize * gridSize * 1.5);
44
+
45
+ // --- Color scheme derived from hash ---
46
+ const colorScheme = new SacredColorScheme(gitHash);
47
+ const colors = colorScheme.getColors();
48
+ const [bgStart, bgEnd] = colorScheme.getBackgroundColors();
49
+
50
+ // --- Radial gradient background for depth ---
51
+ const cx = width / 2;
52
+ const cy = height / 2;
53
+ const bgRadius = Math.hypot(cx, cy);
54
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, bgRadius);
55
+ gradient.addColorStop(0, bgStart);
56
+ gradient.addColorStop(1, bgEnd);
57
+ ctx.fillStyle = gradient;
58
+ ctx.fillRect(0, 0, width, height);
59
+
60
+ const shapeNames = Object.keys(shapes);
61
+ const scaleFactor = Math.min(width, height) / 1024;
62
+ const adjustedMinSize = minShapeSize * scaleFactor;
63
+ const adjustedMaxSize = maxShapeSize * scaleFactor;
64
+
65
+ // One master RNG seeded from the full hash — all randomness flows from here
66
+ const rng = createRng(seedFromHash(gitHash));
67
+
68
+ // Track shape positions for organic connecting curves later
69
+ const shapePositions: Array<{ x: number; y: number }> = [];
70
+
71
+ for (let layer = 0; layer < layers; layer++) {
72
+ const numShapes =
73
+ finalConfig.shapesPerLayer +
74
+ Math.floor(rng() * finalConfig.shapesPerLayer * 0.3);
75
+
76
+ // Layer opacity decays gently so all layers remain visible
77
+ const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
78
+
79
+ // Later layers use smaller shapes for depth
80
+ const layerSizeScale = 1 - layer * 0.15;
81
+
82
+ for (let i = 0; i < numShapes; i++) {
83
+ const x = rng() * width;
84
+ const y = rng() * height;
85
+
86
+ const shapeIdx = Math.floor(rng() * shapeNames.length);
87
+ const shape = shapeNames[shapeIdx];
88
+
89
+ // Shape size follows a power distribution — many small, few large
90
+ const sizeT = Math.pow(rng(), 1.8);
91
+ const size =
92
+ (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) *
93
+ layerSizeScale;
94
+
95
+ const rotation = rng() * 360;
96
+
97
+ const fillColor = colors[Math.floor(rng() * colors.length)];
98
+ const strokeColor = colors[Math.floor(rng() * colors.length)];
99
+
100
+ const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor;
101
+
102
+ ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5);
103
+
104
+ enhanceShapeGeneration(ctx, shape, x, y, {
105
+ fillColor,
106
+ strokeColor,
107
+ strokeWidth,
108
+ size,
109
+ rotation,
110
+ proportionType: "GOLDEN_RATIO",
111
+ });
112
+
113
+ shapePositions.push({ x, y });
114
+ }
115
+ }
116
+
117
+ // --- Organic connecting curves between nearby shapes ---
118
+ if (shapePositions.length > 1) {
119
+ const numCurves = Math.floor((8 * (width * height)) / (1024 * 1024));
120
+ ctx.lineWidth = 0.8 * scaleFactor;
121
+
122
+ for (let i = 0; i < numCurves; i++) {
123
+ const idxA = Math.floor(rng() * shapePositions.length);
124
+ const offset =
125
+ 1 + Math.floor(rng() * Math.min(5, shapePositions.length - 1));
126
+ const idxB = (idxA + offset) % shapePositions.length;
127
+
128
+ const a = shapePositions[idxA];
129
+ const b = shapePositions[idxB];
130
+
131
+ const mx = (a.x + b.x) / 2;
132
+ const my = (a.y + b.y) / 2;
133
+ const dx = b.x - a.x;
134
+ const dy = b.y - a.y;
135
+ const dist = Math.hypot(dx, dy);
136
+ const bulge = (rng() - 0.5) * dist * 0.4;
137
+
138
+ const cpx = mx + (-dy / (dist || 1)) * bulge;
139
+ const cpy = my + (dx / (dist || 1)) * bulge;
140
+
141
+ ctx.globalAlpha = 0.08 + rng() * 0.12;
142
+ ctx.strokeStyle = colors[Math.floor(rng() * colors.length)];
143
+
144
+ ctx.beginPath();
145
+ ctx.moveTo(a.x, a.y);
146
+ ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);
147
+ ctx.stroke();
148
+ }
149
+ }
150
+
151
+ ctx.globalAlpha = 1;
152
+ }
package/src/lib/utils.ts CHANGED
@@ -1,18 +1,45 @@
1
- import { shapes } from "./canvas/shapes";
2
-
3
1
  export function gitHashToSeed(gitHash: string): number {
4
2
  return parseInt(gitHash.slice(0, 8), 16);
5
3
  }
6
4
 
5
+ /**
6
+ * Mulberry32 — a fast, high-quality 32-bit seeded PRNG.
7
+ * Returns a function that produces deterministic floats in [0, 1).
8
+ */
9
+ export function createRng(seed: number): () => number {
10
+ let s = seed | 0;
11
+ return () => {
12
+ s = (s + 0x6d2b79f5) | 0;
13
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
14
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
15
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Derive a deterministic seed from a hash string and an extra index
21
+ * so each call-site gets its own independent stream.
22
+ */
23
+ export function seedFromHash(hash: string, offset = 0): number {
24
+ let h = 0;
25
+ for (let i = 0; i < hash.length; i++) {
26
+ h = (Math.imul(31, h) + hash.charCodeAt(i)) | 0;
27
+ }
28
+ return (h + offset) | 0;
29
+ }
30
+
31
+ /**
32
+ * Legacy helper kept for backward compat — now backed by mulberry32.
33
+ * Prefer createRng() + rng() for new code.
34
+ */
7
35
  export function getRandomFromHash(
8
36
  hash: string,
9
37
  index: number,
10
38
  min: number,
11
39
  max: number,
12
40
  ): number {
13
- const hexPair = hash.substr((index * 2) % hash.length, 2);
14
- const decimal = parseInt(hexPair, 16);
15
- return min + (decimal / 255) * (max - min);
41
+ const rng = createRng(seedFromHash(hash, index));
42
+ return min + rng() * (max - min);
16
43
  }
17
44
 
18
45
  // Golden ratio and other important proportions
@@ -29,7 +56,7 @@ export type ProportionType = keyof typeof Proportions;
29
56
 
30
57
  interface Pattern {
31
58
  type: string;
32
- config: any; // You might want to define a more specific type for config
59
+ config: any;
33
60
  }
34
61
 
35
62
  interface LayerConfig {
package/src/types.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Configuration options for image generation.
3
+ */
4
+ export interface GenerationConfig {
5
+ /** Canvas width in pixels (default: 2048) */
6
+ width: number;
7
+ /** Canvas height in pixels (default: 2048) */
8
+ height: number;
9
+ /** Controls base shape count per layer — gridSize² × 1.5 (default: 5) */
10
+ gridSize: number;
11
+ /** Number of layers to generate (default: 4) */
12
+ layers: number;
13
+ /** Minimum shape size in pixels, scaled to canvas (default: 30) */
14
+ minShapeSize: number;
15
+ /** Maximum shape size in pixels, scaled to canvas (default: 400) */
16
+ maxShapeSize: number;
17
+ /** Starting opacity for the first layer (default: 0.7) */
18
+ baseOpacity: number;
19
+ /** Opacity reduction per layer (default: 0.12) */
20
+ opacityReduction: number;
21
+ /** Base shapes per layer — defaults to gridSize² × 1.5 when 0 */
22
+ shapesPerLayer: number;
23
+ }
24
+
25
+ export const DEFAULT_CONFIG: GenerationConfig = {
26
+ width: 2048,
27
+ height: 2048,
28
+ gridSize: 5,
29
+ layers: 4,
30
+ minShapeSize: 30,
31
+ maxShapeSize: 400,
32
+ baseOpacity: 0.7,
33
+ opacityReduction: 0.12,
34
+ shapesPerLayer: 0,
35
+ };