git-hash-art 0.10.0 → 0.11.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/.github/workflows/deploy-www.yml +57 -0
- package/ALGORITHM.md +108 -8
- package/CHANGELOG.md +20 -0
- package/dist/browser.js +873 -64
- package/dist/browser.js.map +1 -1
- package/dist/main.js +875 -64
- package/dist/main.js.map +1 -1
- package/dist/module.js +875 -64
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/archetypes.ts +33 -1
- package/src/lib/canvas/colors.ts +52 -5
- package/src/lib/canvas/draw.ts +94 -5
- package/src/lib/render.ts +501 -67
- 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",
|
|
@@ -400,15 +580,17 @@ function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
|
|
|
400
580
|
accent: colors[colors.length - 1] || "#888888",
|
|
401
581
|
all: colors
|
|
402
582
|
};
|
|
403
|
-
// Pick dominant as the color
|
|
583
|
+
// Pick dominant as the color with the highest chroma (saturation × distance from gray)
|
|
584
|
+
// This selects the most visually prominent color rather than the average
|
|
404
585
|
const hsls = colors.map((c)=>$b5a262d09b87e373$var$hexToHsl(c));
|
|
405
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
406
586
|
let dominantIdx = 0;
|
|
407
|
-
let
|
|
587
|
+
let maxChroma = -1;
|
|
408
588
|
for(let i = 0; i < hsls.length; i++){
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
589
|
+
// Chroma approximation: saturation × how far lightness is from 50% (gray)
|
|
590
|
+
const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
|
|
591
|
+
const chroma = hsls[i][1] * lightnessVibrancy;
|
|
592
|
+
if (chroma > maxChroma) {
|
|
593
|
+
maxChroma = chroma;
|
|
412
594
|
dominantIdx = i;
|
|
413
595
|
}
|
|
414
596
|
}
|
|
@@ -527,7 +709,8 @@ function $b5a262d09b87e373$export$703ba40a4347f77a(base, layerRatio, hueShiftPer
|
|
|
527
709
|
return {
|
|
528
710
|
dominant: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.dominant, shift),
|
|
529
711
|
secondary: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
|
|
530
|
-
accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
|
|
712
|
+
accent: $b5a262d09b87e373$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
|
|
713
|
+
all: base.all.map((c)=>$b5a262d09b87e373$export$1793a1bfbe4f6ff5(c, shift * 0.6))
|
|
531
714
|
};
|
|
532
715
|
}
|
|
533
716
|
|
|
@@ -1910,6 +2093,23 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
1910
2093
|
ctx.fill();
|
|
1911
2094
|
ctx.fillStyle = origFill;
|
|
1912
2095
|
ctx.restore();
|
|
2096
|
+
// Pass 4: Organic edge erosion — irregular bites along the boundary
|
|
2097
|
+
if (rng && size > 20) {
|
|
2098
|
+
const erosionBites = 6 + Math.floor(rng() * 8);
|
|
2099
|
+
const edgeRadius = size * 0.45;
|
|
2100
|
+
ctx.save();
|
|
2101
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
2102
|
+
ctx.globalAlpha = 0.6 + rng() * 0.3;
|
|
2103
|
+
for(let eb = 0; eb < erosionBites; eb++){
|
|
2104
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
2105
|
+
const biteDist = edgeRadius * (0.85 + rng() * 0.25);
|
|
2106
|
+
const biteR = size * (0.02 + rng() * 0.04);
|
|
2107
|
+
ctx.beginPath();
|
|
2108
|
+
ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
|
|
2109
|
+
ctx.fill();
|
|
2110
|
+
}
|
|
2111
|
+
ctx.restore();
|
|
2112
|
+
}
|
|
1913
2113
|
ctx.globalAlpha = savedAlpha;
|
|
1914
2114
|
// Soft stroke on top — thinner than normal for delicacy
|
|
1915
2115
|
ctx.globalAlpha *= 0.25;
|
|
@@ -2219,6 +2419,23 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2219
2419
|
ctx.stroke();
|
|
2220
2420
|
ctx.restore();
|
|
2221
2421
|
}
|
|
2422
|
+
// Organic edge erosion — small irregular bites for rough paper feel
|
|
2423
|
+
if (rng && size > 20) {
|
|
2424
|
+
const erosionBites = 4 + Math.floor(rng() * 6);
|
|
2425
|
+
const edgeRadius = size * 0.42;
|
|
2426
|
+
ctx.save();
|
|
2427
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
2428
|
+
ctx.globalAlpha = 0.5 + rng() * 0.3;
|
|
2429
|
+
for(let eb = 0; eb < erosionBites; eb++){
|
|
2430
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
2431
|
+
const biteDist = edgeRadius * (0.9 + rng() * 0.2);
|
|
2432
|
+
const biteR = size * (0.015 + rng() * 0.03);
|
|
2433
|
+
ctx.beginPath();
|
|
2434
|
+
ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
|
|
2435
|
+
ctx.fill();
|
|
2436
|
+
}
|
|
2437
|
+
ctx.restore();
|
|
2438
|
+
}
|
|
2222
2439
|
ctx.globalAlpha = savedAlphaHD;
|
|
2223
2440
|
break;
|
|
2224
2441
|
}
|
|
@@ -2230,12 +2447,20 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2230
2447
|
}
|
|
2231
2448
|
}
|
|
2232
2449
|
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;
|
|
2450
|
+
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
2451
|
ctx.save();
|
|
2235
2452
|
ctx.translate(x, y);
|
|
2236
2453
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2237
|
-
//
|
|
2238
|
-
if (
|
|
2454
|
+
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2455
|
+
if (lightAngle !== undefined && size > 10) {
|
|
2456
|
+
const shadowDist = size * 0.035;
|
|
2457
|
+
const shadowBlurR = size * 0.06;
|
|
2458
|
+
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2459
|
+
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2460
|
+
ctx.shadowBlur = shadowBlurR;
|
|
2461
|
+
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2462
|
+
} else if (glowRadius > 0) {
|
|
2463
|
+
// Glow / shadow effect (legacy path)
|
|
2239
2464
|
ctx.shadowBlur = glowRadius;
|
|
2240
2465
|
ctx.shadowColor = glowColor || fillColor;
|
|
2241
2466
|
ctx.shadowOffsetX = 0;
|
|
@@ -2257,8 +2482,39 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2257
2482
|
});
|
|
2258
2483
|
$e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2259
2484
|
}
|
|
2260
|
-
// Reset shadow so patterns aren't double-
|
|
2261
|
-
|
|
2485
|
+
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2486
|
+
ctx.shadowBlur = 0;
|
|
2487
|
+
ctx.shadowOffsetX = 0;
|
|
2488
|
+
ctx.shadowOffsetY = 0;
|
|
2489
|
+
ctx.shadowColor = "transparent";
|
|
2490
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2491
|
+
if (lightAngle !== undefined && size > 15 && rng) {
|
|
2492
|
+
const hlRadius = size * 0.35;
|
|
2493
|
+
const hlDist = size * 0.15;
|
|
2494
|
+
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2495
|
+
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2496
|
+
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2497
|
+
// Tint highlight warm/cool based on fill color for cohesion
|
|
2498
|
+
// Parse fill to detect warmth — fallback to white for non-parseable
|
|
2499
|
+
let hlBase = "255,255,255";
|
|
2500
|
+
if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
|
|
2501
|
+
const r = parseInt(fillColor.slice(1, 3), 16);
|
|
2502
|
+
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2503
|
+
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2504
|
+
// Blend toward white but keep a hint of the fill's warmth
|
|
2505
|
+
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2506
|
+
}
|
|
2507
|
+
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2508
|
+
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2509
|
+
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2510
|
+
const savedOp = ctx.globalCompositeOperation;
|
|
2511
|
+
ctx.globalCompositeOperation = "soft-light";
|
|
2512
|
+
ctx.fillStyle = hlGrad;
|
|
2513
|
+
ctx.beginPath();
|
|
2514
|
+
ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
|
|
2515
|
+
ctx.fill();
|
|
2516
|
+
ctx.globalCompositeOperation = savedOp;
|
|
2517
|
+
}
|
|
2262
2518
|
// Layer additional patterns if specified
|
|
2263
2519
|
if (patterns.length > 0) (0, $616009579e3d72c5$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
|
|
2264
2520
|
baseSize: size,
|
|
@@ -3312,6 +3568,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3312
3568
|
"watercolor",
|
|
3313
3569
|
"fill-only"
|
|
3314
3570
|
],
|
|
3571
|
+
preferredCompositions: [
|
|
3572
|
+
"clustered",
|
|
3573
|
+
"flow-field",
|
|
3574
|
+
"radial"
|
|
3575
|
+
],
|
|
3315
3576
|
flowLineMultiplier: 2.5,
|
|
3316
3577
|
heroShape: false,
|
|
3317
3578
|
glowMultiplier: 0.5,
|
|
@@ -3333,6 +3594,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3333
3594
|
"stroke-only",
|
|
3334
3595
|
"incomplete"
|
|
3335
3596
|
],
|
|
3597
|
+
preferredCompositions: [
|
|
3598
|
+
"golden-spiral",
|
|
3599
|
+
"grid-subdivision"
|
|
3600
|
+
],
|
|
3336
3601
|
flowLineMultiplier: 0.3,
|
|
3337
3602
|
heroShape: true,
|
|
3338
3603
|
glowMultiplier: 0,
|
|
@@ -3354,6 +3619,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3354
3619
|
"fill-only",
|
|
3355
3620
|
"incomplete"
|
|
3356
3621
|
],
|
|
3622
|
+
preferredCompositions: [
|
|
3623
|
+
"flow-field",
|
|
3624
|
+
"golden-spiral",
|
|
3625
|
+
"spiral"
|
|
3626
|
+
],
|
|
3357
3627
|
flowLineMultiplier: 4,
|
|
3358
3628
|
heroShape: false,
|
|
3359
3629
|
glowMultiplier: 0.3,
|
|
@@ -3376,6 +3646,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3376
3646
|
"double-stroke",
|
|
3377
3647
|
"hatched"
|
|
3378
3648
|
],
|
|
3649
|
+
preferredCompositions: [
|
|
3650
|
+
"grid-subdivision",
|
|
3651
|
+
"radial"
|
|
3652
|
+
],
|
|
3379
3653
|
flowLineMultiplier: 0,
|
|
3380
3654
|
heroShape: false,
|
|
3381
3655
|
glowMultiplier: 0,
|
|
@@ -3397,6 +3671,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3397
3671
|
"incomplete",
|
|
3398
3672
|
"fill-only"
|
|
3399
3673
|
],
|
|
3674
|
+
preferredCompositions: [
|
|
3675
|
+
"golden-spiral",
|
|
3676
|
+
"radial",
|
|
3677
|
+
"spiral"
|
|
3678
|
+
],
|
|
3400
3679
|
flowLineMultiplier: 1.5,
|
|
3401
3680
|
heroShape: true,
|
|
3402
3681
|
glowMultiplier: 2,
|
|
@@ -3417,6 +3696,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3417
3696
|
"fill-and-stroke",
|
|
3418
3697
|
"double-stroke"
|
|
3419
3698
|
],
|
|
3699
|
+
preferredCompositions: [
|
|
3700
|
+
"grid-subdivision",
|
|
3701
|
+
"golden-spiral"
|
|
3702
|
+
],
|
|
3420
3703
|
flowLineMultiplier: 0,
|
|
3421
3704
|
heroShape: true,
|
|
3422
3705
|
glowMultiplier: 0,
|
|
@@ -3438,6 +3721,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3438
3721
|
"double-stroke",
|
|
3439
3722
|
"dashed"
|
|
3440
3723
|
],
|
|
3724
|
+
preferredCompositions: [
|
|
3725
|
+
"radial",
|
|
3726
|
+
"spiral",
|
|
3727
|
+
"clustered"
|
|
3728
|
+
],
|
|
3441
3729
|
flowLineMultiplier: 2,
|
|
3442
3730
|
heroShape: true,
|
|
3443
3731
|
glowMultiplier: 3,
|
|
@@ -3460,6 +3748,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3460
3748
|
"stroke-only",
|
|
3461
3749
|
"dashed"
|
|
3462
3750
|
],
|
|
3751
|
+
preferredCompositions: [
|
|
3752
|
+
"flow-field",
|
|
3753
|
+
"grid-subdivision",
|
|
3754
|
+
"clustered"
|
|
3755
|
+
],
|
|
3463
3756
|
flowLineMultiplier: 1.5,
|
|
3464
3757
|
heroShape: false,
|
|
3465
3758
|
glowMultiplier: 0,
|
|
@@ -3481,6 +3774,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3481
3774
|
"watercolor",
|
|
3482
3775
|
"fill-and-stroke"
|
|
3483
3776
|
],
|
|
3777
|
+
preferredCompositions: [
|
|
3778
|
+
"radial",
|
|
3779
|
+
"spiral",
|
|
3780
|
+
"golden-spiral"
|
|
3781
|
+
],
|
|
3484
3782
|
flowLineMultiplier: 3,
|
|
3485
3783
|
heroShape: true,
|
|
3486
3784
|
glowMultiplier: 2.5,
|
|
@@ -3502,6 +3800,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3502
3800
|
"fill-only",
|
|
3503
3801
|
"incomplete"
|
|
3504
3802
|
],
|
|
3803
|
+
preferredCompositions: [
|
|
3804
|
+
"golden-spiral",
|
|
3805
|
+
"flow-field",
|
|
3806
|
+
"radial"
|
|
3807
|
+
],
|
|
3505
3808
|
flowLineMultiplier: 0.5,
|
|
3506
3809
|
heroShape: false,
|
|
3507
3810
|
glowMultiplier: 0.3,
|
|
@@ -3523,6 +3826,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3523
3826
|
"stroke-only",
|
|
3524
3827
|
"dashed"
|
|
3525
3828
|
],
|
|
3829
|
+
preferredCompositions: [
|
|
3830
|
+
"grid-subdivision",
|
|
3831
|
+
"radial"
|
|
3832
|
+
],
|
|
3526
3833
|
flowLineMultiplier: 0,
|
|
3527
3834
|
heroShape: false,
|
|
3528
3835
|
glowMultiplier: 0,
|
|
@@ -3544,6 +3851,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3544
3851
|
"fill-only",
|
|
3545
3852
|
"double-stroke"
|
|
3546
3853
|
],
|
|
3854
|
+
preferredCompositions: [
|
|
3855
|
+
"grid-subdivision",
|
|
3856
|
+
"clustered"
|
|
3857
|
+
],
|
|
3547
3858
|
flowLineMultiplier: 0,
|
|
3548
3859
|
heroShape: true,
|
|
3549
3860
|
glowMultiplier: 0,
|
|
@@ -3565,6 +3876,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3565
3876
|
"watercolor",
|
|
3566
3877
|
"fill-only"
|
|
3567
3878
|
],
|
|
3879
|
+
preferredCompositions: [
|
|
3880
|
+
"radial",
|
|
3881
|
+
"golden-spiral",
|
|
3882
|
+
"flow-field"
|
|
3883
|
+
],
|
|
3568
3884
|
flowLineMultiplier: 1,
|
|
3569
3885
|
heroShape: true,
|
|
3570
3886
|
glowMultiplier: 1,
|
|
@@ -3586,6 +3902,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3586
3902
|
"stroke-only",
|
|
3587
3903
|
"fill-only"
|
|
3588
3904
|
],
|
|
3905
|
+
preferredCompositions: [
|
|
3906
|
+
"clustered",
|
|
3907
|
+
"grid-subdivision",
|
|
3908
|
+
"radial"
|
|
3909
|
+
],
|
|
3589
3910
|
flowLineMultiplier: 0,
|
|
3590
3911
|
heroShape: false,
|
|
3591
3912
|
glowMultiplier: 0.3,
|
|
@@ -3607,6 +3928,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3607
3928
|
"fill-only",
|
|
3608
3929
|
"incomplete"
|
|
3609
3930
|
],
|
|
3931
|
+
preferredCompositions: [
|
|
3932
|
+
"flow-field",
|
|
3933
|
+
"golden-spiral",
|
|
3934
|
+
"spiral"
|
|
3935
|
+
],
|
|
3610
3936
|
flowLineMultiplier: 3,
|
|
3611
3937
|
heroShape: true,
|
|
3612
3938
|
glowMultiplier: 0.2,
|
|
@@ -3628,6 +3954,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3628
3954
|
"fill-only",
|
|
3629
3955
|
"hatched"
|
|
3630
3956
|
],
|
|
3957
|
+
preferredCompositions: [
|
|
3958
|
+
"radial",
|
|
3959
|
+
"clustered",
|
|
3960
|
+
"flow-field"
|
|
3961
|
+
],
|
|
3631
3962
|
flowLineMultiplier: 0,
|
|
3632
3963
|
heroShape: false,
|
|
3633
3964
|
glowMultiplier: 0,
|
|
@@ -3650,6 +3981,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3650
3981
|
"stroke-only",
|
|
3651
3982
|
"incomplete"
|
|
3652
3983
|
],
|
|
3984
|
+
preferredCompositions: [
|
|
3985
|
+
"spiral",
|
|
3986
|
+
"radial",
|
|
3987
|
+
"golden-spiral"
|
|
3988
|
+
],
|
|
3653
3989
|
flowLineMultiplier: 2,
|
|
3654
3990
|
heroShape: true,
|
|
3655
3991
|
glowMultiplier: 2.5,
|
|
@@ -3673,6 +4009,12 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3673
4009
|
...b.preferredStyles
|
|
3674
4010
|
])
|
|
3675
4011
|
];
|
|
4012
|
+
const mergedCompositions = [
|
|
4013
|
+
...new Set([
|
|
4014
|
+
...a.preferredCompositions,
|
|
4015
|
+
...b.preferredCompositions
|
|
4016
|
+
])
|
|
4017
|
+
];
|
|
3676
4018
|
return {
|
|
3677
4019
|
name: `${a.name}+${b.name}`,
|
|
3678
4020
|
gridSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3684,6 +4026,7 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3684
4026
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3685
4027
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3686
4028
|
preferredStyles: mergedStyles,
|
|
4029
|
+
preferredCompositions: mergedCompositions,
|
|
3687
4030
|
flowLineMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3688
4031
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3689
4032
|
glowMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3717,12 +4060,14 @@ const $1f63dc64b5593c73$var$SACRED_SHAPES = [
|
|
|
3717
4060
|
"torus",
|
|
3718
4061
|
"eggOfLife"
|
|
3719
4062
|
];
|
|
3720
|
-
|
|
4063
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4064
|
+
const $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES = [
|
|
3721
4065
|
"radial",
|
|
3722
4066
|
"flow-field",
|
|
3723
4067
|
"spiral",
|
|
3724
4068
|
"grid-subdivision",
|
|
3725
|
-
"clustered"
|
|
4069
|
+
"clustered",
|
|
4070
|
+
"golden-spiral"
|
|
3726
4071
|
];
|
|
3727
4072
|
// ── Helper: get position based on composition mode ──────────────────
|
|
3728
4073
|
function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
|
|
@@ -3780,6 +4125,21 @@ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height,
|
|
|
3780
4125
|
x: rng() * width,
|
|
3781
4126
|
y: rng() * height
|
|
3782
4127
|
};
|
|
4128
|
+
case "golden-spiral":
|
|
4129
|
+
{
|
|
4130
|
+
// Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
|
|
4131
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4132
|
+
const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
|
|
4133
|
+
const t = shapeIndex / totalShapes;
|
|
4134
|
+
const angle = shapeIndex * goldenAngle + rng() * 0.3;
|
|
4135
|
+
const maxR = Math.min(width, height) * 0.44;
|
|
4136
|
+
// Shapes spiral outward with sqrt distribution for even area coverage
|
|
4137
|
+
const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
|
|
4138
|
+
return {
|
|
4139
|
+
x: cx + Math.cos(angle) * r,
|
|
4140
|
+
y: cy + Math.sin(angle) * r
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
3783
4143
|
}
|
|
3784
4144
|
}
|
|
3785
4145
|
// ── Helper: positional color from hierarchy ─────────────────────────
|
|
@@ -3803,7 +4163,67 @@ function $1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones) {
|
|
|
3803
4163
|
}
|
|
3804
4164
|
return false;
|
|
3805
4165
|
}
|
|
3806
|
-
// ──
|
|
4166
|
+
// ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
|
|
4167
|
+
class $1f63dc64b5593c73$var$SpatialGrid {
|
|
4168
|
+
constructor(cellSize){
|
|
4169
|
+
this.cells = new Map();
|
|
4170
|
+
this.cellSize = cellSize;
|
|
4171
|
+
}
|
|
4172
|
+
key(cx, cy) {
|
|
4173
|
+
return `${cx},${cy}`;
|
|
4174
|
+
}
|
|
4175
|
+
insert(item) {
|
|
4176
|
+
const cx = Math.floor(item.x / this.cellSize);
|
|
4177
|
+
const cy = Math.floor(item.y / this.cellSize);
|
|
4178
|
+
const k = this.key(cx, cy);
|
|
4179
|
+
const cell = this.cells.get(k);
|
|
4180
|
+
if (cell) cell.push(item);
|
|
4181
|
+
else this.cells.set(k, [
|
|
4182
|
+
item
|
|
4183
|
+
]);
|
|
4184
|
+
}
|
|
4185
|
+
/** Count items within radius of (x, y) */ countNear(x, y, radius) {
|
|
4186
|
+
const r2 = radius * radius;
|
|
4187
|
+
const minCx = Math.floor((x - radius) / this.cellSize);
|
|
4188
|
+
const maxCx = Math.floor((x + radius) / this.cellSize);
|
|
4189
|
+
const minCy = Math.floor((y - radius) / this.cellSize);
|
|
4190
|
+
const maxCy = Math.floor((y + radius) / this.cellSize);
|
|
4191
|
+
let count = 0;
|
|
4192
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4193
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4194
|
+
if (!cell) continue;
|
|
4195
|
+
for (const p of cell){
|
|
4196
|
+
const dx = x - p.x;
|
|
4197
|
+
const dy = y - p.y;
|
|
4198
|
+
if (dx * dx + dy * dy < r2) count++;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
return count;
|
|
4202
|
+
}
|
|
4203
|
+
/** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
|
|
4204
|
+
const minCx = Math.floor((x - searchRadius) / this.cellSize);
|
|
4205
|
+
const maxCx = Math.floor((x + searchRadius) / this.cellSize);
|
|
4206
|
+
const minCy = Math.floor((y - searchRadius) / this.cellSize);
|
|
4207
|
+
const maxCy = Math.floor((y + searchRadius) / this.cellSize);
|
|
4208
|
+
let nearest = null;
|
|
4209
|
+
let bestDist2 = Infinity;
|
|
4210
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4211
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4212
|
+
if (!cell) continue;
|
|
4213
|
+
for (const p of cell){
|
|
4214
|
+
const dx = x - p.x;
|
|
4215
|
+
const dy = y - p.y;
|
|
4216
|
+
const d2 = dx * dx + dy * dy;
|
|
4217
|
+
if (d2 > 0 && d2 < bestDist2) {
|
|
4218
|
+
bestDist2 = d2;
|
|
4219
|
+
nearest = p;
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
return nearest;
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
// ── Helper: density check (legacy wrapper) ──────────────────────────
|
|
3807
4227
|
function $1f63dc64b5593c73$var$localDensity(x, y, positions, radius) {
|
|
3808
4228
|
let count = 0;
|
|
3809
4229
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4102,42 +4522,43 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4102
4522
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4103
4523
|
const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4104
4524
|
if (bgPatternRoll < 0.2) {
|
|
4105
|
-
// Dot grid
|
|
4525
|
+
// Dot grid — batched into a single path
|
|
4106
4526
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4107
4527
|
const dotR = dotSpacing * 0.08;
|
|
4108
4528
|
ctx.globalAlpha = patternOpacity;
|
|
4109
4529
|
ctx.fillStyle = patternColor;
|
|
4530
|
+
ctx.beginPath();
|
|
4110
4531
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4111
|
-
ctx.
|
|
4532
|
+
ctx.moveTo(px + dotR, py);
|
|
4112
4533
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4113
|
-
ctx.fill();
|
|
4114
4534
|
}
|
|
4535
|
+
ctx.fill();
|
|
4115
4536
|
} else if (bgPatternRoll < 0.4) {
|
|
4116
|
-
// Diagonal lines
|
|
4537
|
+
// Diagonal lines — batched into a single path
|
|
4117
4538
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4118
4539
|
ctx.globalAlpha = patternOpacity;
|
|
4119
4540
|
ctx.strokeStyle = patternColor;
|
|
4120
4541
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4121
4542
|
const diag = Math.hypot(width, height);
|
|
4543
|
+
ctx.beginPath();
|
|
4122
4544
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4123
|
-
ctx.beginPath();
|
|
4124
4545
|
ctx.moveTo(d, 0);
|
|
4125
4546
|
ctx.lineTo(d + height, height);
|
|
4126
|
-
ctx.stroke();
|
|
4127
4547
|
}
|
|
4548
|
+
ctx.stroke();
|
|
4128
4549
|
} else {
|
|
4129
|
-
// Tessellation — hexagonal grid
|
|
4550
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4130
4551
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4131
4552
|
const tessH = tessSize * Math.sqrt(3);
|
|
4132
4553
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4133
4554
|
ctx.strokeStyle = patternColor;
|
|
4134
4555
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4556
|
+
ctx.beginPath();
|
|
4135
4557
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4136
4558
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4137
4559
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4138
4560
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4139
4561
|
const hy = row * tessH;
|
|
4140
|
-
ctx.beginPath();
|
|
4141
4562
|
for(let s = 0; s < 6; s++){
|
|
4142
4563
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4143
4564
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4146,18 +4567,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4146
4567
|
else ctx.lineTo(vx, vy);
|
|
4147
4568
|
}
|
|
4148
4569
|
ctx.closePath();
|
|
4149
|
-
ctx.stroke();
|
|
4150
4570
|
}
|
|
4151
4571
|
}
|
|
4572
|
+
ctx.stroke();
|
|
4152
4573
|
}
|
|
4153
4574
|
ctx.restore();
|
|
4154
4575
|
}
|
|
4155
4576
|
ctx.globalCompositeOperation = "source-over";
|
|
4156
|
-
// ── 2. Composition mode
|
|
4157
|
-
const compositionMode = $1f63dc64b5593c73$var$
|
|
4577
|
+
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4578
|
+
const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES.length)];
|
|
4158
4579
|
const symRoll = rng();
|
|
4159
4580
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4160
|
-
// ── 3. Focal points + void zones
|
|
4581
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4161
4582
|
const THIRDS_POINTS = [
|
|
4162
4583
|
{
|
|
4163
4584
|
x: 1 / 3,
|
|
@@ -4190,9 +4611,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4190
4611
|
y: height * (0.2 + rng() * 0.6),
|
|
4191
4612
|
strength: 0.3 + rng() * 0.4
|
|
4192
4613
|
});
|
|
4193
|
-
|
|
4614
|
+
// Archetype-aware void zones: dense archetypes get fewer/no voids,
|
|
4615
|
+
// minimal archetypes get golden-ratio positioned voids
|
|
4616
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4617
|
+
const isMinimalArchetype = archetype.gridSize <= 3;
|
|
4618
|
+
const isDenseArchetype = archetype.gridSize >= 8;
|
|
4619
|
+
const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
|
|
4194
4620
|
const voidZones = [];
|
|
4195
|
-
for(let v = 0; v < numVoids; v++)
|
|
4621
|
+
for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
|
|
4622
|
+
// Place voids at golden-ratio positions for intentional negative space
|
|
4623
|
+
const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
|
|
4624
|
+
const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
|
|
4625
|
+
voidZones.push({
|
|
4626
|
+
x: width * (gx + (rng() - 0.5) * 0.05),
|
|
4627
|
+
y: height * (gy + (rng() - 0.5) * 0.05),
|
|
4628
|
+
radius: Math.min(width, height) * (0.08 + rng() * 0.08)
|
|
4629
|
+
});
|
|
4630
|
+
} else voidZones.push({
|
|
4196
4631
|
x: width * (0.15 + rng() * 0.7),
|
|
4197
4632
|
y: height * (0.15 + rng() * 0.7),
|
|
4198
4633
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4248,14 +4683,30 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4248
4683
|
}
|
|
4249
4684
|
}
|
|
4250
4685
|
ctx.globalAlpha = 1;
|
|
4251
|
-
// ── 4. Flow field
|
|
4686
|
+
// ── 4. Flow field — simplex noise for organic variation ─────────
|
|
4687
|
+
// Create a seeded simplex noise field (unique per hash)
|
|
4688
|
+
const noiseFieldRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 333));
|
|
4689
|
+
const simplexNoise = (0, $616009579e3d72c5$export$bbde7fbaaf9a8d66)(noiseFieldRng);
|
|
4690
|
+
const fbmNoise = (0, $616009579e3d72c5$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
|
|
4252
4691
|
const fieldAngleBase = rng() * Math.PI * 2;
|
|
4253
|
-
const fieldFreq =
|
|
4692
|
+
const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
|
|
4254
4693
|
function flowAngle(x, y) {
|
|
4255
|
-
|
|
4694
|
+
// Sample FBM noise at the position, scaled by frequency
|
|
4695
|
+
const nx = x / width * fieldFreq;
|
|
4696
|
+
const ny = y / height * fieldFreq;
|
|
4697
|
+
return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
|
|
4698
|
+
}
|
|
4699
|
+
// Noise-based size modulation — shapes in "high noise" areas get scaled
|
|
4700
|
+
function noiseSizeModulation(x, y) {
|
|
4701
|
+
const n = simplexNoise(x / width * 3, y / height * 3);
|
|
4702
|
+
// Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
|
|
4703
|
+
return 0.7 + (n + 1) * 0.3;
|
|
4256
4704
|
}
|
|
4257
4705
|
// Track all placed shapes for density checks and connecting curves
|
|
4258
4706
|
const shapePositions = [];
|
|
4707
|
+
// Spatial grid for O(1) density and nearest-neighbor lookups
|
|
4708
|
+
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4709
|
+
const spatialGrid = new $1f63dc64b5593c73$var$SpatialGrid(densityCheckRadius);
|
|
4259
4710
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4260
4711
|
let heroCenter = null;
|
|
4261
4712
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4286,7 +4737,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4286
4737
|
glowColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(heroStroke, 0.4),
|
|
4287
4738
|
gradientFillEnd: (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
|
|
4288
4739
|
renderStyle: heroStyle,
|
|
4289
|
-
rng: rng
|
|
4740
|
+
rng: rng,
|
|
4741
|
+
lightAngle: lightAngle,
|
|
4742
|
+
scaleFactor: scaleFactor
|
|
4290
4743
|
});
|
|
4291
4744
|
heroCenter = {
|
|
4292
4745
|
x: heroFocal.x,
|
|
@@ -4299,9 +4752,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4299
4752
|
size: heroSize,
|
|
4300
4753
|
shape: heroShape
|
|
4301
4754
|
});
|
|
4755
|
+
spatialGrid.insert({
|
|
4756
|
+
x: heroFocal.x,
|
|
4757
|
+
y: heroFocal.y,
|
|
4758
|
+
size: heroSize,
|
|
4759
|
+
shape: heroShape
|
|
4760
|
+
});
|
|
4302
4761
|
}
|
|
4303
4762
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4304
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4305
4763
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4306
4764
|
for(let layer = 0; layer < layers; layer++){
|
|
4307
4765
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
@@ -4340,12 +4798,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4340
4798
|
if ($1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones)) {
|
|
4341
4799
|
if (rng() < 0.85) continue;
|
|
4342
4800
|
}
|
|
4343
|
-
if (
|
|
4801
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4344
4802
|
if (rng() < 0.6) continue;
|
|
4345
4803
|
}
|
|
4346
4804
|
// Power distribution for size — archetype controls the curve
|
|
4347
4805
|
const sizeT = Math.pow(rng(), archetype.sizePower);
|
|
4348
|
-
const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
|
|
4806
|
+
const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
|
|
4349
4807
|
// Size fraction for affinity-aware shape selection
|
|
4350
4808
|
const sizeFraction = size / adjustedMaxSize;
|
|
4351
4809
|
// Palette-driven shape selection (replaces naive pickShape)
|
|
@@ -4400,17 +4858,11 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4400
4858
|
let finalX = x;
|
|
4401
4859
|
let finalY = y;
|
|
4402
4860
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4403
|
-
//
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
for (const sp of shapePositions){
|
|
4407
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4408
|
-
if (d < nearestDist && d > 0) {
|
|
4409
|
-
nearestDist = d;
|
|
4410
|
-
nearestPos = sp;
|
|
4411
|
-
}
|
|
4412
|
-
}
|
|
4861
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
4862
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
4863
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4413
4864
|
if (nearestPos) {
|
|
4865
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4414
4866
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4415
4867
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4416
4868
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4448,7 +4900,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4448
4900
|
glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
|
|
4449
4901
|
gradientFillEnd: gradientEnd,
|
|
4450
4902
|
renderStyle: finalRenderStyle,
|
|
4451
|
-
rng: rng
|
|
4903
|
+
rng: rng,
|
|
4904
|
+
lightAngle: lightAngle,
|
|
4905
|
+
scaleFactor: scaleFactor
|
|
4452
4906
|
};
|
|
4453
4907
|
if (shouldMirror) (0, $e0f99502ff383dd8$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
4454
4908
|
...shapeConfig,
|
|
@@ -4481,6 +4935,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4481
4935
|
size: size,
|
|
4482
4936
|
shape: shape
|
|
4483
4937
|
});
|
|
4938
|
+
spatialGrid.insert({
|
|
4939
|
+
x: finalX,
|
|
4940
|
+
y: finalY,
|
|
4941
|
+
size: size,
|
|
4942
|
+
shape: shape
|
|
4943
|
+
});
|
|
4484
4944
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4485
4945
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4486
4946
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
@@ -4509,6 +4969,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4509
4969
|
size: echoSize,
|
|
4510
4970
|
shape: shape
|
|
4511
4971
|
});
|
|
4972
|
+
spatialGrid.insert({
|
|
4973
|
+
x: echoX,
|
|
4974
|
+
y: echoY,
|
|
4975
|
+
size: echoSize,
|
|
4976
|
+
shape: shape
|
|
4977
|
+
});
|
|
4512
4978
|
}
|
|
4513
4979
|
}
|
|
4514
4980
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
@@ -4573,12 +5039,119 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4573
5039
|
size: member.size,
|
|
4574
5040
|
shape: memberShape
|
|
4575
5041
|
});
|
|
5042
|
+
spatialGrid.insert({
|
|
5043
|
+
x: mx,
|
|
5044
|
+
y: my,
|
|
5045
|
+
size: member.size,
|
|
5046
|
+
shape: memberShape
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
5051
|
+
// ~12% of medium-large shapes spawn a rhythmic sequence
|
|
5052
|
+
if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
|
|
5053
|
+
const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
|
|
5054
|
+
const rhythmAngle = rng() * Math.PI * 2;
|
|
5055
|
+
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5056
|
+
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5057
|
+
const rhythmShape = shape; // same shape for visual rhythm
|
|
5058
|
+
let rhythmSize = size * 0.6;
|
|
5059
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5060
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5061
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5062
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5063
|
+
if ($1f63dc64b5593c73$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5064
|
+
rhythmSize *= rhythmDecay;
|
|
5065
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5066
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5067
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5068
|
+
const rhythmFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5069
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5070
|
+
fillColor: rhythmFill,
|
|
5071
|
+
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5072
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5073
|
+
size: rhythmSize,
|
|
5074
|
+
rotation: rotation + (r + 1) * 12,
|
|
5075
|
+
proportionType: "GOLDEN_RATIO",
|
|
5076
|
+
renderStyle: finalRenderStyle,
|
|
5077
|
+
rng: rng
|
|
5078
|
+
});
|
|
5079
|
+
shapePositions.push({
|
|
5080
|
+
x: rx,
|
|
5081
|
+
y: ry,
|
|
5082
|
+
size: rhythmSize,
|
|
5083
|
+
shape: rhythmShape
|
|
5084
|
+
});
|
|
5085
|
+
spatialGrid.insert({
|
|
5086
|
+
x: rx,
|
|
5087
|
+
y: ry,
|
|
5088
|
+
size: rhythmSize,
|
|
5089
|
+
shape: rhythmShape
|
|
5090
|
+
});
|
|
4576
5091
|
}
|
|
4577
5092
|
}
|
|
4578
5093
|
}
|
|
4579
5094
|
}
|
|
4580
5095
|
// Reset blend mode for post-processing passes
|
|
4581
5096
|
ctx.globalCompositeOperation = "source-over";
|
|
5097
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
5098
|
+
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
5099
|
+
// with a tinted background wash, creating a "peek through" effect.
|
|
5100
|
+
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
5101
|
+
const portalCount = 1 + Math.floor(rng() * 2);
|
|
5102
|
+
for(let p = 0; p < portalCount; p++){
|
|
5103
|
+
// Pick a position biased toward placed shapes
|
|
5104
|
+
const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5105
|
+
const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
5106
|
+
const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
5107
|
+
const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
|
|
5108
|
+
// Pick a portal shape from the palette
|
|
5109
|
+
const portalShape = (0, $8286059160ee2e04$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
|
|
5110
|
+
const portalRotation = rng() * 360;
|
|
5111
|
+
const portalAlpha = 0.6 + rng() * 0.35;
|
|
5112
|
+
ctx.save();
|
|
5113
|
+
ctx.translate(portalX, portalY);
|
|
5114
|
+
ctx.rotate(portalRotation * Math.PI / 180);
|
|
5115
|
+
// Step 1: Clip to the portal shape and fill with background wash
|
|
5116
|
+
ctx.beginPath();
|
|
5117
|
+
(0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
|
|
5118
|
+
ctx.clip();
|
|
5119
|
+
// Fill the clipped region with a radial gradient from background colors
|
|
5120
|
+
const portalColor = (0, $b5a262d09b87e373$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
|
|
5121
|
+
const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
|
|
5122
|
+
portalGrad.addColorStop(0, portalColor);
|
|
5123
|
+
portalGrad.addColorStop(1, bgEnd);
|
|
5124
|
+
ctx.globalAlpha = portalAlpha;
|
|
5125
|
+
ctx.fillStyle = portalGrad;
|
|
5126
|
+
ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
|
|
5127
|
+
// Optional: subtle inner texture — a few tiny dots inside the portal
|
|
5128
|
+
if (rng() < 0.5) {
|
|
5129
|
+
const dotCount = 3 + Math.floor(rng() * 5);
|
|
5130
|
+
ctx.globalAlpha = portalAlpha * 0.3;
|
|
5131
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
|
|
5132
|
+
for(let d = 0; d < dotCount; d++){
|
|
5133
|
+
const dx = (rng() - 0.5) * portalSize * 1.4;
|
|
5134
|
+
const dy = (rng() - 0.5) * portalSize * 1.4;
|
|
5135
|
+
const dr = (1 + rng() * 3) * scaleFactor;
|
|
5136
|
+
ctx.beginPath();
|
|
5137
|
+
ctx.arc(dx, dy, dr, 0, Math.PI * 2);
|
|
5138
|
+
ctx.fill();
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
ctx.restore();
|
|
5142
|
+
// Step 2: Draw a border ring around the portal (outside the clip)
|
|
5143
|
+
ctx.save();
|
|
5144
|
+
ctx.translate(portalX, portalY);
|
|
5145
|
+
ctx.rotate(portalRotation * Math.PI / 180);
|
|
5146
|
+
ctx.globalAlpha = 0.15 + rng() * 0.2;
|
|
5147
|
+
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
|
|
5148
|
+
ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
|
|
5149
|
+
ctx.beginPath();
|
|
5150
|
+
(0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
|
|
5151
|
+
ctx.stroke();
|
|
5152
|
+
ctx.restore();
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
4582
5155
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
4583
5156
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
4584
5157
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
@@ -4602,6 +5175,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4602
5175
|
fx += Math.cos(angle) * stepLen;
|
|
4603
5176
|
fy += Math.sin(angle) * stepLen;
|
|
4604
5177
|
if (fx < 0 || fx > width || fy < 0 || fy > height) break;
|
|
5178
|
+
// Skip segments that pass through void zones
|
|
5179
|
+
if ($1f63dc64b5593c73$var$isInVoidZone(fx, fy, voidZones)) {
|
|
5180
|
+
prevX = fx;
|
|
5181
|
+
prevY = fy;
|
|
5182
|
+
continue;
|
|
5183
|
+
}
|
|
4605
5184
|
const t = s / steps;
|
|
4606
5185
|
// Taper + pressure
|
|
4607
5186
|
const taper = 1 - t * 0.8;
|
|
@@ -4699,30 +5278,60 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4699
5278
|
}
|
|
4700
5279
|
ctx.restore();
|
|
4701
5280
|
}
|
|
4702
|
-
// ── 7. Noise texture overlay
|
|
5281
|
+
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
4703
5282
|
const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
|
|
4704
5283
|
const noiseDensity = Math.floor(width * height / 800);
|
|
4705
|
-
|
|
4706
|
-
const
|
|
4707
|
-
const
|
|
4708
|
-
const
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
5284
|
+
try {
|
|
5285
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5286
|
+
const data = imageData.data;
|
|
5287
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5288
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5289
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5290
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5291
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5292
|
+
const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
|
|
5293
|
+
// Write a small block of pixels for scale
|
|
5294
|
+
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5295
|
+
const idx = ((ny + dy) * width + (nx + dx)) * 4;
|
|
5296
|
+
// Alpha-blend the noise dot onto existing pixel data
|
|
5297
|
+
const srcA = alpha / 255;
|
|
5298
|
+
const invA = 1 - srcA;
|
|
5299
|
+
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5300
|
+
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5301
|
+
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5302
|
+
// Keep existing alpha
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
ctx.putImageData(imageData, 0, 0);
|
|
5306
|
+
} catch {
|
|
5307
|
+
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5308
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5309
|
+
const nx = noiseRng() * width;
|
|
5310
|
+
const ny = noiseRng() * height;
|
|
5311
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5312
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5313
|
+
ctx.globalAlpha = alpha;
|
|
5314
|
+
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5315
|
+
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5316
|
+
}
|
|
4713
5317
|
}
|
|
4714
5318
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
4715
5319
|
ctx.globalAlpha = 1;
|
|
4716
5320
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
4717
5321
|
const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
|
|
5322
|
+
// Tint vignette based on background: warm sepia for light, cool blue for dark
|
|
5323
|
+
const isLightBg = bgLum > 0.5;
|
|
5324
|
+
const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
|
|
5325
|
+
: `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
|
|
4718
5326
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
4719
5327
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
4720
|
-
vigGrad.addColorStop(1,
|
|
5328
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
4721
5329
|
ctx.fillStyle = vigGrad;
|
|
4722
5330
|
ctx.fillRect(0, 0, width, height);
|
|
4723
|
-
// ── 9. Organic connecting curves
|
|
5331
|
+
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
4724
5332
|
if (shapePositions.length > 1) {
|
|
4725
5333
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5334
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
4726
5335
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
4727
5336
|
for(let i = 0; i < numCurves; i++){
|
|
4728
5337
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
@@ -4730,11 +5339,13 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4730
5339
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
4731
5340
|
const a = shapePositions[idxA];
|
|
4732
5341
|
const b = shapePositions[idxB];
|
|
4733
|
-
const mx = (a.x + b.x) / 2;
|
|
4734
|
-
const my = (a.y + b.y) / 2;
|
|
4735
5342
|
const dx = b.x - a.x;
|
|
4736
5343
|
const dy = b.y - a.y;
|
|
4737
5344
|
const dist = Math.hypot(dx, dy);
|
|
5345
|
+
// Skip connections between distant shapes
|
|
5346
|
+
if (dist > maxCurveDist) continue;
|
|
5347
|
+
const mx = (a.x + b.x) / 2;
|
|
5348
|
+
const my = (a.y + b.y) / 2;
|
|
4738
5349
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
4739
5350
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
4740
5351
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
@@ -4790,13 +5401,211 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4790
5401
|
ctx.restore();
|
|
4791
5402
|
ctx.globalCompositeOperation = "source-over";
|
|
4792
5403
|
}
|
|
4793
|
-
//
|
|
5404
|
+
// 10d. Gradient map — map luminance through a two-color gradient
|
|
5405
|
+
// Uses dominant→accent as the dark→light ramp for a cohesive tonal look
|
|
5406
|
+
if (rng() < 0.35) {
|
|
5407
|
+
const gmDark = colorHierarchy.dominant;
|
|
5408
|
+
const gmLight = colorHierarchy.accent;
|
|
5409
|
+
ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
|
|
5410
|
+
ctx.globalCompositeOperation = "color";
|
|
5411
|
+
// Paint a linear gradient from dark color (top) to light color (bottom)
|
|
5412
|
+
const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
|
|
5413
|
+
gmGrad.addColorStop(0, gmDark);
|
|
5414
|
+
gmGrad.addColorStop(1, gmLight);
|
|
5415
|
+
ctx.fillStyle = gmGrad;
|
|
5416
|
+
ctx.fillRect(0, 0, width, height);
|
|
5417
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5418
|
+
}
|
|
5419
|
+
// ── 10e. Generative borders — archetype-driven decorative frames ──
|
|
5420
|
+
{
|
|
5421
|
+
ctx.save();
|
|
5422
|
+
ctx.globalAlpha = 1;
|
|
5423
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5424
|
+
const borderRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 314));
|
|
5425
|
+
const borderPad = Math.min(width, height) * 0.025;
|
|
5426
|
+
const borderColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
|
|
5427
|
+
const borderColorSolid = colorHierarchy.accent;
|
|
5428
|
+
const archName = archetype.name;
|
|
5429
|
+
if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
|
|
5430
|
+
// Clean ruled lines with corner ornaments
|
|
5431
|
+
ctx.strokeStyle = borderColor;
|
|
5432
|
+
ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
|
|
5433
|
+
ctx.globalAlpha = 0.18 + borderRng() * 0.1;
|
|
5434
|
+
// Outer rule
|
|
5435
|
+
ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
|
|
5436
|
+
// Inner rule (thinner, offset)
|
|
5437
|
+
const innerPad = borderPad * 1.8;
|
|
5438
|
+
ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
|
|
5439
|
+
ctx.globalAlpha *= 0.7;
|
|
5440
|
+
ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
|
|
5441
|
+
// Corner ornaments — small squares at each corner
|
|
5442
|
+
const ornSize = borderPad * 0.6;
|
|
5443
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(borderColorSolid, 0.12);
|
|
5444
|
+
const corners = [
|
|
5445
|
+
[
|
|
5446
|
+
borderPad,
|
|
5447
|
+
borderPad
|
|
5448
|
+
],
|
|
5449
|
+
[
|
|
5450
|
+
width - borderPad - ornSize,
|
|
5451
|
+
borderPad
|
|
5452
|
+
],
|
|
5453
|
+
[
|
|
5454
|
+
borderPad,
|
|
5455
|
+
height - borderPad - ornSize
|
|
5456
|
+
],
|
|
5457
|
+
[
|
|
5458
|
+
width - borderPad - ornSize,
|
|
5459
|
+
height - borderPad - ornSize
|
|
5460
|
+
]
|
|
5461
|
+
];
|
|
5462
|
+
for (const [cx2, cy2] of corners){
|
|
5463
|
+
ctx.fillRect(cx2, cy2, ornSize, ornSize);
|
|
5464
|
+
// Diagonal cross inside ornament
|
|
5465
|
+
ctx.beginPath();
|
|
5466
|
+
ctx.moveTo(cx2, cy2);
|
|
5467
|
+
ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
|
|
5468
|
+
ctx.moveTo(cx2 + ornSize, cy2);
|
|
5469
|
+
ctx.lineTo(cx2, cy2 + ornSize);
|
|
5470
|
+
ctx.stroke();
|
|
5471
|
+
}
|
|
5472
|
+
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5473
|
+
// Vine tendrils — organic curving lines along edges
|
|
5474
|
+
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5475
|
+
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5476
|
+
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5477
|
+
ctx.lineCap = "round";
|
|
5478
|
+
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5479
|
+
for(let t = 0; t < tendrilCount; t++){
|
|
5480
|
+
// Start from a random edge point
|
|
5481
|
+
const edge = Math.floor(borderRng() * 4);
|
|
5482
|
+
let tx, ty;
|
|
5483
|
+
if (edge === 0) {
|
|
5484
|
+
tx = borderRng() * width;
|
|
5485
|
+
ty = borderPad;
|
|
5486
|
+
} else if (edge === 1) {
|
|
5487
|
+
tx = borderRng() * width;
|
|
5488
|
+
ty = height - borderPad;
|
|
5489
|
+
} else if (edge === 2) {
|
|
5490
|
+
tx = borderPad;
|
|
5491
|
+
ty = borderRng() * height;
|
|
5492
|
+
} else {
|
|
5493
|
+
tx = width - borderPad;
|
|
5494
|
+
ty = borderRng() * height;
|
|
5495
|
+
}
|
|
5496
|
+
ctx.beginPath();
|
|
5497
|
+
ctx.moveTo(tx, ty);
|
|
5498
|
+
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5499
|
+
for(let s = 0; s < segs; s++){
|
|
5500
|
+
const inward = borderPad * (1 + borderRng() * 2);
|
|
5501
|
+
// Curl inward from edge
|
|
5502
|
+
const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
|
|
5503
|
+
const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
|
|
5504
|
+
const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
|
|
5505
|
+
const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
|
|
5506
|
+
tx = cpx3;
|
|
5507
|
+
ty = cpy3;
|
|
5508
|
+
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5509
|
+
}
|
|
5510
|
+
ctx.stroke();
|
|
5511
|
+
// Small leaf/dot at tendril end
|
|
5512
|
+
if (borderRng() < 0.6) {
|
|
5513
|
+
ctx.beginPath();
|
|
5514
|
+
ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
|
|
5515
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5516
|
+
ctx.fill();
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5520
|
+
// Star-studded arcs along edges
|
|
5521
|
+
ctx.globalAlpha = 0.1 + borderRng() * 0.08;
|
|
5522
|
+
ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
|
|
5523
|
+
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
|
|
5524
|
+
ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
|
|
5525
|
+
// Subtle arc along top and bottom
|
|
5526
|
+
ctx.beginPath();
|
|
5527
|
+
ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
|
|
5528
|
+
ctx.stroke();
|
|
5529
|
+
ctx.beginPath();
|
|
5530
|
+
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5531
|
+
ctx.stroke();
|
|
5532
|
+
// Scatter small stars along the border region
|
|
5533
|
+
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5534
|
+
for(let s = 0; s < starCount; s++){
|
|
5535
|
+
const edge = Math.floor(borderRng() * 4);
|
|
5536
|
+
let sx, sy;
|
|
5537
|
+
if (edge === 0) {
|
|
5538
|
+
sx = borderRng() * width;
|
|
5539
|
+
sy = borderPad * (0.5 + borderRng());
|
|
5540
|
+
} else if (edge === 1) {
|
|
5541
|
+
sx = borderRng() * width;
|
|
5542
|
+
sy = height - borderPad * (0.5 + borderRng());
|
|
5543
|
+
} else if (edge === 2) {
|
|
5544
|
+
sx = borderPad * (0.5 + borderRng());
|
|
5545
|
+
sy = borderRng() * height;
|
|
5546
|
+
} else {
|
|
5547
|
+
sx = width - borderPad * (0.5 + borderRng());
|
|
5548
|
+
sy = borderRng() * height;
|
|
5549
|
+
}
|
|
5550
|
+
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5551
|
+
// 4-point star
|
|
5552
|
+
ctx.beginPath();
|
|
5553
|
+
for(let p = 0; p < 8; p++){
|
|
5554
|
+
const a = p / 8 * Math.PI * 2;
|
|
5555
|
+
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
5556
|
+
const px2 = sx + Math.cos(a) * r;
|
|
5557
|
+
const py2 = sy + Math.sin(a) * r;
|
|
5558
|
+
if (p === 0) ctx.moveTo(px2, py2);
|
|
5559
|
+
else ctx.lineTo(px2, py2);
|
|
5560
|
+
}
|
|
5561
|
+
ctx.closePath();
|
|
5562
|
+
ctx.fill();
|
|
5563
|
+
}
|
|
5564
|
+
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5565
|
+
// Thin single rule — understated elegance
|
|
5566
|
+
ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
5567
|
+
ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
|
|
5568
|
+
ctx.globalAlpha = 0.1 + borderRng() * 0.06;
|
|
5569
|
+
ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
|
|
5570
|
+
}
|
|
5571
|
+
// Other archetypes: no border (intentional — not every image needs one)
|
|
5572
|
+
ctx.restore();
|
|
5573
|
+
}
|
|
5574
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
4794
5575
|
{
|
|
4795
5576
|
const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
|
|
4796
5577
|
const sigSize = Math.min(width, height) * 0.025;
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
const
|
|
5578
|
+
const sigMargin = sigSize * 2.5;
|
|
5579
|
+
// Find the corner with the lowest local density
|
|
5580
|
+
const cornerCandidates = [
|
|
5581
|
+
{
|
|
5582
|
+
x: sigMargin,
|
|
5583
|
+
y: sigMargin
|
|
5584
|
+
},
|
|
5585
|
+
{
|
|
5586
|
+
x: width - sigMargin,
|
|
5587
|
+
y: sigMargin
|
|
5588
|
+
},
|
|
5589
|
+
{
|
|
5590
|
+
x: sigMargin,
|
|
5591
|
+
y: height - sigMargin
|
|
5592
|
+
},
|
|
5593
|
+
{
|
|
5594
|
+
x: width - sigMargin,
|
|
5595
|
+
y: height - sigMargin
|
|
5596
|
+
}
|
|
5597
|
+
];
|
|
5598
|
+
let bestCorner = cornerCandidates[3]; // default: bottom-right
|
|
5599
|
+
let minDensity = Infinity;
|
|
5600
|
+
for (const corner of cornerCandidates){
|
|
5601
|
+
const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
|
|
5602
|
+
if (density < minDensity) {
|
|
5603
|
+
minDensity = density;
|
|
5604
|
+
bestCorner = corner;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
const sigX = bestCorner.x;
|
|
5608
|
+
const sigY = bestCorner.y;
|
|
4800
5609
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
4801
5610
|
const sigColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
4802
5611
|
ctx.save();
|