git-hash-art 0.10.0 → 0.10.1
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/.github/workflows/deploy-www.yml +47 -0
- package/ALGORITHM.md +108 -8
- package/CHANGELOG.md +10 -0
- package/dist/browser.js +518 -13
- package/dist/browser.js.map +1 -1
- package/dist/main.js +518 -13
- package/dist/main.js.map +1 -1
- package/dist/module.js +518 -13
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/archetypes.ts +4 -1
- package/src/lib/canvas/colors.ts +45 -0
- package/src/lib/canvas/draw.ts +84 -5
- package/src/lib/render.ts +255 -10
- package/src/lib/utils.ts +109 -0
package/dist/browser.js
CHANGED
|
@@ -61,6 +61,136 @@ const $616009579e3d72c5$export$bb9e4790bc99ae59 = {
|
|
|
61
61
|
PI: Math.PI,
|
|
62
62
|
PHI: (1 + Math.sqrt(5)) / 2
|
|
63
63
|
};
|
|
64
|
+
function $616009579e3d72c5$export$bbde7fbaaf9a8d66(rng) {
|
|
65
|
+
// Build a deterministic permutation table (256 entries, doubled)
|
|
66
|
+
const perm = new Uint8Array(512);
|
|
67
|
+
const p = new Uint8Array(256);
|
|
68
|
+
for(let i = 0; i < 256; i++)p[i] = i;
|
|
69
|
+
// Fisher-Yates shuffle with our seeded RNG
|
|
70
|
+
for(let i = 255; i > 0; i--){
|
|
71
|
+
const j = Math.floor(rng() * (i + 1));
|
|
72
|
+
const tmp = p[i];
|
|
73
|
+
p[i] = p[j];
|
|
74
|
+
p[j] = tmp;
|
|
75
|
+
}
|
|
76
|
+
for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
|
|
77
|
+
// 12 gradient vectors for 2D simplex
|
|
78
|
+
const GRAD2 = [
|
|
79
|
+
[
|
|
80
|
+
1,
|
|
81
|
+
1
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
-1,
|
|
85
|
+
1
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
1,
|
|
89
|
+
-1
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
-1,
|
|
93
|
+
-1
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
1,
|
|
97
|
+
0
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
-1,
|
|
101
|
+
0
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
0,
|
|
105
|
+
1
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
0,
|
|
109
|
+
-1
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
1,
|
|
113
|
+
1
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
-1,
|
|
117
|
+
1
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
1,
|
|
121
|
+
-1
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
-1,
|
|
125
|
+
-1
|
|
126
|
+
]
|
|
127
|
+
];
|
|
128
|
+
const F2 = 0.5 * (Math.sqrt(3) - 1);
|
|
129
|
+
const G2 = (3 - Math.sqrt(3)) / 6;
|
|
130
|
+
function dot2(g, x, y) {
|
|
131
|
+
return g[0] * x + g[1] * y;
|
|
132
|
+
}
|
|
133
|
+
return function noise2D(xin, yin) {
|
|
134
|
+
const s = (xin + yin) * F2;
|
|
135
|
+
const i = Math.floor(xin + s);
|
|
136
|
+
const j = Math.floor(yin + s);
|
|
137
|
+
const t = (i + j) * G2;
|
|
138
|
+
const X0 = i - t;
|
|
139
|
+
const Y0 = j - t;
|
|
140
|
+
const x0 = xin - X0;
|
|
141
|
+
const y0 = yin - Y0;
|
|
142
|
+
let i1, j1;
|
|
143
|
+
if (x0 > y0) {
|
|
144
|
+
i1 = 1;
|
|
145
|
+
j1 = 0;
|
|
146
|
+
} else {
|
|
147
|
+
i1 = 0;
|
|
148
|
+
j1 = 1;
|
|
149
|
+
}
|
|
150
|
+
const x1 = x0 - i1 + G2;
|
|
151
|
+
const y1 = y0 - j1 + G2;
|
|
152
|
+
const x2 = x0 - 1 + 2 * G2;
|
|
153
|
+
const y2 = y0 - 1 + 2 * G2;
|
|
154
|
+
const ii = i & 255;
|
|
155
|
+
const jj = j & 255;
|
|
156
|
+
let n0 = 0, n1 = 0, n2 = 0;
|
|
157
|
+
let t0 = 0.5 - x0 * x0 - y0 * y0;
|
|
158
|
+
if (t0 >= 0) {
|
|
159
|
+
t0 *= t0;
|
|
160
|
+
const gi0 = perm[ii + perm[jj]] % 12;
|
|
161
|
+
n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
|
|
162
|
+
}
|
|
163
|
+
let t1 = 0.5 - x1 * x1 - y1 * y1;
|
|
164
|
+
if (t1 >= 0) {
|
|
165
|
+
t1 *= t1;
|
|
166
|
+
const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
|
|
167
|
+
n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
|
|
168
|
+
}
|
|
169
|
+
let t2 = 0.5 - x2 * x2 - y2 * y2;
|
|
170
|
+
if (t2 >= 0) {
|
|
171
|
+
t2 *= t2;
|
|
172
|
+
const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
|
|
173
|
+
n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
|
|
174
|
+
}
|
|
175
|
+
// Scale to approximately [-1, 1]
|
|
176
|
+
return 70 * (n0 + n1 + n2);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function $616009579e3d72c5$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
|
|
180
|
+
return function fbm(x, y) {
|
|
181
|
+
let value = 0;
|
|
182
|
+
let amplitude = 1;
|
|
183
|
+
let frequency = 1;
|
|
184
|
+
let maxAmp = 0;
|
|
185
|
+
for(let i = 0; i < octaves; i++){
|
|
186
|
+
value += noise(x * frequency, y * frequency) * amplitude;
|
|
187
|
+
maxAmp += amplitude;
|
|
188
|
+
amplitude *= gain;
|
|
189
|
+
frequency *= lacunarity;
|
|
190
|
+
}
|
|
191
|
+
return value / maxAmp;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
64
194
|
class $616009579e3d72c5$export$da2372f11bc66b3f {
|
|
65
195
|
static getProportionalSize(baseSize, proportion) {
|
|
66
196
|
return baseSize * proportion;
|
|
@@ -254,6 +384,48 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
254
384
|
$b5a262d09b87e373$var$hslToHex(baseHue, 0.7, 0.35)
|
|
255
385
|
];
|
|
256
386
|
}
|
|
387
|
+
case "split-complementary":
|
|
388
|
+
{
|
|
389
|
+
// Base hue + two colors flanking the complement (±30°)
|
|
390
|
+
const comp = (baseHue + 180) % 360;
|
|
391
|
+
const split1 = (comp - 30 + 360) % 360;
|
|
392
|
+
const split2 = (comp + 30) % 360;
|
|
393
|
+
const sat = 0.55 + this.rng() * 0.25;
|
|
394
|
+
return [
|
|
395
|
+
$b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
|
|
396
|
+
$b5a262d09b87e373$var$hslToHex(baseHue, sat * 0.8, 0.65),
|
|
397
|
+
$b5a262d09b87e373$var$hslToHex(split1, sat, 0.5),
|
|
398
|
+
$b5a262d09b87e373$var$hslToHex(split2, sat, 0.5),
|
|
399
|
+
$b5a262d09b87e373$var$hslToHex(split1, sat * 0.7, 0.7)
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
case "analogous-accent":
|
|
403
|
+
{
|
|
404
|
+
// Tight cluster of 3 analogous hues + 1 distant accent
|
|
405
|
+
const step = 15 + this.rng() * 20; // 15-35° apart
|
|
406
|
+
const h1 = (baseHue - step + 360) % 360;
|
|
407
|
+
const h2 = (baseHue + step) % 360;
|
|
408
|
+
const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
|
|
409
|
+
const sat = 0.5 + this.rng() * 0.3;
|
|
410
|
+
return [
|
|
411
|
+
$b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
|
|
412
|
+
$b5a262d09b87e373$var$hslToHex(h1, sat, 0.55),
|
|
413
|
+
$b5a262d09b87e373$var$hslToHex(h2, sat, 0.45),
|
|
414
|
+
$b5a262d09b87e373$var$hslToHex(accentHue, sat + 0.15, 0.5)
|
|
415
|
+
];
|
|
416
|
+
}
|
|
417
|
+
case "limited-palette":
|
|
418
|
+
{
|
|
419
|
+
// Only 3 colors — like a risograph print
|
|
420
|
+
const h2 = (baseHue + 120 + this.rng() * 40) % 360;
|
|
421
|
+
const h3 = (baseHue + 220 + this.rng() * 40) % 360;
|
|
422
|
+
const sat = 0.6 + this.rng() * 0.2;
|
|
423
|
+
return [
|
|
424
|
+
$b5a262d09b87e373$var$hslToHex(baseHue, sat, 0.5),
|
|
425
|
+
$b5a262d09b87e373$var$hslToHex(h2, sat, 0.5),
|
|
426
|
+
$b5a262d09b87e373$var$hslToHex(h3, sat * 0.9, 0.55)
|
|
427
|
+
];
|
|
428
|
+
}
|
|
257
429
|
case "harmonious":
|
|
258
430
|
default:
|
|
259
431
|
return this.getColors();
|
|
@@ -274,6 +446,14 @@ class $b5a262d09b87e373$export$ab958c550f521376 {
|
|
|
274
446
|
"#f5f5f0",
|
|
275
447
|
"#e8e8e0"
|
|
276
448
|
];
|
|
449
|
+
case "split-complementary":
|
|
450
|
+
case "analogous-accent":
|
|
451
|
+
return this.getBackgroundColors();
|
|
452
|
+
case "limited-palette":
|
|
453
|
+
return [
|
|
454
|
+
$b5a262d09b87e373$var$hslToHex(this.seed % 360, 0.08, 0.94),
|
|
455
|
+
$b5a262d09b87e373$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
|
|
456
|
+
];
|
|
277
457
|
case "neon":
|
|
278
458
|
return [
|
|
279
459
|
"#0a0a12",
|
|
@@ -527,7 +707,8 @@ function $b5a262d09b87e373$export$703ba40a4347f77a(base, layerRatio, hueShiftPer
|
|
|
527
707
|
return {
|
|
528
708
|
dominant: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.dominant, shift),
|
|
529
709
|
secondary: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
|
|
530
|
-
accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
|
|
710
|
+
accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
|
|
711
|
+
all: base.all.map((c)=>$b5a262d09b87e373$export$1793a1bfbe4f6ff5(c, shift * 0.6))
|
|
531
712
|
};
|
|
532
713
|
}
|
|
533
714
|
|
|
@@ -1910,6 +2091,23 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
1910
2091
|
ctx.fill();
|
|
1911
2092
|
ctx.fillStyle = origFill;
|
|
1912
2093
|
ctx.restore();
|
|
2094
|
+
// Pass 4: Organic edge erosion — irregular bites along the boundary
|
|
2095
|
+
if (rng && size > 20) {
|
|
2096
|
+
const erosionBites = 6 + Math.floor(rng() * 8);
|
|
2097
|
+
const edgeRadius = size * 0.45;
|
|
2098
|
+
ctx.save();
|
|
2099
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
2100
|
+
ctx.globalAlpha = 0.6 + rng() * 0.3;
|
|
2101
|
+
for(let eb = 0; eb < erosionBites; eb++){
|
|
2102
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
2103
|
+
const biteDist = edgeRadius * (0.85 + rng() * 0.25);
|
|
2104
|
+
const biteR = size * (0.02 + rng() * 0.04);
|
|
2105
|
+
ctx.beginPath();
|
|
2106
|
+
ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
|
|
2107
|
+
ctx.fill();
|
|
2108
|
+
}
|
|
2109
|
+
ctx.restore();
|
|
2110
|
+
}
|
|
1913
2111
|
ctx.globalAlpha = savedAlpha;
|
|
1914
2112
|
// Soft stroke on top — thinner than normal for delicacy
|
|
1915
2113
|
ctx.globalAlpha *= 0.25;
|
|
@@ -2219,6 +2417,23 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2219
2417
|
ctx.stroke();
|
|
2220
2418
|
ctx.restore();
|
|
2221
2419
|
}
|
|
2420
|
+
// Organic edge erosion — small irregular bites for rough paper feel
|
|
2421
|
+
if (rng && size > 20) {
|
|
2422
|
+
const erosionBites = 4 + Math.floor(rng() * 6);
|
|
2423
|
+
const edgeRadius = size * 0.42;
|
|
2424
|
+
ctx.save();
|
|
2425
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
2426
|
+
ctx.globalAlpha = 0.5 + rng() * 0.3;
|
|
2427
|
+
for(let eb = 0; eb < erosionBites; eb++){
|
|
2428
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
2429
|
+
const biteDist = edgeRadius * (0.9 + rng() * 0.2);
|
|
2430
|
+
const biteR = size * (0.015 + rng() * 0.03);
|
|
2431
|
+
ctx.beginPath();
|
|
2432
|
+
ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
|
|
2433
|
+
ctx.fill();
|
|
2434
|
+
}
|
|
2435
|
+
ctx.restore();
|
|
2436
|
+
}
|
|
2222
2437
|
ctx.globalAlpha = savedAlphaHD;
|
|
2223
2438
|
break;
|
|
2224
2439
|
}
|
|
@@ -2230,12 +2445,20 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2230
2445
|
}
|
|
2231
2446
|
}
|
|
2232
2447
|
function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
2233
|
-
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, patterns: patterns = [], proportionType: proportionType = "GOLDEN_RATIO", baseOpacity: baseOpacity = 0.6, opacityReduction: opacityReduction = 0.1, glowRadius: glowRadius = 0, glowColor: glowColor, gradientFillEnd: gradientFillEnd, renderStyle: renderStyle = "fill-and-stroke", rng: rng } = config;
|
|
2448
|
+
const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, patterns: patterns = [], proportionType: proportionType = "GOLDEN_RATIO", baseOpacity: baseOpacity = 0.6, opacityReduction: opacityReduction = 0.1, glowRadius: glowRadius = 0, glowColor: glowColor, gradientFillEnd: gradientFillEnd, renderStyle: renderStyle = "fill-and-stroke", rng: rng, lightAngle: lightAngle, scaleFactor: scaleFactor = 1 } = config;
|
|
2234
2449
|
ctx.save();
|
|
2235
2450
|
ctx.translate(x, y);
|
|
2236
2451
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2237
|
-
//
|
|
2238
|
-
if (
|
|
2452
|
+
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2453
|
+
if (lightAngle !== undefined && size > 10) {
|
|
2454
|
+
const shadowDist = size * 0.035;
|
|
2455
|
+
const shadowBlurR = size * 0.06;
|
|
2456
|
+
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2457
|
+
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2458
|
+
ctx.shadowBlur = shadowBlurR;
|
|
2459
|
+
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2460
|
+
} else if (glowRadius > 0) {
|
|
2461
|
+
// Glow / shadow effect (legacy path)
|
|
2239
2462
|
ctx.shadowBlur = glowRadius;
|
|
2240
2463
|
ctx.shadowColor = glowColor || fillColor;
|
|
2241
2464
|
ctx.shadowOffsetX = 0;
|
|
@@ -2257,8 +2480,29 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2257
2480
|
});
|
|
2258
2481
|
$e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2259
2482
|
}
|
|
2260
|
-
// Reset shadow so patterns aren't double-
|
|
2261
|
-
|
|
2483
|
+
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2484
|
+
ctx.shadowBlur = 0;
|
|
2485
|
+
ctx.shadowOffsetX = 0;
|
|
2486
|
+
ctx.shadowOffsetY = 0;
|
|
2487
|
+
ctx.shadowColor = "transparent";
|
|
2488
|
+
// ── Specular highlight — bright arc on the light-facing side ──
|
|
2489
|
+
if (lightAngle !== undefined && size > 15 && rng) {
|
|
2490
|
+
const hlRadius = size * 0.35;
|
|
2491
|
+
const hlDist = size * 0.15;
|
|
2492
|
+
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2493
|
+
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2494
|
+
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2495
|
+
hlGrad.addColorStop(0, "rgba(255,255,255,0.18)");
|
|
2496
|
+
hlGrad.addColorStop(0.5, "rgba(255,255,255,0.05)");
|
|
2497
|
+
hlGrad.addColorStop(1, "rgba(255,255,255,0)");
|
|
2498
|
+
const savedOp = ctx.globalCompositeOperation;
|
|
2499
|
+
ctx.globalCompositeOperation = "soft-light";
|
|
2500
|
+
ctx.fillStyle = hlGrad;
|
|
2501
|
+
ctx.beginPath();
|
|
2502
|
+
ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
|
|
2503
|
+
ctx.fill();
|
|
2504
|
+
ctx.globalCompositeOperation = savedOp;
|
|
2505
|
+
}
|
|
2262
2506
|
// Layer additional patterns if specified
|
|
2263
2507
|
if (patterns.length > 0) (0, $616009579e3d72c5$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
|
|
2264
2508
|
baseSize: size,
|
|
@@ -3722,7 +3966,8 @@ const $1f63dc64b5593c73$var$COMPOSITION_MODES = [
|
|
|
3722
3966
|
"flow-field",
|
|
3723
3967
|
"spiral",
|
|
3724
3968
|
"grid-subdivision",
|
|
3725
|
-
"clustered"
|
|
3969
|
+
"clustered",
|
|
3970
|
+
"golden-spiral"
|
|
3726
3971
|
];
|
|
3727
3972
|
// ── Helper: get position based on composition mode ──────────────────
|
|
3728
3973
|
function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
|
|
@@ -3780,6 +4025,21 @@ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height,
|
|
|
3780
4025
|
x: rng() * width,
|
|
3781
4026
|
y: rng() * height
|
|
3782
4027
|
};
|
|
4028
|
+
case "golden-spiral":
|
|
4029
|
+
{
|
|
4030
|
+
// Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
|
|
4031
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4032
|
+
const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
|
|
4033
|
+
const t = shapeIndex / totalShapes;
|
|
4034
|
+
const angle = shapeIndex * goldenAngle + rng() * 0.3;
|
|
4035
|
+
const maxR = Math.min(width, height) * 0.44;
|
|
4036
|
+
// Shapes spiral outward with sqrt distribution for even area coverage
|
|
4037
|
+
const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
|
|
4038
|
+
return {
|
|
4039
|
+
x: cx + Math.cos(angle) * r,
|
|
4040
|
+
y: cy + Math.sin(angle) * r
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
3783
4043
|
}
|
|
3784
4044
|
}
|
|
3785
4045
|
// ── Helper: positional color from hierarchy ─────────────────────────
|
|
@@ -4248,11 +4508,24 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4248
4508
|
}
|
|
4249
4509
|
}
|
|
4250
4510
|
ctx.globalAlpha = 1;
|
|
4251
|
-
// ── 4. Flow field
|
|
4511
|
+
// ── 4. Flow field — simplex noise for organic variation ─────────
|
|
4512
|
+
// Create a seeded simplex noise field (unique per hash)
|
|
4513
|
+
const noiseFieldRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 333));
|
|
4514
|
+
const simplexNoise = (0, $616009579e3d72c5$export$bbde7fbaaf9a8d66)(noiseFieldRng);
|
|
4515
|
+
const fbmNoise = (0, $616009579e3d72c5$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
|
|
4252
4516
|
const fieldAngleBase = rng() * Math.PI * 2;
|
|
4253
|
-
const fieldFreq =
|
|
4517
|
+
const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
|
|
4254
4518
|
function flowAngle(x, y) {
|
|
4255
|
-
|
|
4519
|
+
// Sample FBM noise at the position, scaled by frequency
|
|
4520
|
+
const nx = x / width * fieldFreq;
|
|
4521
|
+
const ny = y / height * fieldFreq;
|
|
4522
|
+
return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
|
|
4523
|
+
}
|
|
4524
|
+
// Noise-based size modulation — shapes in "high noise" areas get scaled
|
|
4525
|
+
function noiseSizeModulation(x, y) {
|
|
4526
|
+
const n = simplexNoise(x / width * 3, y / height * 3);
|
|
4527
|
+
// Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
|
|
4528
|
+
return 0.7 + (n + 1) * 0.3;
|
|
4256
4529
|
}
|
|
4257
4530
|
// Track all placed shapes for density checks and connecting curves
|
|
4258
4531
|
const shapePositions = [];
|
|
@@ -4286,7 +4559,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4286
4559
|
glowColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(heroStroke, 0.4),
|
|
4287
4560
|
gradientFillEnd: (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
|
|
4288
4561
|
renderStyle: heroStyle,
|
|
4289
|
-
rng: rng
|
|
4562
|
+
rng: rng,
|
|
4563
|
+
lightAngle: lightAngle,
|
|
4564
|
+
scaleFactor: scaleFactor
|
|
4290
4565
|
});
|
|
4291
4566
|
heroCenter = {
|
|
4292
4567
|
x: heroFocal.x,
|
|
@@ -4345,7 +4620,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4345
4620
|
}
|
|
4346
4621
|
// Power distribution for size — archetype controls the curve
|
|
4347
4622
|
const sizeT = Math.pow(rng(), archetype.sizePower);
|
|
4348
|
-
const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
|
|
4623
|
+
const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
|
|
4349
4624
|
// Size fraction for affinity-aware shape selection
|
|
4350
4625
|
const sizeFraction = size / adjustedMaxSize;
|
|
4351
4626
|
// Palette-driven shape selection (replaces naive pickShape)
|
|
@@ -4448,7 +4723,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4448
4723
|
glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
|
|
4449
4724
|
gradientFillEnd: gradientEnd,
|
|
4450
4725
|
renderStyle: finalRenderStyle,
|
|
4451
|
-
rng: rng
|
|
4726
|
+
rng: rng,
|
|
4727
|
+
lightAngle: lightAngle,
|
|
4728
|
+
scaleFactor: scaleFactor
|
|
4452
4729
|
};
|
|
4453
4730
|
if (shouldMirror) (0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
4454
4731
|
...shapeConfig,
|
|
@@ -4579,6 +4856,64 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4579
4856
|
}
|
|
4580
4857
|
// Reset blend mode for post-processing passes
|
|
4581
4858
|
ctx.globalCompositeOperation = "source-over";
|
|
4859
|
+
// ── 5f. Layered masking / cutout portals ───────────────────────
|
|
4860
|
+
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
4861
|
+
// with a tinted background wash, creating a "peek through" effect.
|
|
4862
|
+
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
4863
|
+
const portalCount = 1 + Math.floor(rng() * 2);
|
|
4864
|
+
for(let p = 0; p < portalCount; p++){
|
|
4865
|
+
// Pick a position biased toward placed shapes
|
|
4866
|
+
const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
4867
|
+
const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
4868
|
+
const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
4869
|
+
const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
|
|
4870
|
+
// Pick a portal shape from the palette
|
|
4871
|
+
const portalShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
|
|
4872
|
+
const portalRotation = rng() * 360;
|
|
4873
|
+
const portalAlpha = 0.6 + rng() * 0.35;
|
|
4874
|
+
ctx.save();
|
|
4875
|
+
ctx.translate(portalX, portalY);
|
|
4876
|
+
ctx.rotate(portalRotation * Math.PI / 180);
|
|
4877
|
+
// Step 1: Clip to the portal shape and fill with background wash
|
|
4878
|
+
ctx.beginPath();
|
|
4879
|
+
(0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
|
|
4880
|
+
ctx.clip();
|
|
4881
|
+
// Fill the clipped region with a radial gradient from background colors
|
|
4882
|
+
const portalColor = (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
|
|
4883
|
+
const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
|
|
4884
|
+
portalGrad.addColorStop(0, portalColor);
|
|
4885
|
+
portalGrad.addColorStop(1, bgEnd);
|
|
4886
|
+
ctx.globalAlpha = portalAlpha;
|
|
4887
|
+
ctx.fillStyle = portalGrad;
|
|
4888
|
+
ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
|
|
4889
|
+
// Optional: subtle inner texture — a few tiny dots inside the portal
|
|
4890
|
+
if (rng() < 0.5) {
|
|
4891
|
+
const dotCount = 3 + Math.floor(rng() * 5);
|
|
4892
|
+
ctx.globalAlpha = portalAlpha * 0.3;
|
|
4893
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
|
|
4894
|
+
for(let d = 0; d < dotCount; d++){
|
|
4895
|
+
const dx = (rng() - 0.5) * portalSize * 1.4;
|
|
4896
|
+
const dy = (rng() - 0.5) * portalSize * 1.4;
|
|
4897
|
+
const dr = (1 + rng() * 3) * scaleFactor;
|
|
4898
|
+
ctx.beginPath();
|
|
4899
|
+
ctx.arc(dx, dy, dr, 0, Math.PI * 2);
|
|
4900
|
+
ctx.fill();
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
ctx.restore();
|
|
4904
|
+
// Step 2: Draw a border ring around the portal (outside the clip)
|
|
4905
|
+
ctx.save();
|
|
4906
|
+
ctx.translate(portalX, portalY);
|
|
4907
|
+
ctx.rotate(portalRotation * Math.PI / 180);
|
|
4908
|
+
ctx.globalAlpha = 0.15 + rng() * 0.2;
|
|
4909
|
+
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
|
|
4910
|
+
ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
|
|
4911
|
+
ctx.beginPath();
|
|
4912
|
+
(0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
|
|
4913
|
+
ctx.stroke();
|
|
4914
|
+
ctx.restore();
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4582
4917
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
4583
4918
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
4584
4919
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
@@ -4790,6 +5125,176 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4790
5125
|
ctx.restore();
|
|
4791
5126
|
ctx.globalCompositeOperation = "source-over";
|
|
4792
5127
|
}
|
|
5128
|
+
// 10d. Gradient map — map luminance through a two-color gradient
|
|
5129
|
+
// Uses dominant→accent as the dark→light ramp for a cohesive tonal look
|
|
5130
|
+
if (rng() < 0.35) {
|
|
5131
|
+
const gmDark = colorHierarchy.dominant;
|
|
5132
|
+
const gmLight = colorHierarchy.accent;
|
|
5133
|
+
ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
|
|
5134
|
+
ctx.globalCompositeOperation = "color";
|
|
5135
|
+
// Paint a linear gradient from dark color (top) to light color (bottom)
|
|
5136
|
+
const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
|
|
5137
|
+
gmGrad.addColorStop(0, gmDark);
|
|
5138
|
+
gmGrad.addColorStop(1, gmLight);
|
|
5139
|
+
ctx.fillStyle = gmGrad;
|
|
5140
|
+
ctx.fillRect(0, 0, width, height);
|
|
5141
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5142
|
+
}
|
|
5143
|
+
// ── 10e. Generative borders — archetype-driven decorative frames ──
|
|
5144
|
+
{
|
|
5145
|
+
ctx.save();
|
|
5146
|
+
ctx.globalAlpha = 1;
|
|
5147
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5148
|
+
const borderRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 314));
|
|
5149
|
+
const borderPad = Math.min(width, height) * 0.025;
|
|
5150
|
+
const borderColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
|
|
5151
|
+
const borderColorSolid = colorHierarchy.accent;
|
|
5152
|
+
const archName = archetype.name;
|
|
5153
|
+
if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
|
|
5154
|
+
// Clean ruled lines with corner ornaments
|
|
5155
|
+
ctx.strokeStyle = borderColor;
|
|
5156
|
+
ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
|
|
5157
|
+
ctx.globalAlpha = 0.18 + borderRng() * 0.1;
|
|
5158
|
+
// Outer rule
|
|
5159
|
+
ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
|
|
5160
|
+
// Inner rule (thinner, offset)
|
|
5161
|
+
const innerPad = borderPad * 1.8;
|
|
5162
|
+
ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
|
|
5163
|
+
ctx.globalAlpha *= 0.7;
|
|
5164
|
+
ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
|
|
5165
|
+
// Corner ornaments — small squares at each corner
|
|
5166
|
+
const ornSize = borderPad * 0.6;
|
|
5167
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(borderColorSolid, 0.12);
|
|
5168
|
+
const corners = [
|
|
5169
|
+
[
|
|
5170
|
+
borderPad,
|
|
5171
|
+
borderPad
|
|
5172
|
+
],
|
|
5173
|
+
[
|
|
5174
|
+
width - borderPad - ornSize,
|
|
5175
|
+
borderPad
|
|
5176
|
+
],
|
|
5177
|
+
[
|
|
5178
|
+
borderPad,
|
|
5179
|
+
height - borderPad - ornSize
|
|
5180
|
+
],
|
|
5181
|
+
[
|
|
5182
|
+
width - borderPad - ornSize,
|
|
5183
|
+
height - borderPad - ornSize
|
|
5184
|
+
]
|
|
5185
|
+
];
|
|
5186
|
+
for (const [cx2, cy2] of corners){
|
|
5187
|
+
ctx.fillRect(cx2, cy2, ornSize, ornSize);
|
|
5188
|
+
// Diagonal cross inside ornament
|
|
5189
|
+
ctx.beginPath();
|
|
5190
|
+
ctx.moveTo(cx2, cy2);
|
|
5191
|
+
ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
|
|
5192
|
+
ctx.moveTo(cx2 + ornSize, cy2);
|
|
5193
|
+
ctx.lineTo(cx2, cy2 + ornSize);
|
|
5194
|
+
ctx.stroke();
|
|
5195
|
+
}
|
|
5196
|
+
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5197
|
+
// Vine tendrils — organic curving lines along edges
|
|
5198
|
+
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5199
|
+
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5200
|
+
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5201
|
+
ctx.lineCap = "round";
|
|
5202
|
+
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5203
|
+
for(let t = 0; t < tendrilCount; t++){
|
|
5204
|
+
// Start from a random edge point
|
|
5205
|
+
const edge = Math.floor(borderRng() * 4);
|
|
5206
|
+
let tx, ty;
|
|
5207
|
+
if (edge === 0) {
|
|
5208
|
+
tx = borderRng() * width;
|
|
5209
|
+
ty = borderPad;
|
|
5210
|
+
} else if (edge === 1) {
|
|
5211
|
+
tx = borderRng() * width;
|
|
5212
|
+
ty = height - borderPad;
|
|
5213
|
+
} else if (edge === 2) {
|
|
5214
|
+
tx = borderPad;
|
|
5215
|
+
ty = borderRng() * height;
|
|
5216
|
+
} else {
|
|
5217
|
+
tx = width - borderPad;
|
|
5218
|
+
ty = borderRng() * height;
|
|
5219
|
+
}
|
|
5220
|
+
ctx.beginPath();
|
|
5221
|
+
ctx.moveTo(tx, ty);
|
|
5222
|
+
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5223
|
+
for(let s = 0; s < segs; s++){
|
|
5224
|
+
const inward = borderPad * (1 + borderRng() * 2);
|
|
5225
|
+
// Curl inward from edge
|
|
5226
|
+
const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
|
|
5227
|
+
const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
|
|
5228
|
+
const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
|
|
5229
|
+
const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
|
|
5230
|
+
tx = cpx3;
|
|
5231
|
+
ty = cpy3;
|
|
5232
|
+
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5233
|
+
}
|
|
5234
|
+
ctx.stroke();
|
|
5235
|
+
// Small leaf/dot at tendril end
|
|
5236
|
+
if (borderRng() < 0.6) {
|
|
5237
|
+
ctx.beginPath();
|
|
5238
|
+
ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
|
|
5239
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5240
|
+
ctx.fill();
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5244
|
+
// Star-studded arcs along edges
|
|
5245
|
+
ctx.globalAlpha = 0.1 + borderRng() * 0.08;
|
|
5246
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
|
|
5247
|
+
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
|
|
5248
|
+
ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
|
|
5249
|
+
// Subtle arc along top and bottom
|
|
5250
|
+
ctx.beginPath();
|
|
5251
|
+
ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
|
|
5252
|
+
ctx.stroke();
|
|
5253
|
+
ctx.beginPath();
|
|
5254
|
+
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5255
|
+
ctx.stroke();
|
|
5256
|
+
// Scatter small stars along the border region
|
|
5257
|
+
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5258
|
+
for(let s = 0; s < starCount; s++){
|
|
5259
|
+
const edge = Math.floor(borderRng() * 4);
|
|
5260
|
+
let sx, sy;
|
|
5261
|
+
if (edge === 0) {
|
|
5262
|
+
sx = borderRng() * width;
|
|
5263
|
+
sy = borderPad * (0.5 + borderRng());
|
|
5264
|
+
} else if (edge === 1) {
|
|
5265
|
+
sx = borderRng() * width;
|
|
5266
|
+
sy = height - borderPad * (0.5 + borderRng());
|
|
5267
|
+
} else if (edge === 2) {
|
|
5268
|
+
sx = borderPad * (0.5 + borderRng());
|
|
5269
|
+
sy = borderRng() * height;
|
|
5270
|
+
} else {
|
|
5271
|
+
sx = width - borderPad * (0.5 + borderRng());
|
|
5272
|
+
sy = borderRng() * height;
|
|
5273
|
+
}
|
|
5274
|
+
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5275
|
+
// 4-point star
|
|
5276
|
+
ctx.beginPath();
|
|
5277
|
+
for(let p = 0; p < 8; p++){
|
|
5278
|
+
const a = p / 8 * Math.PI * 2;
|
|
5279
|
+
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
5280
|
+
const px2 = sx + Math.cos(a) * r;
|
|
5281
|
+
const py2 = sy + Math.sin(a) * r;
|
|
5282
|
+
if (p === 0) ctx.moveTo(px2, py2);
|
|
5283
|
+
else ctx.lineTo(px2, py2);
|
|
5284
|
+
}
|
|
5285
|
+
ctx.closePath();
|
|
5286
|
+
ctx.fill();
|
|
5287
|
+
}
|
|
5288
|
+
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5289
|
+
// Thin single rule — understated elegance
|
|
5290
|
+
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
5291
|
+
ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
|
|
5292
|
+
ctx.globalAlpha = 0.1 + borderRng() * 0.06;
|
|
5293
|
+
ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
|
|
5294
|
+
}
|
|
5295
|
+
// Other archetypes: no border (intentional — not every image needs one)
|
|
5296
|
+
ctx.restore();
|
|
5297
|
+
}
|
|
4793
5298
|
// ── 11. Signature mark — unique geometric chop from hash prefix ──
|
|
4794
5299
|
{
|
|
4795
5300
|
const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
|