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/main.js
CHANGED
|
@@ -76,6 +76,136 @@ const $e4b03e131ed2a289$export$bb9e4790bc99ae59 = {
|
|
|
76
76
|
PI: Math.PI,
|
|
77
77
|
PHI: (1 + Math.sqrt(5)) / 2
|
|
78
78
|
};
|
|
79
|
+
function $e4b03e131ed2a289$export$bbde7fbaaf9a8d66(rng) {
|
|
80
|
+
// Build a deterministic permutation table (256 entries, doubled)
|
|
81
|
+
const perm = new Uint8Array(512);
|
|
82
|
+
const p = new Uint8Array(256);
|
|
83
|
+
for(let i = 0; i < 256; i++)p[i] = i;
|
|
84
|
+
// Fisher-Yates shuffle with our seeded RNG
|
|
85
|
+
for(let i = 255; i > 0; i--){
|
|
86
|
+
const j = Math.floor(rng() * (i + 1));
|
|
87
|
+
const tmp = p[i];
|
|
88
|
+
p[i] = p[j];
|
|
89
|
+
p[j] = tmp;
|
|
90
|
+
}
|
|
91
|
+
for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
|
|
92
|
+
// 12 gradient vectors for 2D simplex
|
|
93
|
+
const GRAD2 = [
|
|
94
|
+
[
|
|
95
|
+
1,
|
|
96
|
+
1
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
-1,
|
|
100
|
+
1
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
1,
|
|
104
|
+
-1
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
-1,
|
|
108
|
+
-1
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
1,
|
|
112
|
+
0
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
-1,
|
|
116
|
+
0
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
0,
|
|
120
|
+
1
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
0,
|
|
124
|
+
-1
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
1,
|
|
128
|
+
1
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
-1,
|
|
132
|
+
1
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
1,
|
|
136
|
+
-1
|
|
137
|
+
],
|
|
138
|
+
[
|
|
139
|
+
-1,
|
|
140
|
+
-1
|
|
141
|
+
]
|
|
142
|
+
];
|
|
143
|
+
const F2 = 0.5 * (Math.sqrt(3) - 1);
|
|
144
|
+
const G2 = (3 - Math.sqrt(3)) / 6;
|
|
145
|
+
function dot2(g, x, y) {
|
|
146
|
+
return g[0] * x + g[1] * y;
|
|
147
|
+
}
|
|
148
|
+
return function noise2D(xin, yin) {
|
|
149
|
+
const s = (xin + yin) * F2;
|
|
150
|
+
const i = Math.floor(xin + s);
|
|
151
|
+
const j = Math.floor(yin + s);
|
|
152
|
+
const t = (i + j) * G2;
|
|
153
|
+
const X0 = i - t;
|
|
154
|
+
const Y0 = j - t;
|
|
155
|
+
const x0 = xin - X0;
|
|
156
|
+
const y0 = yin - Y0;
|
|
157
|
+
let i1, j1;
|
|
158
|
+
if (x0 > y0) {
|
|
159
|
+
i1 = 1;
|
|
160
|
+
j1 = 0;
|
|
161
|
+
} else {
|
|
162
|
+
i1 = 0;
|
|
163
|
+
j1 = 1;
|
|
164
|
+
}
|
|
165
|
+
const x1 = x0 - i1 + G2;
|
|
166
|
+
const y1 = y0 - j1 + G2;
|
|
167
|
+
const x2 = x0 - 1 + 2 * G2;
|
|
168
|
+
const y2 = y0 - 1 + 2 * G2;
|
|
169
|
+
const ii = i & 255;
|
|
170
|
+
const jj = j & 255;
|
|
171
|
+
let n0 = 0, n1 = 0, n2 = 0;
|
|
172
|
+
let t0 = 0.5 - x0 * x0 - y0 * y0;
|
|
173
|
+
if (t0 >= 0) {
|
|
174
|
+
t0 *= t0;
|
|
175
|
+
const gi0 = perm[ii + perm[jj]] % 12;
|
|
176
|
+
n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
|
|
177
|
+
}
|
|
178
|
+
let t1 = 0.5 - x1 * x1 - y1 * y1;
|
|
179
|
+
if (t1 >= 0) {
|
|
180
|
+
t1 *= t1;
|
|
181
|
+
const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
|
|
182
|
+
n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
|
|
183
|
+
}
|
|
184
|
+
let t2 = 0.5 - x2 * x2 - y2 * y2;
|
|
185
|
+
if (t2 >= 0) {
|
|
186
|
+
t2 *= t2;
|
|
187
|
+
const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
|
|
188
|
+
n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
|
|
189
|
+
}
|
|
190
|
+
// Scale to approximately [-1, 1]
|
|
191
|
+
return 70 * (n0 + n1 + n2);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function $e4b03e131ed2a289$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
|
|
195
|
+
return function fbm(x, y) {
|
|
196
|
+
let value = 0;
|
|
197
|
+
let amplitude = 1;
|
|
198
|
+
let frequency = 1;
|
|
199
|
+
let maxAmp = 0;
|
|
200
|
+
for(let i = 0; i < octaves; i++){
|
|
201
|
+
value += noise(x * frequency, y * frequency) * amplitude;
|
|
202
|
+
maxAmp += amplitude;
|
|
203
|
+
amplitude *= gain;
|
|
204
|
+
frequency *= lacunarity;
|
|
205
|
+
}
|
|
206
|
+
return value / maxAmp;
|
|
207
|
+
};
|
|
208
|
+
}
|
|
79
209
|
class $e4b03e131ed2a289$export$da2372f11bc66b3f {
|
|
80
210
|
static getProportionalSize(baseSize, proportion) {
|
|
81
211
|
return baseSize * proportion;
|
|
@@ -277,6 +407,48 @@ class $d016ad53434219a1$export$ab958c550f521376 {
|
|
|
277
407
|
$d016ad53434219a1$var$hslToHex(baseHue, 0.7, 0.35)
|
|
278
408
|
];
|
|
279
409
|
}
|
|
410
|
+
case "split-complementary":
|
|
411
|
+
{
|
|
412
|
+
// Base hue + two colors flanking the complement (±30°)
|
|
413
|
+
const comp = (baseHue + 180) % 360;
|
|
414
|
+
const split1 = (comp - 30 + 360) % 360;
|
|
415
|
+
const split2 = (comp + 30) % 360;
|
|
416
|
+
const sat = 0.55 + this.rng() * 0.25;
|
|
417
|
+
return [
|
|
418
|
+
$d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
|
|
419
|
+
$d016ad53434219a1$var$hslToHex(baseHue, sat * 0.8, 0.65),
|
|
420
|
+
$d016ad53434219a1$var$hslToHex(split1, sat, 0.5),
|
|
421
|
+
$d016ad53434219a1$var$hslToHex(split2, sat, 0.5),
|
|
422
|
+
$d016ad53434219a1$var$hslToHex(split1, sat * 0.7, 0.7)
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
case "analogous-accent":
|
|
426
|
+
{
|
|
427
|
+
// Tight cluster of 3 analogous hues + 1 distant accent
|
|
428
|
+
const step = 15 + this.rng() * 20; // 15-35° apart
|
|
429
|
+
const h1 = (baseHue - step + 360) % 360;
|
|
430
|
+
const h2 = (baseHue + step) % 360;
|
|
431
|
+
const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
|
|
432
|
+
const sat = 0.5 + this.rng() * 0.3;
|
|
433
|
+
return [
|
|
434
|
+
$d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
|
|
435
|
+
$d016ad53434219a1$var$hslToHex(h1, sat, 0.55),
|
|
436
|
+
$d016ad53434219a1$var$hslToHex(h2, sat, 0.45),
|
|
437
|
+
$d016ad53434219a1$var$hslToHex(accentHue, sat + 0.15, 0.5)
|
|
438
|
+
];
|
|
439
|
+
}
|
|
440
|
+
case "limited-palette":
|
|
441
|
+
{
|
|
442
|
+
// Only 3 colors — like a risograph print
|
|
443
|
+
const h2 = (baseHue + 120 + this.rng() * 40) % 360;
|
|
444
|
+
const h3 = (baseHue + 220 + this.rng() * 40) % 360;
|
|
445
|
+
const sat = 0.6 + this.rng() * 0.2;
|
|
446
|
+
return [
|
|
447
|
+
$d016ad53434219a1$var$hslToHex(baseHue, sat, 0.5),
|
|
448
|
+
$d016ad53434219a1$var$hslToHex(h2, sat, 0.5),
|
|
449
|
+
$d016ad53434219a1$var$hslToHex(h3, sat * 0.9, 0.55)
|
|
450
|
+
];
|
|
451
|
+
}
|
|
280
452
|
case "harmonious":
|
|
281
453
|
default:
|
|
282
454
|
return this.getColors();
|
|
@@ -297,6 +469,14 @@ class $d016ad53434219a1$export$ab958c550f521376 {
|
|
|
297
469
|
"#f5f5f0",
|
|
298
470
|
"#e8e8e0"
|
|
299
471
|
];
|
|
472
|
+
case "split-complementary":
|
|
473
|
+
case "analogous-accent":
|
|
474
|
+
return this.getBackgroundColors();
|
|
475
|
+
case "limited-palette":
|
|
476
|
+
return [
|
|
477
|
+
$d016ad53434219a1$var$hslToHex(this.seed % 360, 0.08, 0.94),
|
|
478
|
+
$d016ad53434219a1$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
|
|
479
|
+
];
|
|
300
480
|
case "neon":
|
|
301
481
|
return [
|
|
302
482
|
"#0a0a12",
|
|
@@ -423,15 +603,17 @@ function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
|
|
|
423
603
|
accent: colors[colors.length - 1] || "#888888",
|
|
424
604
|
all: colors
|
|
425
605
|
};
|
|
426
|
-
// Pick dominant as the color
|
|
606
|
+
// Pick dominant as the color with the highest chroma (saturation × distance from gray)
|
|
607
|
+
// This selects the most visually prominent color rather than the average
|
|
427
608
|
const hsls = colors.map((c)=>$d016ad53434219a1$var$hexToHsl(c));
|
|
428
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
429
609
|
let dominantIdx = 0;
|
|
430
|
-
let
|
|
610
|
+
let maxChroma = -1;
|
|
431
611
|
for(let i = 0; i < hsls.length; i++){
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
612
|
+
// Chroma approximation: saturation × how far lightness is from 50% (gray)
|
|
613
|
+
const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
|
|
614
|
+
const chroma = hsls[i][1] * lightnessVibrancy;
|
|
615
|
+
if (chroma > maxChroma) {
|
|
616
|
+
maxChroma = chroma;
|
|
435
617
|
dominantIdx = i;
|
|
436
618
|
}
|
|
437
619
|
}
|
|
@@ -550,7 +732,8 @@ function $d016ad53434219a1$export$703ba40a4347f77a(base, layerRatio, hueShiftPer
|
|
|
550
732
|
return {
|
|
551
733
|
dominant: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.dominant, shift),
|
|
552
734
|
secondary: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
|
|
553
|
-
accent: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
|
|
735
|
+
accent: $d016ad53434219a1$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
|
|
736
|
+
all: base.all.map((c)=>$d016ad53434219a1$export$1793a1bfbe4f6ff5(c, shift * 0.6))
|
|
554
737
|
};
|
|
555
738
|
}
|
|
556
739
|
|
|
@@ -1933,6 +2116,23 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
1933
2116
|
ctx.fill();
|
|
1934
2117
|
ctx.fillStyle = origFill;
|
|
1935
2118
|
ctx.restore();
|
|
2119
|
+
// Pass 4: Organic edge erosion — irregular bites along the boundary
|
|
2120
|
+
if (rng && size > 20) {
|
|
2121
|
+
const erosionBites = 6 + Math.floor(rng() * 8);
|
|
2122
|
+
const edgeRadius = size * 0.45;
|
|
2123
|
+
ctx.save();
|
|
2124
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
2125
|
+
ctx.globalAlpha = 0.6 + rng() * 0.3;
|
|
2126
|
+
for(let eb = 0; eb < erosionBites; eb++){
|
|
2127
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
2128
|
+
const biteDist = edgeRadius * (0.85 + rng() * 0.25);
|
|
2129
|
+
const biteR = size * (0.02 + rng() * 0.04);
|
|
2130
|
+
ctx.beginPath();
|
|
2131
|
+
ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
|
|
2132
|
+
ctx.fill();
|
|
2133
|
+
}
|
|
2134
|
+
ctx.restore();
|
|
2135
|
+
}
|
|
1936
2136
|
ctx.globalAlpha = savedAlpha;
|
|
1937
2137
|
// Soft stroke on top — thinner than normal for delicacy
|
|
1938
2138
|
ctx.globalAlpha *= 0.25;
|
|
@@ -2242,6 +2442,23 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2242
2442
|
ctx.stroke();
|
|
2243
2443
|
ctx.restore();
|
|
2244
2444
|
}
|
|
2445
|
+
// Organic edge erosion — small irregular bites for rough paper feel
|
|
2446
|
+
if (rng && size > 20) {
|
|
2447
|
+
const erosionBites = 4 + Math.floor(rng() * 6);
|
|
2448
|
+
const edgeRadius = size * 0.42;
|
|
2449
|
+
ctx.save();
|
|
2450
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
2451
|
+
ctx.globalAlpha = 0.5 + rng() * 0.3;
|
|
2452
|
+
for(let eb = 0; eb < erosionBites; eb++){
|
|
2453
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
2454
|
+
const biteDist = edgeRadius * (0.9 + rng() * 0.2);
|
|
2455
|
+
const biteR = size * (0.015 + rng() * 0.03);
|
|
2456
|
+
ctx.beginPath();
|
|
2457
|
+
ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
|
|
2458
|
+
ctx.fill();
|
|
2459
|
+
}
|
|
2460
|
+
ctx.restore();
|
|
2461
|
+
}
|
|
2245
2462
|
ctx.globalAlpha = savedAlphaHD;
|
|
2246
2463
|
break;
|
|
2247
2464
|
}
|
|
@@ -2253,12 +2470,20 @@ function $c3de8257a8baa3b0$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2253
2470
|
}
|
|
2254
2471
|
}
|
|
2255
2472
|
function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
2256
|
-
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;
|
|
2473
|
+
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;
|
|
2257
2474
|
ctx.save();
|
|
2258
2475
|
ctx.translate(x, y);
|
|
2259
2476
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2260
|
-
//
|
|
2261
|
-
if (
|
|
2477
|
+
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2478
|
+
if (lightAngle !== undefined && size > 10) {
|
|
2479
|
+
const shadowDist = size * 0.035;
|
|
2480
|
+
const shadowBlurR = size * 0.06;
|
|
2481
|
+
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2482
|
+
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2483
|
+
ctx.shadowBlur = shadowBlurR;
|
|
2484
|
+
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2485
|
+
} else if (glowRadius > 0) {
|
|
2486
|
+
// Glow / shadow effect (legacy path)
|
|
2262
2487
|
ctx.shadowBlur = glowRadius;
|
|
2263
2488
|
ctx.shadowColor = glowColor || fillColor;
|
|
2264
2489
|
ctx.shadowOffsetX = 0;
|
|
@@ -2280,8 +2505,39 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2280
2505
|
});
|
|
2281
2506
|
$c3de8257a8baa3b0$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2282
2507
|
}
|
|
2283
|
-
// Reset shadow so patterns aren't double-
|
|
2284
|
-
|
|
2508
|
+
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2509
|
+
ctx.shadowBlur = 0;
|
|
2510
|
+
ctx.shadowOffsetX = 0;
|
|
2511
|
+
ctx.shadowOffsetY = 0;
|
|
2512
|
+
ctx.shadowColor = "transparent";
|
|
2513
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2514
|
+
if (lightAngle !== undefined && size > 15 && rng) {
|
|
2515
|
+
const hlRadius = size * 0.35;
|
|
2516
|
+
const hlDist = size * 0.15;
|
|
2517
|
+
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2518
|
+
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2519
|
+
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2520
|
+
// Tint highlight warm/cool based on fill color for cohesion
|
|
2521
|
+
// Parse fill to detect warmth — fallback to white for non-parseable
|
|
2522
|
+
let hlBase = "255,255,255";
|
|
2523
|
+
if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
|
|
2524
|
+
const r = parseInt(fillColor.slice(1, 3), 16);
|
|
2525
|
+
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2526
|
+
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2527
|
+
// Blend toward white but keep a hint of the fill's warmth
|
|
2528
|
+
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2529
|
+
}
|
|
2530
|
+
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2531
|
+
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2532
|
+
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2533
|
+
const savedOp = ctx.globalCompositeOperation;
|
|
2534
|
+
ctx.globalCompositeOperation = "soft-light";
|
|
2535
|
+
ctx.fillStyle = hlGrad;
|
|
2536
|
+
ctx.beginPath();
|
|
2537
|
+
ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
|
|
2538
|
+
ctx.fill();
|
|
2539
|
+
ctx.globalCompositeOperation = savedOp;
|
|
2540
|
+
}
|
|
2285
2541
|
// Layer additional patterns if specified
|
|
2286
2542
|
if (patterns.length > 0) (0, $e4b03e131ed2a289$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
|
|
2287
2543
|
baseSize: size,
|
|
@@ -3335,6 +3591,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3335
3591
|
"watercolor",
|
|
3336
3592
|
"fill-only"
|
|
3337
3593
|
],
|
|
3594
|
+
preferredCompositions: [
|
|
3595
|
+
"clustered",
|
|
3596
|
+
"flow-field",
|
|
3597
|
+
"radial"
|
|
3598
|
+
],
|
|
3338
3599
|
flowLineMultiplier: 2.5,
|
|
3339
3600
|
heroShape: false,
|
|
3340
3601
|
glowMultiplier: 0.5,
|
|
@@ -3356,6 +3617,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3356
3617
|
"stroke-only",
|
|
3357
3618
|
"incomplete"
|
|
3358
3619
|
],
|
|
3620
|
+
preferredCompositions: [
|
|
3621
|
+
"golden-spiral",
|
|
3622
|
+
"grid-subdivision"
|
|
3623
|
+
],
|
|
3359
3624
|
flowLineMultiplier: 0.3,
|
|
3360
3625
|
heroShape: true,
|
|
3361
3626
|
glowMultiplier: 0,
|
|
@@ -3377,6 +3642,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3377
3642
|
"fill-only",
|
|
3378
3643
|
"incomplete"
|
|
3379
3644
|
],
|
|
3645
|
+
preferredCompositions: [
|
|
3646
|
+
"flow-field",
|
|
3647
|
+
"golden-spiral",
|
|
3648
|
+
"spiral"
|
|
3649
|
+
],
|
|
3380
3650
|
flowLineMultiplier: 4,
|
|
3381
3651
|
heroShape: false,
|
|
3382
3652
|
glowMultiplier: 0.3,
|
|
@@ -3399,6 +3669,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3399
3669
|
"double-stroke",
|
|
3400
3670
|
"hatched"
|
|
3401
3671
|
],
|
|
3672
|
+
preferredCompositions: [
|
|
3673
|
+
"grid-subdivision",
|
|
3674
|
+
"radial"
|
|
3675
|
+
],
|
|
3402
3676
|
flowLineMultiplier: 0,
|
|
3403
3677
|
heroShape: false,
|
|
3404
3678
|
glowMultiplier: 0,
|
|
@@ -3420,6 +3694,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3420
3694
|
"incomplete",
|
|
3421
3695
|
"fill-only"
|
|
3422
3696
|
],
|
|
3697
|
+
preferredCompositions: [
|
|
3698
|
+
"golden-spiral",
|
|
3699
|
+
"radial",
|
|
3700
|
+
"spiral"
|
|
3701
|
+
],
|
|
3423
3702
|
flowLineMultiplier: 1.5,
|
|
3424
3703
|
heroShape: true,
|
|
3425
3704
|
glowMultiplier: 2,
|
|
@@ -3440,6 +3719,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3440
3719
|
"fill-and-stroke",
|
|
3441
3720
|
"double-stroke"
|
|
3442
3721
|
],
|
|
3722
|
+
preferredCompositions: [
|
|
3723
|
+
"grid-subdivision",
|
|
3724
|
+
"golden-spiral"
|
|
3725
|
+
],
|
|
3443
3726
|
flowLineMultiplier: 0,
|
|
3444
3727
|
heroShape: true,
|
|
3445
3728
|
glowMultiplier: 0,
|
|
@@ -3461,6 +3744,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3461
3744
|
"double-stroke",
|
|
3462
3745
|
"dashed"
|
|
3463
3746
|
],
|
|
3747
|
+
preferredCompositions: [
|
|
3748
|
+
"radial",
|
|
3749
|
+
"spiral",
|
|
3750
|
+
"clustered"
|
|
3751
|
+
],
|
|
3464
3752
|
flowLineMultiplier: 2,
|
|
3465
3753
|
heroShape: true,
|
|
3466
3754
|
glowMultiplier: 3,
|
|
@@ -3483,6 +3771,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3483
3771
|
"stroke-only",
|
|
3484
3772
|
"dashed"
|
|
3485
3773
|
],
|
|
3774
|
+
preferredCompositions: [
|
|
3775
|
+
"flow-field",
|
|
3776
|
+
"grid-subdivision",
|
|
3777
|
+
"clustered"
|
|
3778
|
+
],
|
|
3486
3779
|
flowLineMultiplier: 1.5,
|
|
3487
3780
|
heroShape: false,
|
|
3488
3781
|
glowMultiplier: 0,
|
|
@@ -3504,6 +3797,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3504
3797
|
"watercolor",
|
|
3505
3798
|
"fill-and-stroke"
|
|
3506
3799
|
],
|
|
3800
|
+
preferredCompositions: [
|
|
3801
|
+
"radial",
|
|
3802
|
+
"spiral",
|
|
3803
|
+
"golden-spiral"
|
|
3804
|
+
],
|
|
3507
3805
|
flowLineMultiplier: 3,
|
|
3508
3806
|
heroShape: true,
|
|
3509
3807
|
glowMultiplier: 2.5,
|
|
@@ -3525,6 +3823,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3525
3823
|
"fill-only",
|
|
3526
3824
|
"incomplete"
|
|
3527
3825
|
],
|
|
3826
|
+
preferredCompositions: [
|
|
3827
|
+
"golden-spiral",
|
|
3828
|
+
"flow-field",
|
|
3829
|
+
"radial"
|
|
3830
|
+
],
|
|
3528
3831
|
flowLineMultiplier: 0.5,
|
|
3529
3832
|
heroShape: false,
|
|
3530
3833
|
glowMultiplier: 0.3,
|
|
@@ -3546,6 +3849,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3546
3849
|
"stroke-only",
|
|
3547
3850
|
"dashed"
|
|
3548
3851
|
],
|
|
3852
|
+
preferredCompositions: [
|
|
3853
|
+
"grid-subdivision",
|
|
3854
|
+
"radial"
|
|
3855
|
+
],
|
|
3549
3856
|
flowLineMultiplier: 0,
|
|
3550
3857
|
heroShape: false,
|
|
3551
3858
|
glowMultiplier: 0,
|
|
@@ -3567,6 +3874,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3567
3874
|
"fill-only",
|
|
3568
3875
|
"double-stroke"
|
|
3569
3876
|
],
|
|
3877
|
+
preferredCompositions: [
|
|
3878
|
+
"grid-subdivision",
|
|
3879
|
+
"clustered"
|
|
3880
|
+
],
|
|
3570
3881
|
flowLineMultiplier: 0,
|
|
3571
3882
|
heroShape: true,
|
|
3572
3883
|
glowMultiplier: 0,
|
|
@@ -3588,6 +3899,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3588
3899
|
"watercolor",
|
|
3589
3900
|
"fill-only"
|
|
3590
3901
|
],
|
|
3902
|
+
preferredCompositions: [
|
|
3903
|
+
"radial",
|
|
3904
|
+
"golden-spiral",
|
|
3905
|
+
"flow-field"
|
|
3906
|
+
],
|
|
3591
3907
|
flowLineMultiplier: 1,
|
|
3592
3908
|
heroShape: true,
|
|
3593
3909
|
glowMultiplier: 1,
|
|
@@ -3609,6 +3925,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3609
3925
|
"stroke-only",
|
|
3610
3926
|
"fill-only"
|
|
3611
3927
|
],
|
|
3928
|
+
preferredCompositions: [
|
|
3929
|
+
"clustered",
|
|
3930
|
+
"grid-subdivision",
|
|
3931
|
+
"radial"
|
|
3932
|
+
],
|
|
3612
3933
|
flowLineMultiplier: 0,
|
|
3613
3934
|
heroShape: false,
|
|
3614
3935
|
glowMultiplier: 0.3,
|
|
@@ -3630,6 +3951,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3630
3951
|
"fill-only",
|
|
3631
3952
|
"incomplete"
|
|
3632
3953
|
],
|
|
3954
|
+
preferredCompositions: [
|
|
3955
|
+
"flow-field",
|
|
3956
|
+
"golden-spiral",
|
|
3957
|
+
"spiral"
|
|
3958
|
+
],
|
|
3633
3959
|
flowLineMultiplier: 3,
|
|
3634
3960
|
heroShape: true,
|
|
3635
3961
|
glowMultiplier: 0.2,
|
|
@@ -3651,6 +3977,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3651
3977
|
"fill-only",
|
|
3652
3978
|
"hatched"
|
|
3653
3979
|
],
|
|
3980
|
+
preferredCompositions: [
|
|
3981
|
+
"radial",
|
|
3982
|
+
"clustered",
|
|
3983
|
+
"flow-field"
|
|
3984
|
+
],
|
|
3654
3985
|
flowLineMultiplier: 0,
|
|
3655
3986
|
heroShape: false,
|
|
3656
3987
|
glowMultiplier: 0,
|
|
@@ -3673,6 +4004,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3673
4004
|
"stroke-only",
|
|
3674
4005
|
"incomplete"
|
|
3675
4006
|
],
|
|
4007
|
+
preferredCompositions: [
|
|
4008
|
+
"spiral",
|
|
4009
|
+
"radial",
|
|
4010
|
+
"golden-spiral"
|
|
4011
|
+
],
|
|
3676
4012
|
flowLineMultiplier: 2,
|
|
3677
4013
|
heroShape: true,
|
|
3678
4014
|
glowMultiplier: 2.5,
|
|
@@ -3696,6 +4032,12 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3696
4032
|
...b.preferredStyles
|
|
3697
4033
|
])
|
|
3698
4034
|
];
|
|
4035
|
+
const mergedCompositions = [
|
|
4036
|
+
...new Set([
|
|
4037
|
+
...a.preferredCompositions,
|
|
4038
|
+
...b.preferredCompositions
|
|
4039
|
+
])
|
|
4040
|
+
];
|
|
3699
4041
|
return {
|
|
3700
4042
|
name: `${a.name}+${b.name}`,
|
|
3701
4043
|
gridSize: Math.round($f89bc858f7202849$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3707,6 +4049,7 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3707
4049
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3708
4050
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3709
4051
|
preferredStyles: mergedStyles,
|
|
4052
|
+
preferredCompositions: mergedCompositions,
|
|
3710
4053
|
flowLineMultiplier: $f89bc858f7202849$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3711
4054
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3712
4055
|
glowMultiplier: $f89bc858f7202849$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3740,12 +4083,14 @@ const $4f72c5a314eddf25$var$SACRED_SHAPES = [
|
|
|
3740
4083
|
"torus",
|
|
3741
4084
|
"eggOfLife"
|
|
3742
4085
|
];
|
|
3743
|
-
|
|
4086
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4087
|
+
const $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES = [
|
|
3744
4088
|
"radial",
|
|
3745
4089
|
"flow-field",
|
|
3746
4090
|
"spiral",
|
|
3747
4091
|
"grid-subdivision",
|
|
3748
|
-
"clustered"
|
|
4092
|
+
"clustered",
|
|
4093
|
+
"golden-spiral"
|
|
3749
4094
|
];
|
|
3750
4095
|
// ── Helper: get position based on composition mode ──────────────────
|
|
3751
4096
|
function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
|
|
@@ -3803,6 +4148,21 @@ function $4f72c5a314eddf25$var$getCompositionPosition(mode, rng, width, height,
|
|
|
3803
4148
|
x: rng() * width,
|
|
3804
4149
|
y: rng() * height
|
|
3805
4150
|
};
|
|
4151
|
+
case "golden-spiral":
|
|
4152
|
+
{
|
|
4153
|
+
// Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
|
|
4154
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4155
|
+
const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
|
|
4156
|
+
const t = shapeIndex / totalShapes;
|
|
4157
|
+
const angle = shapeIndex * goldenAngle + rng() * 0.3;
|
|
4158
|
+
const maxR = Math.min(width, height) * 0.44;
|
|
4159
|
+
// Shapes spiral outward with sqrt distribution for even area coverage
|
|
4160
|
+
const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
|
|
4161
|
+
return {
|
|
4162
|
+
x: cx + Math.cos(angle) * r,
|
|
4163
|
+
y: cy + Math.sin(angle) * r
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
3806
4166
|
}
|
|
3807
4167
|
}
|
|
3808
4168
|
// ── Helper: positional color from hierarchy ─────────────────────────
|
|
@@ -3826,7 +4186,69 @@ function $4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones) {
|
|
|
3826
4186
|
}
|
|
3827
4187
|
return false;
|
|
3828
4188
|
}
|
|
3829
|
-
// ──
|
|
4189
|
+
// ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
|
|
4190
|
+
class $4f72c5a314eddf25$var$SpatialGrid {
|
|
4191
|
+
cells;
|
|
4192
|
+
cellSize;
|
|
4193
|
+
constructor(cellSize){
|
|
4194
|
+
this.cells = new Map();
|
|
4195
|
+
this.cellSize = cellSize;
|
|
4196
|
+
}
|
|
4197
|
+
key(cx, cy) {
|
|
4198
|
+
return `${cx},${cy}`;
|
|
4199
|
+
}
|
|
4200
|
+
insert(item) {
|
|
4201
|
+
const cx = Math.floor(item.x / this.cellSize);
|
|
4202
|
+
const cy = Math.floor(item.y / this.cellSize);
|
|
4203
|
+
const k = this.key(cx, cy);
|
|
4204
|
+
const cell = this.cells.get(k);
|
|
4205
|
+
if (cell) cell.push(item);
|
|
4206
|
+
else this.cells.set(k, [
|
|
4207
|
+
item
|
|
4208
|
+
]);
|
|
4209
|
+
}
|
|
4210
|
+
/** Count items within radius of (x, y) */ countNear(x, y, radius) {
|
|
4211
|
+
const r2 = radius * radius;
|
|
4212
|
+
const minCx = Math.floor((x - radius) / this.cellSize);
|
|
4213
|
+
const maxCx = Math.floor((x + radius) / this.cellSize);
|
|
4214
|
+
const minCy = Math.floor((y - radius) / this.cellSize);
|
|
4215
|
+
const maxCy = Math.floor((y + radius) / this.cellSize);
|
|
4216
|
+
let count = 0;
|
|
4217
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4218
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4219
|
+
if (!cell) continue;
|
|
4220
|
+
for (const p of cell){
|
|
4221
|
+
const dx = x - p.x;
|
|
4222
|
+
const dy = y - p.y;
|
|
4223
|
+
if (dx * dx + dy * dy < r2) count++;
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
return count;
|
|
4227
|
+
}
|
|
4228
|
+
/** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
|
|
4229
|
+
const minCx = Math.floor((x - searchRadius) / this.cellSize);
|
|
4230
|
+
const maxCx = Math.floor((x + searchRadius) / this.cellSize);
|
|
4231
|
+
const minCy = Math.floor((y - searchRadius) / this.cellSize);
|
|
4232
|
+
const maxCy = Math.floor((y + searchRadius) / this.cellSize);
|
|
4233
|
+
let nearest = null;
|
|
4234
|
+
let bestDist2 = Infinity;
|
|
4235
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4236
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4237
|
+
if (!cell) continue;
|
|
4238
|
+
for (const p of cell){
|
|
4239
|
+
const dx = x - p.x;
|
|
4240
|
+
const dy = y - p.y;
|
|
4241
|
+
const d2 = dx * dx + dy * dy;
|
|
4242
|
+
if (d2 > 0 && d2 < bestDist2) {
|
|
4243
|
+
bestDist2 = d2;
|
|
4244
|
+
nearest = p;
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
return nearest;
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
// ── Helper: density check (legacy wrapper) ──────────────────────────
|
|
3830
4252
|
function $4f72c5a314eddf25$var$localDensity(x, y, positions, radius) {
|
|
3831
4253
|
let count = 0;
|
|
3832
4254
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4125,42 +4547,43 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4125
4547
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4126
4548
|
const patternColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4127
4549
|
if (bgPatternRoll < 0.2) {
|
|
4128
|
-
// Dot grid
|
|
4550
|
+
// Dot grid — batched into a single path
|
|
4129
4551
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4130
4552
|
const dotR = dotSpacing * 0.08;
|
|
4131
4553
|
ctx.globalAlpha = patternOpacity;
|
|
4132
4554
|
ctx.fillStyle = patternColor;
|
|
4555
|
+
ctx.beginPath();
|
|
4133
4556
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4134
|
-
ctx.
|
|
4557
|
+
ctx.moveTo(px + dotR, py);
|
|
4135
4558
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4136
|
-
ctx.fill();
|
|
4137
4559
|
}
|
|
4560
|
+
ctx.fill();
|
|
4138
4561
|
} else if (bgPatternRoll < 0.4) {
|
|
4139
|
-
// Diagonal lines
|
|
4562
|
+
// Diagonal lines — batched into a single path
|
|
4140
4563
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4141
4564
|
ctx.globalAlpha = patternOpacity;
|
|
4142
4565
|
ctx.strokeStyle = patternColor;
|
|
4143
4566
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4144
4567
|
const diag = Math.hypot(width, height);
|
|
4568
|
+
ctx.beginPath();
|
|
4145
4569
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4146
|
-
ctx.beginPath();
|
|
4147
4570
|
ctx.moveTo(d, 0);
|
|
4148
4571
|
ctx.lineTo(d + height, height);
|
|
4149
|
-
ctx.stroke();
|
|
4150
4572
|
}
|
|
4573
|
+
ctx.stroke();
|
|
4151
4574
|
} else {
|
|
4152
|
-
// Tessellation — hexagonal grid
|
|
4575
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4153
4576
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4154
4577
|
const tessH = tessSize * Math.sqrt(3);
|
|
4155
4578
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4156
4579
|
ctx.strokeStyle = patternColor;
|
|
4157
4580
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4581
|
+
ctx.beginPath();
|
|
4158
4582
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4159
4583
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4160
4584
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4161
4585
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4162
4586
|
const hy = row * tessH;
|
|
4163
|
-
ctx.beginPath();
|
|
4164
4587
|
for(let s = 0; s < 6; s++){
|
|
4165
4588
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4166
4589
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4169,18 +4592,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4169
4592
|
else ctx.lineTo(vx, vy);
|
|
4170
4593
|
}
|
|
4171
4594
|
ctx.closePath();
|
|
4172
|
-
ctx.stroke();
|
|
4173
4595
|
}
|
|
4174
4596
|
}
|
|
4597
|
+
ctx.stroke();
|
|
4175
4598
|
}
|
|
4176
4599
|
ctx.restore();
|
|
4177
4600
|
}
|
|
4178
4601
|
ctx.globalCompositeOperation = "source-over";
|
|
4179
|
-
// ── 2. Composition mode
|
|
4180
|
-
const compositionMode = $4f72c5a314eddf25$var$
|
|
4602
|
+
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4603
|
+
const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES.length)];
|
|
4181
4604
|
const symRoll = rng();
|
|
4182
4605
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4183
|
-
// ── 3. Focal points + void zones
|
|
4606
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4184
4607
|
const THIRDS_POINTS = [
|
|
4185
4608
|
{
|
|
4186
4609
|
x: 1 / 3,
|
|
@@ -4213,9 +4636,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4213
4636
|
y: height * (0.2 + rng() * 0.6),
|
|
4214
4637
|
strength: 0.3 + rng() * 0.4
|
|
4215
4638
|
});
|
|
4216
|
-
|
|
4639
|
+
// Archetype-aware void zones: dense archetypes get fewer/no voids,
|
|
4640
|
+
// minimal archetypes get golden-ratio positioned voids
|
|
4641
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4642
|
+
const isMinimalArchetype = archetype.gridSize <= 3;
|
|
4643
|
+
const isDenseArchetype = archetype.gridSize >= 8;
|
|
4644
|
+
const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
|
|
4217
4645
|
const voidZones = [];
|
|
4218
|
-
for(let v = 0; v < numVoids; v++)
|
|
4646
|
+
for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
|
|
4647
|
+
// Place voids at golden-ratio positions for intentional negative space
|
|
4648
|
+
const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
|
|
4649
|
+
const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
|
|
4650
|
+
voidZones.push({
|
|
4651
|
+
x: width * (gx + (rng() - 0.5) * 0.05),
|
|
4652
|
+
y: height * (gy + (rng() - 0.5) * 0.05),
|
|
4653
|
+
radius: Math.min(width, height) * (0.08 + rng() * 0.08)
|
|
4654
|
+
});
|
|
4655
|
+
} else voidZones.push({
|
|
4219
4656
|
x: width * (0.15 + rng() * 0.7),
|
|
4220
4657
|
y: height * (0.15 + rng() * 0.7),
|
|
4221
4658
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4271,14 +4708,30 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4271
4708
|
}
|
|
4272
4709
|
}
|
|
4273
4710
|
ctx.globalAlpha = 1;
|
|
4274
|
-
// ── 4. Flow field
|
|
4711
|
+
// ── 4. Flow field — simplex noise for organic variation ─────────
|
|
4712
|
+
// Create a seeded simplex noise field (unique per hash)
|
|
4713
|
+
const noiseFieldRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 333));
|
|
4714
|
+
const simplexNoise = (0, $e4b03e131ed2a289$export$bbde7fbaaf9a8d66)(noiseFieldRng);
|
|
4715
|
+
const fbmNoise = (0, $e4b03e131ed2a289$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
|
|
4275
4716
|
const fieldAngleBase = rng() * Math.PI * 2;
|
|
4276
|
-
const fieldFreq =
|
|
4717
|
+
const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
|
|
4277
4718
|
function flowAngle(x, y) {
|
|
4278
|
-
|
|
4719
|
+
// Sample FBM noise at the position, scaled by frequency
|
|
4720
|
+
const nx = x / width * fieldFreq;
|
|
4721
|
+
const ny = y / height * fieldFreq;
|
|
4722
|
+
return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
|
|
4723
|
+
}
|
|
4724
|
+
// Noise-based size modulation — shapes in "high noise" areas get scaled
|
|
4725
|
+
function noiseSizeModulation(x, y) {
|
|
4726
|
+
const n = simplexNoise(x / width * 3, y / height * 3);
|
|
4727
|
+
// Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
|
|
4728
|
+
return 0.7 + (n + 1) * 0.3;
|
|
4279
4729
|
}
|
|
4280
4730
|
// Track all placed shapes for density checks and connecting curves
|
|
4281
4731
|
const shapePositions = [];
|
|
4732
|
+
// Spatial grid for O(1) density and nearest-neighbor lookups
|
|
4733
|
+
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4734
|
+
const spatialGrid = new $4f72c5a314eddf25$var$SpatialGrid(densityCheckRadius);
|
|
4282
4735
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4283
4736
|
let heroCenter = null;
|
|
4284
4737
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4309,7 +4762,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4309
4762
|
glowColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(heroStroke, 0.4),
|
|
4310
4763
|
gradientFillEnd: (0, $d016ad53434219a1$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
|
|
4311
4764
|
renderStyle: heroStyle,
|
|
4312
|
-
rng: rng
|
|
4765
|
+
rng: rng,
|
|
4766
|
+
lightAngle: lightAngle,
|
|
4767
|
+
scaleFactor: scaleFactor
|
|
4313
4768
|
});
|
|
4314
4769
|
heroCenter = {
|
|
4315
4770
|
x: heroFocal.x,
|
|
@@ -4322,9 +4777,14 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4322
4777
|
size: heroSize,
|
|
4323
4778
|
shape: heroShape
|
|
4324
4779
|
});
|
|
4780
|
+
spatialGrid.insert({
|
|
4781
|
+
x: heroFocal.x,
|
|
4782
|
+
y: heroFocal.y,
|
|
4783
|
+
size: heroSize,
|
|
4784
|
+
shape: heroShape
|
|
4785
|
+
});
|
|
4325
4786
|
}
|
|
4326
4787
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4327
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4328
4788
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4329
4789
|
for(let layer = 0; layer < layers; layer++){
|
|
4330
4790
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
@@ -4363,12 +4823,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4363
4823
|
if ($4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones)) {
|
|
4364
4824
|
if (rng() < 0.85) continue;
|
|
4365
4825
|
}
|
|
4366
|
-
if (
|
|
4826
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4367
4827
|
if (rng() < 0.6) continue;
|
|
4368
4828
|
}
|
|
4369
4829
|
// Power distribution for size — archetype controls the curve
|
|
4370
4830
|
const sizeT = Math.pow(rng(), archetype.sizePower);
|
|
4371
|
-
const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
|
|
4831
|
+
const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
|
|
4372
4832
|
// Size fraction for affinity-aware shape selection
|
|
4373
4833
|
const sizeFraction = size / adjustedMaxSize;
|
|
4374
4834
|
// Palette-driven shape selection (replaces naive pickShape)
|
|
@@ -4423,17 +4883,11 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4423
4883
|
let finalX = x;
|
|
4424
4884
|
let finalY = y;
|
|
4425
4885
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4426
|
-
//
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
for (const sp of shapePositions){
|
|
4430
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4431
|
-
if (d < nearestDist && d > 0) {
|
|
4432
|
-
nearestDist = d;
|
|
4433
|
-
nearestPos = sp;
|
|
4434
|
-
}
|
|
4435
|
-
}
|
|
4886
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
4887
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
4888
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4436
4889
|
if (nearestPos) {
|
|
4890
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4437
4891
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4438
4892
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4439
4893
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4471,7 +4925,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4471
4925
|
glowColor: hasGlow ? (0, $d016ad53434219a1$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
|
|
4472
4926
|
gradientFillEnd: gradientEnd,
|
|
4473
4927
|
renderStyle: finalRenderStyle,
|
|
4474
|
-
rng: rng
|
|
4928
|
+
rng: rng,
|
|
4929
|
+
lightAngle: lightAngle,
|
|
4930
|
+
scaleFactor: scaleFactor
|
|
4475
4931
|
};
|
|
4476
4932
|
if (shouldMirror) (0, $c3de8257a8baa3b0$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
4477
4933
|
...shapeConfig,
|
|
@@ -4504,6 +4960,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4504
4960
|
size: size,
|
|
4505
4961
|
shape: shape
|
|
4506
4962
|
});
|
|
4963
|
+
spatialGrid.insert({
|
|
4964
|
+
x: finalX,
|
|
4965
|
+
y: finalY,
|
|
4966
|
+
size: size,
|
|
4967
|
+
shape: shape
|
|
4968
|
+
});
|
|
4507
4969
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4508
4970
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4509
4971
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
@@ -4532,6 +4994,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4532
4994
|
size: echoSize,
|
|
4533
4995
|
shape: shape
|
|
4534
4996
|
});
|
|
4997
|
+
spatialGrid.insert({
|
|
4998
|
+
x: echoX,
|
|
4999
|
+
y: echoY,
|
|
5000
|
+
size: echoSize,
|
|
5001
|
+
shape: shape
|
|
5002
|
+
});
|
|
4535
5003
|
}
|
|
4536
5004
|
}
|
|
4537
5005
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
@@ -4596,12 +5064,119 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4596
5064
|
size: member.size,
|
|
4597
5065
|
shape: memberShape
|
|
4598
5066
|
});
|
|
5067
|
+
spatialGrid.insert({
|
|
5068
|
+
x: mx,
|
|
5069
|
+
y: my,
|
|
5070
|
+
size: member.size,
|
|
5071
|
+
shape: memberShape
|
|
5072
|
+
});
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
5076
|
+
// ~12% of medium-large shapes spawn a rhythmic sequence
|
|
5077
|
+
if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
|
|
5078
|
+
const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
|
|
5079
|
+
const rhythmAngle = rng() * Math.PI * 2;
|
|
5080
|
+
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5081
|
+
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5082
|
+
const rhythmShape = shape; // same shape for visual rhythm
|
|
5083
|
+
let rhythmSize = size * 0.6;
|
|
5084
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5085
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5086
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5087
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5088
|
+
if ($4f72c5a314eddf25$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5089
|
+
rhythmSize *= rhythmDecay;
|
|
5090
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5091
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5092
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5093
|
+
const rhythmFill = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$18a34c25ea7e724b)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5094
|
+
(0, $c3de8257a8baa3b0$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5095
|
+
fillColor: rhythmFill,
|
|
5096
|
+
strokeColor: (0, $d016ad53434219a1$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5097
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5098
|
+
size: rhythmSize,
|
|
5099
|
+
rotation: rotation + (r + 1) * 12,
|
|
5100
|
+
proportionType: "GOLDEN_RATIO",
|
|
5101
|
+
renderStyle: finalRenderStyle,
|
|
5102
|
+
rng: rng
|
|
5103
|
+
});
|
|
5104
|
+
shapePositions.push({
|
|
5105
|
+
x: rx,
|
|
5106
|
+
y: ry,
|
|
5107
|
+
size: rhythmSize,
|
|
5108
|
+
shape: rhythmShape
|
|
5109
|
+
});
|
|
5110
|
+
spatialGrid.insert({
|
|
5111
|
+
x: rx,
|
|
5112
|
+
y: ry,
|
|
5113
|
+
size: rhythmSize,
|
|
5114
|
+
shape: rhythmShape
|
|
5115
|
+
});
|
|
4599
5116
|
}
|
|
4600
5117
|
}
|
|
4601
5118
|
}
|
|
4602
5119
|
}
|
|
4603
5120
|
// Reset blend mode for post-processing passes
|
|
4604
5121
|
ctx.globalCompositeOperation = "source-over";
|
|
5122
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
5123
|
+
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
5124
|
+
// with a tinted background wash, creating a "peek through" effect.
|
|
5125
|
+
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
5126
|
+
const portalCount = 1 + Math.floor(rng() * 2);
|
|
5127
|
+
for(let p = 0; p < portalCount; p++){
|
|
5128
|
+
// Pick a position biased toward placed shapes
|
|
5129
|
+
const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5130
|
+
const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
5131
|
+
const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
5132
|
+
const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
|
|
5133
|
+
// Pick a portal shape from the palette
|
|
5134
|
+
const portalShape = (0, $e73976f898150d4d$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
|
|
5135
|
+
const portalRotation = rng() * 360;
|
|
5136
|
+
const portalAlpha = 0.6 + rng() * 0.35;
|
|
5137
|
+
ctx.save();
|
|
5138
|
+
ctx.translate(portalX, portalY);
|
|
5139
|
+
ctx.rotate(portalRotation * Math.PI / 180);
|
|
5140
|
+
// Step 1: Clip to the portal shape and fill with background wash
|
|
5141
|
+
ctx.beginPath();
|
|
5142
|
+
(0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
|
|
5143
|
+
ctx.clip();
|
|
5144
|
+
// Fill the clipped region with a radial gradient from background colors
|
|
5145
|
+
const portalColor = (0, $d016ad53434219a1$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
|
|
5146
|
+
const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
|
|
5147
|
+
portalGrad.addColorStop(0, portalColor);
|
|
5148
|
+
portalGrad.addColorStop(1, bgEnd);
|
|
5149
|
+
ctx.globalAlpha = portalAlpha;
|
|
5150
|
+
ctx.fillStyle = portalGrad;
|
|
5151
|
+
ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
|
|
5152
|
+
// Optional: subtle inner texture — a few tiny dots inside the portal
|
|
5153
|
+
if (rng() < 0.5) {
|
|
5154
|
+
const dotCount = 3 + Math.floor(rng() * 5);
|
|
5155
|
+
ctx.globalAlpha = portalAlpha * 0.3;
|
|
5156
|
+
ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
|
|
5157
|
+
for(let d = 0; d < dotCount; d++){
|
|
5158
|
+
const dx = (rng() - 0.5) * portalSize * 1.4;
|
|
5159
|
+
const dy = (rng() - 0.5) * portalSize * 1.4;
|
|
5160
|
+
const dr = (1 + rng() * 3) * scaleFactor;
|
|
5161
|
+
ctx.beginPath();
|
|
5162
|
+
ctx.arc(dx, dy, dr, 0, Math.PI * 2);
|
|
5163
|
+
ctx.fill();
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
ctx.restore();
|
|
5167
|
+
// Step 2: Draw a border ring around the portal (outside the clip)
|
|
5168
|
+
ctx.save();
|
|
5169
|
+
ctx.translate(portalX, portalY);
|
|
5170
|
+
ctx.rotate(portalRotation * Math.PI / 180);
|
|
5171
|
+
ctx.globalAlpha = 0.15 + rng() * 0.2;
|
|
5172
|
+
ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)((0, $d016ad53434219a1$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
|
|
5173
|
+
ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
|
|
5174
|
+
ctx.beginPath();
|
|
5175
|
+
(0, $9c828bde2acaae64$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
|
|
5176
|
+
ctx.stroke();
|
|
5177
|
+
ctx.restore();
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
4605
5180
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
4606
5181
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
4607
5182
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
@@ -4625,6 +5200,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4625
5200
|
fx += Math.cos(angle) * stepLen;
|
|
4626
5201
|
fy += Math.sin(angle) * stepLen;
|
|
4627
5202
|
if (fx < 0 || fx > width || fy < 0 || fy > height) break;
|
|
5203
|
+
// Skip segments that pass through void zones
|
|
5204
|
+
if ($4f72c5a314eddf25$var$isInVoidZone(fx, fy, voidZones)) {
|
|
5205
|
+
prevX = fx;
|
|
5206
|
+
prevY = fy;
|
|
5207
|
+
continue;
|
|
5208
|
+
}
|
|
4628
5209
|
const t = s / steps;
|
|
4629
5210
|
// Taper + pressure
|
|
4630
5211
|
const taper = 1 - t * 0.8;
|
|
@@ -4722,30 +5303,60 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4722
5303
|
}
|
|
4723
5304
|
ctx.restore();
|
|
4724
5305
|
}
|
|
4725
|
-
// ── 7. Noise texture overlay
|
|
5306
|
+
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
4726
5307
|
const noiseRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 777));
|
|
4727
5308
|
const noiseDensity = Math.floor(width * height / 800);
|
|
4728
|
-
|
|
4729
|
-
const
|
|
4730
|
-
const
|
|
4731
|
-
const
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
5309
|
+
try {
|
|
5310
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5311
|
+
const data = imageData.data;
|
|
5312
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5313
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5314
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5315
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5316
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5317
|
+
const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
|
|
5318
|
+
// Write a small block of pixels for scale
|
|
5319
|
+
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5320
|
+
const idx = ((ny + dy) * width + (nx + dx)) * 4;
|
|
5321
|
+
// Alpha-blend the noise dot onto existing pixel data
|
|
5322
|
+
const srcA = alpha / 255;
|
|
5323
|
+
const invA = 1 - srcA;
|
|
5324
|
+
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5325
|
+
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5326
|
+
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5327
|
+
// Keep existing alpha
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
ctx.putImageData(imageData, 0, 0);
|
|
5331
|
+
} catch {
|
|
5332
|
+
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5333
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5334
|
+
const nx = noiseRng() * width;
|
|
5335
|
+
const ny = noiseRng() * height;
|
|
5336
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5337
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5338
|
+
ctx.globalAlpha = alpha;
|
|
5339
|
+
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5340
|
+
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5341
|
+
}
|
|
4736
5342
|
}
|
|
4737
5343
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
4738
5344
|
ctx.globalAlpha = 1;
|
|
4739
5345
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
4740
5346
|
const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
|
|
5347
|
+
// Tint vignette based on background: warm sepia for light, cool blue for dark
|
|
5348
|
+
const isLightBg = bgLum > 0.5;
|
|
5349
|
+
const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
|
|
5350
|
+
: `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
|
|
4741
5351
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
4742
5352
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
4743
|
-
vigGrad.addColorStop(1,
|
|
5353
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
4744
5354
|
ctx.fillStyle = vigGrad;
|
|
4745
5355
|
ctx.fillRect(0, 0, width, height);
|
|
4746
|
-
// ── 9. Organic connecting curves
|
|
5356
|
+
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
4747
5357
|
if (shapePositions.length > 1) {
|
|
4748
5358
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5359
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
4749
5360
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
4750
5361
|
for(let i = 0; i < numCurves; i++){
|
|
4751
5362
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
@@ -4753,11 +5364,13 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4753
5364
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
4754
5365
|
const a = shapePositions[idxA];
|
|
4755
5366
|
const b = shapePositions[idxB];
|
|
4756
|
-
const mx = (a.x + b.x) / 2;
|
|
4757
|
-
const my = (a.y + b.y) / 2;
|
|
4758
5367
|
const dx = b.x - a.x;
|
|
4759
5368
|
const dy = b.y - a.y;
|
|
4760
5369
|
const dist = Math.hypot(dx, dy);
|
|
5370
|
+
// Skip connections between distant shapes
|
|
5371
|
+
if (dist > maxCurveDist) continue;
|
|
5372
|
+
const mx = (a.x + b.x) / 2;
|
|
5373
|
+
const my = (a.y + b.y) / 2;
|
|
4761
5374
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
4762
5375
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
4763
5376
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
@@ -4813,13 +5426,211 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4813
5426
|
ctx.restore();
|
|
4814
5427
|
ctx.globalCompositeOperation = "source-over";
|
|
4815
5428
|
}
|
|
4816
|
-
//
|
|
5429
|
+
// 10d. Gradient map — map luminance through a two-color gradient
|
|
5430
|
+
// Uses dominant→accent as the dark→light ramp for a cohesive tonal look
|
|
5431
|
+
if (rng() < 0.35) {
|
|
5432
|
+
const gmDark = colorHierarchy.dominant;
|
|
5433
|
+
const gmLight = colorHierarchy.accent;
|
|
5434
|
+
ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
|
|
5435
|
+
ctx.globalCompositeOperation = "color";
|
|
5436
|
+
// Paint a linear gradient from dark color (top) to light color (bottom)
|
|
5437
|
+
const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
|
|
5438
|
+
gmGrad.addColorStop(0, gmDark);
|
|
5439
|
+
gmGrad.addColorStop(1, gmLight);
|
|
5440
|
+
ctx.fillStyle = gmGrad;
|
|
5441
|
+
ctx.fillRect(0, 0, width, height);
|
|
5442
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5443
|
+
}
|
|
5444
|
+
// ── 10e. Generative borders — archetype-driven decorative frames ──
|
|
5445
|
+
{
|
|
5446
|
+
ctx.save();
|
|
5447
|
+
ctx.globalAlpha = 1;
|
|
5448
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5449
|
+
const borderRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 314));
|
|
5450
|
+
const borderPad = Math.min(width, height) * 0.025;
|
|
5451
|
+
const borderColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
|
|
5452
|
+
const borderColorSolid = colorHierarchy.accent;
|
|
5453
|
+
const archName = archetype.name;
|
|
5454
|
+
if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
|
|
5455
|
+
// Clean ruled lines with corner ornaments
|
|
5456
|
+
ctx.strokeStyle = borderColor;
|
|
5457
|
+
ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
|
|
5458
|
+
ctx.globalAlpha = 0.18 + borderRng() * 0.1;
|
|
5459
|
+
// Outer rule
|
|
5460
|
+
ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
|
|
5461
|
+
// Inner rule (thinner, offset)
|
|
5462
|
+
const innerPad = borderPad * 1.8;
|
|
5463
|
+
ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
|
|
5464
|
+
ctx.globalAlpha *= 0.7;
|
|
5465
|
+
ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
|
|
5466
|
+
// Corner ornaments — small squares at each corner
|
|
5467
|
+
const ornSize = borderPad * 0.6;
|
|
5468
|
+
ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(borderColorSolid, 0.12);
|
|
5469
|
+
const corners = [
|
|
5470
|
+
[
|
|
5471
|
+
borderPad,
|
|
5472
|
+
borderPad
|
|
5473
|
+
],
|
|
5474
|
+
[
|
|
5475
|
+
width - borderPad - ornSize,
|
|
5476
|
+
borderPad
|
|
5477
|
+
],
|
|
5478
|
+
[
|
|
5479
|
+
borderPad,
|
|
5480
|
+
height - borderPad - ornSize
|
|
5481
|
+
],
|
|
5482
|
+
[
|
|
5483
|
+
width - borderPad - ornSize,
|
|
5484
|
+
height - borderPad - ornSize
|
|
5485
|
+
]
|
|
5486
|
+
];
|
|
5487
|
+
for (const [cx2, cy2] of corners){
|
|
5488
|
+
ctx.fillRect(cx2, cy2, ornSize, ornSize);
|
|
5489
|
+
// Diagonal cross inside ornament
|
|
5490
|
+
ctx.beginPath();
|
|
5491
|
+
ctx.moveTo(cx2, cy2);
|
|
5492
|
+
ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
|
|
5493
|
+
ctx.moveTo(cx2 + ornSize, cy2);
|
|
5494
|
+
ctx.lineTo(cx2, cy2 + ornSize);
|
|
5495
|
+
ctx.stroke();
|
|
5496
|
+
}
|
|
5497
|
+
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5498
|
+
// Vine tendrils — organic curving lines along edges
|
|
5499
|
+
ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5500
|
+
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5501
|
+
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5502
|
+
ctx.lineCap = "round";
|
|
5503
|
+
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5504
|
+
for(let t = 0; t < tendrilCount; t++){
|
|
5505
|
+
// Start from a random edge point
|
|
5506
|
+
const edge = Math.floor(borderRng() * 4);
|
|
5507
|
+
let tx, ty;
|
|
5508
|
+
if (edge === 0) {
|
|
5509
|
+
tx = borderRng() * width;
|
|
5510
|
+
ty = borderPad;
|
|
5511
|
+
} else if (edge === 1) {
|
|
5512
|
+
tx = borderRng() * width;
|
|
5513
|
+
ty = height - borderPad;
|
|
5514
|
+
} else if (edge === 2) {
|
|
5515
|
+
tx = borderPad;
|
|
5516
|
+
ty = borderRng() * height;
|
|
5517
|
+
} else {
|
|
5518
|
+
tx = width - borderPad;
|
|
5519
|
+
ty = borderRng() * height;
|
|
5520
|
+
}
|
|
5521
|
+
ctx.beginPath();
|
|
5522
|
+
ctx.moveTo(tx, ty);
|
|
5523
|
+
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5524
|
+
for(let s = 0; s < segs; s++){
|
|
5525
|
+
const inward = borderPad * (1 + borderRng() * 2);
|
|
5526
|
+
// Curl inward from edge
|
|
5527
|
+
const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
|
|
5528
|
+
const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
|
|
5529
|
+
const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
|
|
5530
|
+
const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
|
|
5531
|
+
tx = cpx3;
|
|
5532
|
+
ty = cpy3;
|
|
5533
|
+
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5534
|
+
}
|
|
5535
|
+
ctx.stroke();
|
|
5536
|
+
// Small leaf/dot at tendril end
|
|
5537
|
+
if (borderRng() < 0.6) {
|
|
5538
|
+
ctx.beginPath();
|
|
5539
|
+
ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
|
|
5540
|
+
ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5541
|
+
ctx.fill();
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5545
|
+
// Star-studded arcs along edges
|
|
5546
|
+
ctx.globalAlpha = 0.1 + borderRng() * 0.08;
|
|
5547
|
+
ctx.fillStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
|
|
5548
|
+
ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
|
|
5549
|
+
ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
|
|
5550
|
+
// Subtle arc along top and bottom
|
|
5551
|
+
ctx.beginPath();
|
|
5552
|
+
ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
|
|
5553
|
+
ctx.stroke();
|
|
5554
|
+
ctx.beginPath();
|
|
5555
|
+
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5556
|
+
ctx.stroke();
|
|
5557
|
+
// Scatter small stars along the border region
|
|
5558
|
+
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5559
|
+
for(let s = 0; s < starCount; s++){
|
|
5560
|
+
const edge = Math.floor(borderRng() * 4);
|
|
5561
|
+
let sx, sy;
|
|
5562
|
+
if (edge === 0) {
|
|
5563
|
+
sx = borderRng() * width;
|
|
5564
|
+
sy = borderPad * (0.5 + borderRng());
|
|
5565
|
+
} else if (edge === 1) {
|
|
5566
|
+
sx = borderRng() * width;
|
|
5567
|
+
sy = height - borderPad * (0.5 + borderRng());
|
|
5568
|
+
} else if (edge === 2) {
|
|
5569
|
+
sx = borderPad * (0.5 + borderRng());
|
|
5570
|
+
sy = borderRng() * height;
|
|
5571
|
+
} else {
|
|
5572
|
+
sx = width - borderPad * (0.5 + borderRng());
|
|
5573
|
+
sy = borderRng() * height;
|
|
5574
|
+
}
|
|
5575
|
+
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5576
|
+
// 4-point star
|
|
5577
|
+
ctx.beginPath();
|
|
5578
|
+
for(let p = 0; p < 8; p++){
|
|
5579
|
+
const a = p / 8 * Math.PI * 2;
|
|
5580
|
+
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
5581
|
+
const px2 = sx + Math.cos(a) * r;
|
|
5582
|
+
const py2 = sy + Math.sin(a) * r;
|
|
5583
|
+
if (p === 0) ctx.moveTo(px2, py2);
|
|
5584
|
+
else ctx.lineTo(px2, py2);
|
|
5585
|
+
}
|
|
5586
|
+
ctx.closePath();
|
|
5587
|
+
ctx.fill();
|
|
5588
|
+
}
|
|
5589
|
+
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5590
|
+
// Thin single rule — understated elegance
|
|
5591
|
+
ctx.strokeStyle = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
5592
|
+
ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
|
|
5593
|
+
ctx.globalAlpha = 0.1 + borderRng() * 0.06;
|
|
5594
|
+
ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
|
|
5595
|
+
}
|
|
5596
|
+
// Other archetypes: no border (intentional — not every image needs one)
|
|
5597
|
+
ctx.restore();
|
|
5598
|
+
}
|
|
5599
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
4817
5600
|
{
|
|
4818
5601
|
const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));
|
|
4819
5602
|
const sigSize = Math.min(width, height) * 0.025;
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
const
|
|
5603
|
+
const sigMargin = sigSize * 2.5;
|
|
5604
|
+
// Find the corner with the lowest local density
|
|
5605
|
+
const cornerCandidates = [
|
|
5606
|
+
{
|
|
5607
|
+
x: sigMargin,
|
|
5608
|
+
y: sigMargin
|
|
5609
|
+
},
|
|
5610
|
+
{
|
|
5611
|
+
x: width - sigMargin,
|
|
5612
|
+
y: sigMargin
|
|
5613
|
+
},
|
|
5614
|
+
{
|
|
5615
|
+
x: sigMargin,
|
|
5616
|
+
y: height - sigMargin
|
|
5617
|
+
},
|
|
5618
|
+
{
|
|
5619
|
+
x: width - sigMargin,
|
|
5620
|
+
y: height - sigMargin
|
|
5621
|
+
}
|
|
5622
|
+
];
|
|
5623
|
+
let bestCorner = cornerCandidates[3]; // default: bottom-right
|
|
5624
|
+
let minDensity = Infinity;
|
|
5625
|
+
for (const corner of cornerCandidates){
|
|
5626
|
+
const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
|
|
5627
|
+
if (density < minDensity) {
|
|
5628
|
+
minDensity = density;
|
|
5629
|
+
bestCorner = corner;
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
const sigX = bestCorner.x;
|
|
5633
|
+
const sigY = bestCorner.y;
|
|
4823
5634
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
4824
5635
|
const sigColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
4825
5636
|
ctx.save();
|