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.
- package/.kiro/steering/product.md +16 -0
- package/.kiro/steering/structure.md +40 -0
- package/.kiro/steering/tech.md +24 -0
- package/CHANGELOG.md +18 -1
- package/README.md +165 -57
- package/dist/browser.js +1183 -0
- package/dist/browser.js.map +1 -0
- package/dist/main.js +424 -330
- package/dist/main.js.map +1 -1
- package/dist/module.js +417 -326
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +2 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +47 -3
- package/src/__tests__/compatibility.test.ts +42 -0
- package/src/__tests__/generation.test.ts +58 -0
- package/src/__tests__/render.test.ts +81 -0
- package/src/__tests__/shapes.test.ts +71 -0
- package/src/browser.ts +105 -0
- package/src/index.ts +39 -200
- package/src/lib/canvas/colors.ts +90 -66
- package/src/lib/canvas/draw.ts +38 -7
- package/src/lib/canvas/shapes/basic.ts +1 -1
- package/src/lib/canvas/shapes/complex.ts +4 -1
- package/src/lib/render.ts +152 -0
- package/src/lib/utils.ts +33 -6
- package/src/types.ts +35 -0
package/src/lib/canvas/colors.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
}
|
package/src/lib/canvas/draw.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
69
|
-
|
|
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 /
|
|
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
|
-
} =
|
|
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
|
|
14
|
-
|
|
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;
|
|
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
|
+
};
|