git-hash-art 0.4.1 → 0.5.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 +64 -13
- package/CHANGELOG.md +10 -0
- package/dist/browser.js +282 -7
- package/dist/browser.js.map +1 -1
- package/dist/main.js +283 -7
- package/dist/main.js.map +1 -1
- package/dist/module.js +283 -7
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/canvas/colors.ts +104 -5
- package/src/lib/canvas/draw.ts +75 -1
- package/src/lib/render.ts +118 -7
package/ALGORITHM.md
CHANGED
|
@@ -9,27 +9,32 @@ Hash String
|
|
|
9
9
|
│
|
|
10
10
|
├─► Seed (mulberry32 PRNG)
|
|
11
11
|
│
|
|
12
|
-
├─► Color Scheme (hash-driven variation + scheme type
|
|
12
|
+
├─► Color Scheme (hash-driven variation + scheme type + temperature mode)
|
|
13
13
|
│
|
|
14
14
|
└─► Rendering Pipeline
|
|
15
15
|
│
|
|
16
|
-
1. Background Layer (radial gradient)
|
|
16
|
+
1. Background Layer (radial gradient, temperature-shifted)
|
|
17
17
|
1b. Layered Background (faint shapes + concentric rings)
|
|
18
18
|
2. Composition Mode Selection
|
|
19
|
-
|
|
19
|
+
2b. Symmetry Mode Selection (none / bilateral / quad)
|
|
20
|
+
3. Focal Points (rule-of-thirds biased) + Void Zones
|
|
20
21
|
4. Flow Field Initialization
|
|
22
|
+
4b. Hero Shape (large focal anchor, ~60% of images)
|
|
21
23
|
5. Shape Layers (× N layers)
|
|
22
24
|
│ ├─ Blend Mode (per-layer compositing)
|
|
23
|
-
│ ├─ Render Style (fill+stroke, wireframe, dashed, watercolor, etc.)
|
|
25
|
+
│ ├─ Render Style (fill+stroke, wireframe, dashed, watercolor, hatched, incomplete, etc.)
|
|
24
26
|
│ ├─ Position (composition mode + focal bias + density check)
|
|
25
27
|
│ ├─ Shape Selection (layer-weighted)
|
|
26
28
|
│ ├─ Atmospheric Depth (desaturation on later layers)
|
|
29
|
+
│ ├─ Temperature Contrast (foreground opposite to background)
|
|
27
30
|
│ ├─ Styling (transparency, glow, gradients, color jitter)
|
|
28
31
|
│ ├─ Organic Edges (~15% watercolor bleed)
|
|
29
32
|
│ └─ Recursive Nesting (~15% of large shapes)
|
|
30
33
|
6. Flow-Line Pass (tapered brush strokes)
|
|
34
|
+
6b. Symmetry Mirroring (bilateral-x, bilateral-y, or quad)
|
|
31
35
|
7. Noise Texture Overlay
|
|
32
|
-
8.
|
|
36
|
+
8. Vignette (radial edge darkening)
|
|
37
|
+
9. Organic Connecting Curves
|
|
33
38
|
```
|
|
34
39
|
|
|
35
40
|
## 1. Deterministic RNG
|
|
@@ -53,7 +58,19 @@ The `SacredColorScheme` class derives three harmonious palettes from the hash:
|
|
|
53
58
|
| Complementary | hue = seed + 180°, contrasting variation | Contrast accents |
|
|
54
59
|
| Triadic | hue = seed + 120° | Additional variety |
|
|
55
60
|
|
|
56
|
-
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.
|
|
61
|
+
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
|
+
|
|
63
|
+
### Temperature Contrast
|
|
64
|
+
|
|
65
|
+
The hash deterministically selects a **temperature mode** that creates warm/cool tension across the image:
|
|
66
|
+
|
|
67
|
+
| Mode | Probability | Background | Foreground |
|
|
68
|
+
|------|-------------|------------|------------|
|
|
69
|
+
| `warm-bg` | ~40% | Hues shifted toward orange (30°) | Hues shifted toward blue (210°) |
|
|
70
|
+
| `cool-bg` | ~40% | Hues shifted toward blue (210°) | Hues shifted toward orange (30°) |
|
|
71
|
+
| `neutral` | ~20% | No temperature shift | No temperature shift |
|
|
72
|
+
|
|
73
|
+
The shift amount is subtle (15-25%) and increases on later layers, creating progressive temperature separation between foreground and background elements. This produces the kind of warm/cool interplay seen in classical painting.
|
|
57
74
|
|
|
58
75
|
### Hash-Driven Color Variation
|
|
59
76
|
|
|
@@ -75,6 +92,7 @@ Scheme types also vary: `analogic`, `mono`, `contrast`, `triade`, `tetrade`. The
|
|
|
75
92
|
- **`hexWithAlpha(hex, alpha)`** — converts hex to `rgba()` for transparency
|
|
76
93
|
- **`jitterColor(hex, rng, amount)`** — applies ±amount RGB jitter per channel for organic variation
|
|
77
94
|
- **`desaturate(hex, amount)`** — blends toward luminance gray for atmospheric depth
|
|
95
|
+
- **`shiftTemperature(hex, target, amount)`** — shifts hue toward warm (orange) or cool (blue)
|
|
78
96
|
- **Positional blending** — shape fill color is biased by canvas position, creating smooth color flow across the image
|
|
79
97
|
|
|
80
98
|
## 3. Background
|
|
@@ -104,9 +122,31 @@ The hash deterministically selects one of five composition strategies that contr
|
|
|
104
122
|
|
|
105
123
|
Each mode produces fundamentally different visual character from the same shape set.
|
|
106
124
|
|
|
125
|
+
### Symmetry Modes
|
|
126
|
+
|
|
127
|
+
~25% of hashes trigger a symmetry mode that mirrors the rendered content:
|
|
128
|
+
|
|
129
|
+
| Mode | Probability | Effect |
|
|
130
|
+
|------|-------------|--------|
|
|
131
|
+
| `bilateral-x` | 10% | Left half mirrored onto right half |
|
|
132
|
+
| `bilateral-y` | 10% | Top half mirrored onto bottom half |
|
|
133
|
+
| `quad` | 5% | Both axes mirrored (4-fold symmetry) |
|
|
134
|
+
| `none` | 75% | No mirroring |
|
|
135
|
+
|
|
136
|
+
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
|
+
|
|
107
138
|
## 5. Focal Points & Negative Space
|
|
108
139
|
|
|
109
|
-
1-2 focal points are placed on the canvas (
|
|
140
|
+
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
|
+
|
|
142
|
+
### Hero Shape
|
|
143
|
+
|
|
144
|
+
~60% of images receive a **hero shape** — a large sacred or complex geometry piece anchored at the primary focal point. The hero shape:
|
|
145
|
+
- Uses sacred/complex shape types (flower of life, fibonacci spiral, merkaba, fractal, etc.)
|
|
146
|
+
- Is sized at 80-130% of the maximum shape size for visual dominance
|
|
147
|
+
- Gets glow effects, gradient fills, and often watercolor rendering
|
|
148
|
+
- Is drawn before the main shape layers so other shapes layer on top of it
|
|
149
|
+
- Creates a clear center of gravity that anchors the entire composition
|
|
110
150
|
|
|
111
151
|
### Void Zones (Negative Space)
|
|
112
152
|
|
|
@@ -142,12 +182,14 @@ Instead of always `fill()` + `stroke()`, each shape gets a rendering treatment:
|
|
|
142
182
|
|
|
143
183
|
| Style | Description | Probability |
|
|
144
184
|
|-------|-------------|-------------|
|
|
145
|
-
| `fill-and-stroke` | Classic solid fill with outline | ~
|
|
146
|
-
| `fill-only` | Soft shapes with no outline | ~
|
|
147
|
-
| `stroke-only` | Wireframe with ghost fill at 30% alpha | ~
|
|
148
|
-
| `double-stroke` | Outer stroke at 2× width + inner stroke in fill color | ~
|
|
149
|
-
| `dashed` | Dashed outline (5% size dash, 3% gap) | ~
|
|
150
|
-
| `watercolor` | 3-4 slightly offset passes at low opacity for bleed effect | ~
|
|
185
|
+
| `fill-and-stroke` | Classic solid fill with outline | ~22% (weighted) |
|
|
186
|
+
| `fill-only` | Soft shapes with no outline | ~11% |
|
|
187
|
+
| `stroke-only` | Wireframe with ghost fill at 30% alpha | ~11% |
|
|
188
|
+
| `double-stroke` | Outer stroke at 2× width + inner stroke in fill color | ~11% |
|
|
189
|
+
| `dashed` | Dashed outline (5% size dash, 3% gap) | ~11% |
|
|
190
|
+
| `watercolor` | 3-4 slightly offset passes at low opacity for bleed effect | ~11% |
|
|
191
|
+
| `hatched` | Cross-hatch texture fill clipped to shape boundary | ~11% |
|
|
192
|
+
| `incomplete` | Only 60-85% of outline drawn via dash patterns | ~11% |
|
|
151
193
|
|
|
152
194
|
70% of shapes in a layer use the layer's dominant style; 30% pick independently. Additionally, ~15% of `fill-and-stroke` shapes are upgraded to `watercolor` for organic edge effects.
|
|
153
195
|
|
|
@@ -213,6 +255,15 @@ A dedicated noise RNG (seeded separately from the main RNG to avoid affecting sh
|
|
|
213
255
|
- Very low opacity (1-4%)
|
|
214
256
|
- Creates subtle film-grain texture that adds organic depth
|
|
215
257
|
|
|
258
|
+
## 8b. Vignette
|
|
259
|
+
|
|
260
|
+
A radial gradient overlay darkens the edges of the canvas, drawing the viewer's eye toward the center:
|
|
261
|
+
|
|
262
|
+
- Strength varies by hash: 25-45% maximum edge darkening
|
|
263
|
+
- The vignette begins fading at 60% of the canvas radius from center
|
|
264
|
+
- Applied after noise but before connecting curves, so the curves remain visible at edges
|
|
265
|
+
- Creates a natural "spotlight" effect that makes compositions feel more focused and photographic
|
|
266
|
+
|
|
216
267
|
## 9. Organic Connecting Curves
|
|
217
268
|
|
|
218
269
|
Quadratic bezier curves connect nearby shapes:
|
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +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.5.0](https://github.com/gfargo/git-hash-art/compare/0.4.1...0.5.0)
|
|
8
|
+
|
|
9
|
+
- feat: visual improvements — composition, color, and rendering upgrades [`#11`](https://github.com/gfargo/git-hash-art/pull/11)
|
|
10
|
+
- feat: warm/cool temperature contrast in color system [`550bc6a`](https://github.com/gfargo/git-hash-art/commit/550bc6a87929d7ebafa973079845e688a00de929)
|
|
11
|
+
- docs: update ALGORITHM.md with all new visual features [`a05de15`](https://github.com/gfargo/git-hash-art/commit/a05de1586684b91266f5a26ea7279fc601dd2202)
|
|
12
|
+
- feat: hatched and incomplete render styles for shape variety [`4f42f7e`](https://github.com/gfargo/git-hash-art/commit/4f42f7e30bac60239b69468e36ef5481dca5d007)
|
|
13
|
+
|
|
7
14
|
#### [0.4.1](https://github.com/gfargo/git-hash-art/compare/0.4.0...0.4.1)
|
|
8
15
|
|
|
16
|
+
> 19 March 2026
|
|
17
|
+
|
|
18
|
+
- chore: release v0.4.1 [`c453061`](https://github.com/gfargo/git-hash-art/commit/c4530616bc40a540ce4d87a8124a1d2665f77376)
|
|
9
19
|
- update README with new feature details [`1e3fcd7`](https://github.com/gfargo/git-hash-art/commit/1e3fcd771ea602b21c9c90cd3809d0d08ace5d3f)
|
|
10
20
|
- update color variations and complementary logic [`9c8f4ba`](https://github.com/gfargo/git-hash-art/commit/9c8f4ba51e1a5730e4c4ddd59218db23d4719129)
|
|
11
21
|
|
package/dist/browser.js
CHANGED
|
@@ -109,6 +109,27 @@ const $b5a262d09b87e373$var$COLOR_VARIATIONS = [
|
|
|
109
109
|
function $b5a262d09b87e373$var$pickSchemeType(seed) {
|
|
110
110
|
return $b5a262d09b87e373$var$SCHEME_TYPES[Math.abs(seed >> 4) % $b5a262d09b87e373$var$SCHEME_TYPES.length];
|
|
111
111
|
}
|
|
112
|
+
function $b5a262d09b87e373$var$classifyHue(hue) {
|
|
113
|
+
if (hue >= 0 && hue <= 60 || hue >= 300) return "warm";
|
|
114
|
+
if (hue >= 150 && hue <= 270) return "cool";
|
|
115
|
+
return "neutral";
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Shift a hue toward a target temperature zone.
|
|
119
|
+
* Returns a new hue biased warm or cool.
|
|
120
|
+
*/ function $b5a262d09b87e373$var$shiftHueToward(hue, target, amount) {
|
|
121
|
+
if (target === "warm") {
|
|
122
|
+
// Pull toward 30 (orange) — the warmest point
|
|
123
|
+
const warmTarget = 30;
|
|
124
|
+
const diff = (warmTarget - hue + 540) % 360 - 180;
|
|
125
|
+
return (hue + diff * amount + 360) % 360;
|
|
126
|
+
} else {
|
|
127
|
+
// Pull toward 210 (blue) — the coolest point
|
|
128
|
+
const coolTarget = 210;
|
|
129
|
+
const diff = (coolTarget - hue + 540) % 360 - 180;
|
|
130
|
+
return (hue + diff * amount + 360) % 360;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
112
133
|
class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
113
134
|
constructor(gitHash){
|
|
114
135
|
this.seed = (0, $616009579e3d72c5$export$39a95c82b20fdf81)(gitHash);
|
|
@@ -116,6 +137,9 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
116
137
|
// Hash-driven variation and scheme type for palette diversity
|
|
117
138
|
this.variation = $b5a262d09b87e373$var$pickVariation(this.seed);
|
|
118
139
|
this.schemeType = $b5a262d09b87e373$var$pickSchemeType(this.seed);
|
|
140
|
+
// ~40% warm-bg, ~40% cool-bg, ~20% neutral (no temperature bias)
|
|
141
|
+
const tempRoll = this.rng();
|
|
142
|
+
this.temperatureMode = tempRoll < 0.4 ? "warm-bg" : tempRoll < 0.8 ? "cool-bg" : "neutral";
|
|
119
143
|
this.baseScheme = this.generateBaseScheme();
|
|
120
144
|
this.complementaryScheme = this.generateComplementaryScheme();
|
|
121
145
|
this.triadicScheme = this.generateTriadicScheme();
|
|
@@ -153,14 +177,34 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
153
177
|
}
|
|
154
178
|
/**
|
|
155
179
|
* Returns two background colors derived from the hash — darker variants
|
|
156
|
-
* of the base scheme for
|
|
180
|
+
* of the base scheme, temperature-shifted for warm/cool contrast.
|
|
157
181
|
*/ getBackgroundColors() {
|
|
182
|
+
let bg0 = this.baseScheme[0];
|
|
183
|
+
let bg1 = this.baseScheme[1];
|
|
184
|
+
if (this.temperatureMode !== "neutral") {
|
|
185
|
+
const bgTemp = this.temperatureMode === "warm-bg" ? "warm" : "cool";
|
|
186
|
+
bg0 = this.shiftColorTemperature(bg0, bgTemp, 0.3);
|
|
187
|
+
bg1 = this.shiftColorTemperature(bg1, bgTemp, 0.25);
|
|
188
|
+
}
|
|
158
189
|
return [
|
|
159
|
-
this.darken(
|
|
160
|
-
this.darken(
|
|
190
|
+
this.darken(bg0, 0.65),
|
|
191
|
+
this.darken(bg1, 0.55)
|
|
161
192
|
];
|
|
162
193
|
}
|
|
163
194
|
/**
|
|
195
|
+
* Returns the temperature mode so the renderer can apply
|
|
196
|
+
* contrasting temperature to foreground elements.
|
|
197
|
+
*/ getTemperatureMode() {
|
|
198
|
+
return this.temperatureMode;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Shift a hex color's hue toward warm or cool.
|
|
202
|
+
*/ shiftColorTemperature(hex, target, amount) {
|
|
203
|
+
const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
|
|
204
|
+
const shifted = $b5a262d09b87e373$var$shiftHueToward(h, target, amount);
|
|
205
|
+
return $b5a262d09b87e373$var$hslToHex(shifted, s, l);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
164
208
|
* Simple hex color darkening by a factor (0 = black, 1 = unchanged).
|
|
165
209
|
*/ darken(hex, factor) {
|
|
166
210
|
const c = hex.replace("#", "");
|
|
@@ -183,6 +227,55 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
183
227
|
const clamp = (v)=>Math.max(0, Math.min(255, Math.round(v)));
|
|
184
228
|
return `#${clamp(r).toString(16).padStart(2, "0")}${clamp(g).toString(16).padStart(2, "0")}${clamp(b).toString(16).padStart(2, "0")}`;
|
|
185
229
|
}
|
|
230
|
+
/** Convert hex to HSL [h 0-360, s 0-1, l 0-1]. */ function $b5a262d09b87e373$var$hexToHsl(hex) {
|
|
231
|
+
const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex).map((v)=>v / 255);
|
|
232
|
+
const max = Math.max(r, g, b);
|
|
233
|
+
const min = Math.min(r, g, b);
|
|
234
|
+
const l = (max + min) / 2;
|
|
235
|
+
if (max === min) return [
|
|
236
|
+
0,
|
|
237
|
+
0,
|
|
238
|
+
l
|
|
239
|
+
];
|
|
240
|
+
const d = max - min;
|
|
241
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
242
|
+
let h = 0;
|
|
243
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
244
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
245
|
+
else h = ((r - g) / d + 4) / 6;
|
|
246
|
+
return [
|
|
247
|
+
h * 360,
|
|
248
|
+
s,
|
|
249
|
+
l
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
/** Convert HSL [h 0-360, s 0-1, l 0-1] back to hex. */ function $b5a262d09b87e373$var$hslToHex(h, s, l) {
|
|
253
|
+
h = (h % 360 + 360) % 360;
|
|
254
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
255
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
256
|
+
const m = l - c / 2;
|
|
257
|
+
let r = 0, g = 0, b = 0;
|
|
258
|
+
if (h < 60) {
|
|
259
|
+
r = c;
|
|
260
|
+
g = x;
|
|
261
|
+
} else if (h < 120) {
|
|
262
|
+
r = x;
|
|
263
|
+
g = c;
|
|
264
|
+
} else if (h < 180) {
|
|
265
|
+
g = c;
|
|
266
|
+
b = x;
|
|
267
|
+
} else if (h < 240) {
|
|
268
|
+
g = x;
|
|
269
|
+
b = c;
|
|
270
|
+
} else if (h < 300) {
|
|
271
|
+
r = x;
|
|
272
|
+
b = c;
|
|
273
|
+
} else {
|
|
274
|
+
r = c;
|
|
275
|
+
b = x;
|
|
276
|
+
}
|
|
277
|
+
return $b5a262d09b87e373$var$rgbToHex((r + m) * 255, (g + m) * 255, (b + m) * 255);
|
|
278
|
+
}
|
|
186
279
|
function $b5a262d09b87e373$export$f2121afcad3d553f(hex, alpha) {
|
|
187
280
|
const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex);
|
|
188
281
|
return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
|
|
@@ -198,6 +291,10 @@ function $b5a262d09b87e373$export$fb75607d98509d9(hex, amount) {
|
|
|
198
291
|
const mix = (c)=>c + (gray - c) * amount;
|
|
199
292
|
return $b5a262d09b87e373$var$rgbToHex(mix(r), mix(g), mix(b));
|
|
200
293
|
}
|
|
294
|
+
function $b5a262d09b87e373$export$51ea55f869b7e0d3(hex, target, amount) {
|
|
295
|
+
const [h, s, l] = $b5a262d09b87e373$var$hexToHsl(hex);
|
|
296
|
+
return $b5a262d09b87e373$var$hslToHex($b5a262d09b87e373$var$shiftHueToward(h, target, amount), s, l);
|
|
297
|
+
}
|
|
201
298
|
|
|
202
299
|
|
|
203
300
|
|
|
@@ -1006,7 +1103,9 @@ const $e0f99502ff383dd8$var$RENDER_STYLES = [
|
|
|
1006
1103
|
"stroke-only",
|
|
1007
1104
|
"double-stroke",
|
|
1008
1105
|
"dashed",
|
|
1009
|
-
"watercolor"
|
|
1106
|
+
"watercolor",
|
|
1107
|
+
"hatched",
|
|
1108
|
+
"incomplete"
|
|
1010
1109
|
];
|
|
1011
1110
|
function $e0f99502ff383dd8$export$9fd4e64b2acd410e(rng) {
|
|
1012
1111
|
return $e0f99502ff383dd8$var$RENDER_STYLES[Math.floor(rng() * $e0f99502ff383dd8$var$RENDER_STYLES.length)];
|
|
@@ -1083,6 +1182,76 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
1083
1182
|
ctx.globalAlpha /= 0.4;
|
|
1084
1183
|
break;
|
|
1085
1184
|
}
|
|
1185
|
+
case "hatched":
|
|
1186
|
+
{
|
|
1187
|
+
// Fill normally at reduced opacity, then overlay cross-hatch lines
|
|
1188
|
+
const savedAlphaH = ctx.globalAlpha;
|
|
1189
|
+
ctx.globalAlpha = savedAlphaH * 0.3;
|
|
1190
|
+
ctx.fill();
|
|
1191
|
+
ctx.globalAlpha = savedAlphaH;
|
|
1192
|
+
// Clip to shape, then draw hatch lines
|
|
1193
|
+
ctx.save();
|
|
1194
|
+
ctx.clip();
|
|
1195
|
+
const hatchSpacing = Math.max(3, size * 0.06);
|
|
1196
|
+
const hatchAngle = rng ? rng() * Math.PI : Math.PI / 4;
|
|
1197
|
+
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.4);
|
|
1198
|
+
ctx.globalAlpha = savedAlphaH * 0.6;
|
|
1199
|
+
// Draw parallel lines across the bounding box
|
|
1200
|
+
const extent = size * 0.8;
|
|
1201
|
+
const cos = Math.cos(hatchAngle);
|
|
1202
|
+
const sin = Math.sin(hatchAngle);
|
|
1203
|
+
for(let d = -extent; d <= extent; d += hatchSpacing){
|
|
1204
|
+
ctx.beginPath();
|
|
1205
|
+
ctx.moveTo(d * cos - extent * sin, d * sin + extent * cos);
|
|
1206
|
+
ctx.lineTo(d * cos + extent * sin, d * sin - extent * cos);
|
|
1207
|
+
ctx.stroke();
|
|
1208
|
+
}
|
|
1209
|
+
// Second pass at perpendicular angle for cross-hatch (~50% chance)
|
|
1210
|
+
if (!rng || rng() < 0.5) {
|
|
1211
|
+
const crossAngle = hatchAngle + Math.PI / 2;
|
|
1212
|
+
const cos2 = Math.cos(crossAngle);
|
|
1213
|
+
const sin2 = Math.sin(crossAngle);
|
|
1214
|
+
ctx.globalAlpha = savedAlphaH * 0.35;
|
|
1215
|
+
for(let d = -extent; d <= extent; d += hatchSpacing * 1.4){
|
|
1216
|
+
ctx.beginPath();
|
|
1217
|
+
ctx.moveTo(d * cos2 - extent * sin2, d * sin2 + extent * cos2);
|
|
1218
|
+
ctx.lineTo(d * cos2 + extent * sin2, d * sin2 - extent * cos2);
|
|
1219
|
+
ctx.stroke();
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
ctx.restore();
|
|
1223
|
+
ctx.globalAlpha = savedAlphaH;
|
|
1224
|
+
// Outline stroke
|
|
1225
|
+
ctx.globalAlpha *= 0.5;
|
|
1226
|
+
ctx.stroke();
|
|
1227
|
+
ctx.globalAlpha /= 0.5;
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
case "incomplete":
|
|
1231
|
+
{
|
|
1232
|
+
// Draw the fill at low opacity, then a dashed stroke that
|
|
1233
|
+
// simulates drawing only part of the outline
|
|
1234
|
+
const savedAlphaI = ctx.globalAlpha;
|
|
1235
|
+
ctx.globalAlpha = savedAlphaI * 0.25;
|
|
1236
|
+
ctx.fill();
|
|
1237
|
+
ctx.globalAlpha = savedAlphaI;
|
|
1238
|
+
// Use a long dash pattern where gaps create the "incomplete" look
|
|
1239
|
+
const completeness = rng ? 0.6 + rng() * 0.25 : 0.7; // 60-85%
|
|
1240
|
+
const segLen = size * 0.12;
|
|
1241
|
+
const gapLen = segLen * ((1 - completeness) / completeness);
|
|
1242
|
+
ctx.setLineDash([
|
|
1243
|
+
segLen,
|
|
1244
|
+
gapLen
|
|
1245
|
+
]);
|
|
1246
|
+
// Offset the dash so each shape starts at a different point
|
|
1247
|
+
ctx.lineDashOffset = rng ? rng() * segLen * 4 : 0;
|
|
1248
|
+
// Slightly thicker stroke for hand-drawn feel
|
|
1249
|
+
ctx.lineWidth = strokeWidth * 1.3;
|
|
1250
|
+
ctx.stroke();
|
|
1251
|
+
ctx.setLineDash([]);
|
|
1252
|
+
ctx.lineDashOffset = 0;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1086
1255
|
case "fill-and-stroke":
|
|
1087
1256
|
default:
|
|
1088
1257
|
ctx.fill();
|
|
@@ -1288,6 +1457,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
1288
1457
|
const colorScheme = new (0, $b5a262d09b87e373$export$ab958c550f521376)(gitHash);
|
|
1289
1458
|
const colors = colorScheme.getColors();
|
|
1290
1459
|
const [bgStart, bgEnd] = colorScheme.getBackgroundColors();
|
|
1460
|
+
const tempMode = colorScheme.getTemperatureMode();
|
|
1461
|
+
// Foreground shapes get the opposite temperature for contrast
|
|
1462
|
+
const fgTempTarget = tempMode === "warm-bg" ? "cool" : tempMode === "cool-bg" ? "warm" : null;
|
|
1291
1463
|
const shapeNames = Object.keys((0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5));
|
|
1292
1464
|
const scaleFactor = Math.min(width, height) / 1024;
|
|
1293
1465
|
const adjustedMinSize = minShapeSize * scaleFactor;
|
|
@@ -1331,10 +1503,40 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
1331
1503
|
ctx.globalCompositeOperation = "source-over";
|
|
1332
1504
|
// ── 2. Composition mode ────────────────────────────────────────
|
|
1333
1505
|
const compositionMode = $1f63dc64b5593c73$var$COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$COMPOSITION_MODES.length)];
|
|
1506
|
+
const symRoll = rng();
|
|
1507
|
+
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
1334
1508
|
// ── 3. Focal points + void zones ───────────────────────────────
|
|
1509
|
+
// Rule-of-thirds intersection points for intentional composition
|
|
1510
|
+
const THIRDS_POINTS = [
|
|
1511
|
+
{
|
|
1512
|
+
x: 1 / 3,
|
|
1513
|
+
y: 1 / 3
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
x: 2 / 3,
|
|
1517
|
+
y: 1 / 3
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
x: 1 / 3,
|
|
1521
|
+
y: 2 / 3
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
x: 2 / 3,
|
|
1525
|
+
y: 2 / 3
|
|
1526
|
+
}
|
|
1527
|
+
];
|
|
1335
1528
|
const numFocal = 1 + Math.floor(rng() * 2);
|
|
1336
1529
|
const focalPoints = [];
|
|
1337
|
-
for(let f = 0; f < numFocal; f++)
|
|
1530
|
+
for(let f = 0; f < numFocal; f++)// 70% chance to snap to a rule-of-thirds point, 30% free placement
|
|
1531
|
+
if (rng() < 0.7) {
|
|
1532
|
+
const tp = THIRDS_POINTS[Math.floor(rng() * THIRDS_POINTS.length)];
|
|
1533
|
+
// Small jitter around the thirds point so it's not robotic
|
|
1534
|
+
focalPoints.push({
|
|
1535
|
+
x: width * (tp.x + (rng() - 0.5) * 0.08),
|
|
1536
|
+
y: height * (tp.y + (rng() - 0.5) * 0.08),
|
|
1537
|
+
strength: 0.3 + rng() * 0.4
|
|
1538
|
+
});
|
|
1539
|
+
} else focalPoints.push({
|
|
1338
1540
|
x: width * (0.2 + rng() * 0.6),
|
|
1339
1541
|
y: height * (0.2 + rng() * 0.6),
|
|
1340
1542
|
strength: 0.3 + rng() * 0.4
|
|
@@ -1369,8 +1571,45 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
1369
1571
|
function flowAngle(x, y) {
|
|
1370
1572
|
return fieldAngleBase + Math.sin(x / width * fieldFreq * Math.PI * 2) * Math.PI * 0.5 + Math.cos(y / height * fieldFreq * Math.PI * 2) * Math.PI * 0.5;
|
|
1371
1573
|
}
|
|
1372
|
-
//
|
|
1574
|
+
// Track all placed shapes for density checks and connecting curves
|
|
1373
1575
|
const shapePositions = [];
|
|
1576
|
+
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
1577
|
+
// ~60% of images get a hero shape anchored at the primary focal point.
|
|
1578
|
+
// It's a large sacred/complex shape that gives the composition a center of gravity.
|
|
1579
|
+
if (rng() < 0.6) {
|
|
1580
|
+
const heroFocal = focalPoints[0];
|
|
1581
|
+
const heroPool = [
|
|
1582
|
+
...$1f63dc64b5593c73$var$SACRED_SHAPES,
|
|
1583
|
+
"fibonacciSpiral",
|
|
1584
|
+
"merkaba",
|
|
1585
|
+
"fractal"
|
|
1586
|
+
];
|
|
1587
|
+
const heroShape = heroPool.filter((s)=>shapeNames.includes(s))[Math.floor(rng() * heroPool.filter((s)=>shapeNames.includes(s)).length)] || shapeNames[Math.floor(rng() * shapeNames.length)];
|
|
1588
|
+
const heroSize = adjustedMaxSize * (0.8 + rng() * 0.5);
|
|
1589
|
+
const heroRotation = rng() * 360;
|
|
1590
|
+
const heroFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05), 0.15 + rng() * 0.2);
|
|
1591
|
+
const heroStroke = (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.05);
|
|
1592
|
+
ctx.globalAlpha = 0.5 + rng() * 0.2;
|
|
1593
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, heroShape, heroFocal.x, heroFocal.y, {
|
|
1594
|
+
fillColor: heroFill,
|
|
1595
|
+
strokeColor: heroStroke,
|
|
1596
|
+
strokeWidth: (1.5 + rng() * 2) * scaleFactor,
|
|
1597
|
+
size: heroSize,
|
|
1598
|
+
rotation: heroRotation,
|
|
1599
|
+
proportionType: "GOLDEN_RATIO",
|
|
1600
|
+
glowRadius: (12 + rng() * 20) * scaleFactor,
|
|
1601
|
+
glowColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(heroStroke, 0.4),
|
|
1602
|
+
gradientFillEnd: (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1),
|
|
1603
|
+
renderStyle: rng() < 0.4 ? "watercolor" : "fill-and-stroke",
|
|
1604
|
+
rng: rng
|
|
1605
|
+
});
|
|
1606
|
+
shapePositions.push({
|
|
1607
|
+
x: heroFocal.x,
|
|
1608
|
+
y: heroFocal.y,
|
|
1609
|
+
size: heroSize
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
// ── 5. Shape layers ────────────────────────────────────────────
|
|
1374
1613
|
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
1375
1614
|
const maxLocalDensity = Math.ceil(finalConfig.shapesPerLayer * 0.15);
|
|
1376
1615
|
for(let layer = 0; layer < layers; layer++){
|
|
@@ -1409,6 +1648,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
1409
1648
|
const strokeBase = colors[Math.floor(rng() * colors.length)];
|
|
1410
1649
|
// Feature D: desaturate colors on later layers for depth
|
|
1411
1650
|
if (atmosphericDesat > 0) fillBase = (0, $b5a262d09b87e373$export$fb75607d98509d9)(fillBase, atmosphericDesat);
|
|
1651
|
+
// Temperature contrast: shift foreground shapes opposite to background
|
|
1652
|
+
if (fgTempTarget) fillBase = (0, $b5a262d09b87e373$export$51ea55f869b7e0d3)(fillBase, fgTempTarget, 0.15 + layerRatio * 0.1);
|
|
1412
1653
|
const fillColor = (0, $b5a262d09b87e373$export$59539d800dbe6858)(fillBase, rng, 0.06);
|
|
1413
1654
|
const strokeColor = (0, $b5a262d09b87e373$export$59539d800dbe6858)(strokeBase, rng, 0.05);
|
|
1414
1655
|
// Semi-transparent fill
|
|
@@ -1506,6 +1747,31 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
1506
1747
|
prevY = fy;
|
|
1507
1748
|
}
|
|
1508
1749
|
}
|
|
1750
|
+
// ── 6b. Apply symmetry mirroring ─────────────────────────────────
|
|
1751
|
+
// Mirror the rendered content (shapes + flow lines) before post-processing.
|
|
1752
|
+
// Uses ctx.canvas which is available in both Node (@napi-rs/canvas) and browsers.
|
|
1753
|
+
if (symmetryMode !== "none") {
|
|
1754
|
+
const canvas = ctx.canvas;
|
|
1755
|
+
ctx.save();
|
|
1756
|
+
if (symmetryMode === "bilateral-x" || symmetryMode === "quad") {
|
|
1757
|
+
// Mirror left half onto right half
|
|
1758
|
+
ctx.save();
|
|
1759
|
+
ctx.translate(width, 0);
|
|
1760
|
+
ctx.scale(-1, 1);
|
|
1761
|
+
// Draw the left half (0 to cx) onto the mirrored right side
|
|
1762
|
+
ctx.drawImage(canvas, 0, 0, Math.ceil(cx), height, 0, 0, Math.ceil(cx), height);
|
|
1763
|
+
ctx.restore();
|
|
1764
|
+
}
|
|
1765
|
+
if (symmetryMode === "bilateral-y" || symmetryMode === "quad") {
|
|
1766
|
+
// Mirror top half onto bottom half
|
|
1767
|
+
ctx.save();
|
|
1768
|
+
ctx.translate(0, height);
|
|
1769
|
+
ctx.scale(1, -1);
|
|
1770
|
+
ctx.drawImage(canvas, 0, 0, width, Math.ceil(cy), 0, 0, width, Math.ceil(cy));
|
|
1771
|
+
ctx.restore();
|
|
1772
|
+
}
|
|
1773
|
+
ctx.restore();
|
|
1774
|
+
}
|
|
1509
1775
|
// ── 7. Noise texture overlay ───────────────────────────────────
|
|
1510
1776
|
const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
|
|
1511
1777
|
const noiseDensity = Math.floor(width * height / 800);
|
|
@@ -1518,7 +1784,16 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
1518
1784
|
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
1519
1785
|
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
1520
1786
|
}
|
|
1521
|
-
// ── 8.
|
|
1787
|
+
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
1788
|
+
ctx.globalAlpha = 1;
|
|
1789
|
+
const vignetteStrength = 0.25 + rng() * 0.2; // 25-45% edge darkening
|
|
1790
|
+
const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
|
|
1791
|
+
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
1792
|
+
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
1793
|
+
vigGrad.addColorStop(1, `rgba(0,0,0,${vignetteStrength.toFixed(3)})`);
|
|
1794
|
+
ctx.fillStyle = vigGrad;
|
|
1795
|
+
ctx.fillRect(0, 0, width, height);
|
|
1796
|
+
// ── 9. Organic connecting curves ───────────────────────────────
|
|
1522
1797
|
if (shapePositions.length > 1) {
|
|
1523
1798
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
1524
1799
|
ctx.lineWidth = 0.8 * scaleFactor;
|