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/module.js
CHANGED
|
@@ -62,6 +62,136 @@ const $461134e0b6ce0619$export$bb9e4790bc99ae59 = {
|
|
|
62
62
|
PI: Math.PI,
|
|
63
63
|
PHI: (1 + Math.sqrt(5)) / 2
|
|
64
64
|
};
|
|
65
|
+
function $461134e0b6ce0619$export$bbde7fbaaf9a8d66(rng) {
|
|
66
|
+
// Build a deterministic permutation table (256 entries, doubled)
|
|
67
|
+
const perm = new Uint8Array(512);
|
|
68
|
+
const p = new Uint8Array(256);
|
|
69
|
+
for(let i = 0; i < 256; i++)p[i] = i;
|
|
70
|
+
// Fisher-Yates shuffle with our seeded RNG
|
|
71
|
+
for(let i = 255; i > 0; i--){
|
|
72
|
+
const j = Math.floor(rng() * (i + 1));
|
|
73
|
+
const tmp = p[i];
|
|
74
|
+
p[i] = p[j];
|
|
75
|
+
p[j] = tmp;
|
|
76
|
+
}
|
|
77
|
+
for(let i = 0; i < 512; i++)perm[i] = p[i & 255];
|
|
78
|
+
// 12 gradient vectors for 2D simplex
|
|
79
|
+
const GRAD2 = [
|
|
80
|
+
[
|
|
81
|
+
1,
|
|
82
|
+
1
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
-1,
|
|
86
|
+
1
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
1,
|
|
90
|
+
-1
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
-1,
|
|
94
|
+
-1
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
1,
|
|
98
|
+
0
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
-1,
|
|
102
|
+
0
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
0,
|
|
106
|
+
1
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
0,
|
|
110
|
+
-1
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
1,
|
|
114
|
+
1
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
-1,
|
|
118
|
+
1
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
1,
|
|
122
|
+
-1
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
-1,
|
|
126
|
+
-1
|
|
127
|
+
]
|
|
128
|
+
];
|
|
129
|
+
const F2 = 0.5 * (Math.sqrt(3) - 1);
|
|
130
|
+
const G2 = (3 - Math.sqrt(3)) / 6;
|
|
131
|
+
function dot2(g, x, y) {
|
|
132
|
+
return g[0] * x + g[1] * y;
|
|
133
|
+
}
|
|
134
|
+
return function noise2D(xin, yin) {
|
|
135
|
+
const s = (xin + yin) * F2;
|
|
136
|
+
const i = Math.floor(xin + s);
|
|
137
|
+
const j = Math.floor(yin + s);
|
|
138
|
+
const t = (i + j) * G2;
|
|
139
|
+
const X0 = i - t;
|
|
140
|
+
const Y0 = j - t;
|
|
141
|
+
const x0 = xin - X0;
|
|
142
|
+
const y0 = yin - Y0;
|
|
143
|
+
let i1, j1;
|
|
144
|
+
if (x0 > y0) {
|
|
145
|
+
i1 = 1;
|
|
146
|
+
j1 = 0;
|
|
147
|
+
} else {
|
|
148
|
+
i1 = 0;
|
|
149
|
+
j1 = 1;
|
|
150
|
+
}
|
|
151
|
+
const x1 = x0 - i1 + G2;
|
|
152
|
+
const y1 = y0 - j1 + G2;
|
|
153
|
+
const x2 = x0 - 1 + 2 * G2;
|
|
154
|
+
const y2 = y0 - 1 + 2 * G2;
|
|
155
|
+
const ii = i & 255;
|
|
156
|
+
const jj = j & 255;
|
|
157
|
+
let n0 = 0, n1 = 0, n2 = 0;
|
|
158
|
+
let t0 = 0.5 - x0 * x0 - y0 * y0;
|
|
159
|
+
if (t0 >= 0) {
|
|
160
|
+
t0 *= t0;
|
|
161
|
+
const gi0 = perm[ii + perm[jj]] % 12;
|
|
162
|
+
n0 = t0 * t0 * dot2(GRAD2[gi0], x0, y0);
|
|
163
|
+
}
|
|
164
|
+
let t1 = 0.5 - x1 * x1 - y1 * y1;
|
|
165
|
+
if (t1 >= 0) {
|
|
166
|
+
t1 *= t1;
|
|
167
|
+
const gi1 = perm[ii + i1 + perm[jj + j1]] % 12;
|
|
168
|
+
n1 = t1 * t1 * dot2(GRAD2[gi1], x1, y1);
|
|
169
|
+
}
|
|
170
|
+
let t2 = 0.5 - x2 * x2 - y2 * y2;
|
|
171
|
+
if (t2 >= 0) {
|
|
172
|
+
t2 *= t2;
|
|
173
|
+
const gi2 = perm[ii + 1 + perm[jj + 1]] % 12;
|
|
174
|
+
n2 = t2 * t2 * dot2(GRAD2[gi2], x2, y2);
|
|
175
|
+
}
|
|
176
|
+
// Scale to approximately [-1, 1]
|
|
177
|
+
return 70 * (n0 + n1 + n2);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function $461134e0b6ce0619$export$c81d639e83a19b85(noise, octaves = 4, lacunarity = 2.0, gain = 0.5) {
|
|
181
|
+
return function fbm(x, y) {
|
|
182
|
+
let value = 0;
|
|
183
|
+
let amplitude = 1;
|
|
184
|
+
let frequency = 1;
|
|
185
|
+
let maxAmp = 0;
|
|
186
|
+
for(let i = 0; i < octaves; i++){
|
|
187
|
+
value += noise(x * frequency, y * frequency) * amplitude;
|
|
188
|
+
maxAmp += amplitude;
|
|
189
|
+
amplitude *= gain;
|
|
190
|
+
frequency *= lacunarity;
|
|
191
|
+
}
|
|
192
|
+
return value / maxAmp;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
65
195
|
class $461134e0b6ce0619$export$da2372f11bc66b3f {
|
|
66
196
|
static getProportionalSize(baseSize, proportion) {
|
|
67
197
|
return baseSize * proportion;
|
|
@@ -263,6 +393,48 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
|
|
|
263
393
|
$9d614e7d77fc2947$var$hslToHex(baseHue, 0.7, 0.35)
|
|
264
394
|
];
|
|
265
395
|
}
|
|
396
|
+
case "split-complementary":
|
|
397
|
+
{
|
|
398
|
+
// Base hue + two colors flanking the complement (±30°)
|
|
399
|
+
const comp = (baseHue + 180) % 360;
|
|
400
|
+
const split1 = (comp - 30 + 360) % 360;
|
|
401
|
+
const split2 = (comp + 30) % 360;
|
|
402
|
+
const sat = 0.55 + this.rng() * 0.25;
|
|
403
|
+
return [
|
|
404
|
+
$9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
|
|
405
|
+
$9d614e7d77fc2947$var$hslToHex(baseHue, sat * 0.8, 0.65),
|
|
406
|
+
$9d614e7d77fc2947$var$hslToHex(split1, sat, 0.5),
|
|
407
|
+
$9d614e7d77fc2947$var$hslToHex(split2, sat, 0.5),
|
|
408
|
+
$9d614e7d77fc2947$var$hslToHex(split1, sat * 0.7, 0.7)
|
|
409
|
+
];
|
|
410
|
+
}
|
|
411
|
+
case "analogous-accent":
|
|
412
|
+
{
|
|
413
|
+
// Tight cluster of 3 analogous hues + 1 distant accent
|
|
414
|
+
const step = 15 + this.rng() * 20; // 15-35° apart
|
|
415
|
+
const h1 = (baseHue - step + 360) % 360;
|
|
416
|
+
const h2 = (baseHue + step) % 360;
|
|
417
|
+
const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
|
|
418
|
+
const sat = 0.5 + this.rng() * 0.3;
|
|
419
|
+
return [
|
|
420
|
+
$9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
|
|
421
|
+
$9d614e7d77fc2947$var$hslToHex(h1, sat, 0.55),
|
|
422
|
+
$9d614e7d77fc2947$var$hslToHex(h2, sat, 0.45),
|
|
423
|
+
$9d614e7d77fc2947$var$hslToHex(accentHue, sat + 0.15, 0.5)
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
case "limited-palette":
|
|
427
|
+
{
|
|
428
|
+
// Only 3 colors — like a risograph print
|
|
429
|
+
const h2 = (baseHue + 120 + this.rng() * 40) % 360;
|
|
430
|
+
const h3 = (baseHue + 220 + this.rng() * 40) % 360;
|
|
431
|
+
const sat = 0.6 + this.rng() * 0.2;
|
|
432
|
+
return [
|
|
433
|
+
$9d614e7d77fc2947$var$hslToHex(baseHue, sat, 0.5),
|
|
434
|
+
$9d614e7d77fc2947$var$hslToHex(h2, sat, 0.5),
|
|
435
|
+
$9d614e7d77fc2947$var$hslToHex(h3, sat * 0.9, 0.55)
|
|
436
|
+
];
|
|
437
|
+
}
|
|
266
438
|
case "harmonious":
|
|
267
439
|
default:
|
|
268
440
|
return this.getColors();
|
|
@@ -283,6 +455,14 @@ class $9d614e7d77fc2947$export$ab958c550f521376 {
|
|
|
283
455
|
"#f5f5f0",
|
|
284
456
|
"#e8e8e0"
|
|
285
457
|
];
|
|
458
|
+
case "split-complementary":
|
|
459
|
+
case "analogous-accent":
|
|
460
|
+
return this.getBackgroundColors();
|
|
461
|
+
case "limited-palette":
|
|
462
|
+
return [
|
|
463
|
+
$9d614e7d77fc2947$var$hslToHex(this.seed % 360, 0.08, 0.94),
|
|
464
|
+
$9d614e7d77fc2947$var$hslToHex((this.seed + 20) % 360, 0.06, 0.90)
|
|
465
|
+
];
|
|
286
466
|
case "neon":
|
|
287
467
|
return [
|
|
288
468
|
"#0a0a12",
|
|
@@ -409,15 +589,17 @@ function $9d614e7d77fc2947$export$fabac4600b87056(colors, rng) {
|
|
|
409
589
|
accent: colors[colors.length - 1] || "#888888",
|
|
410
590
|
all: colors
|
|
411
591
|
};
|
|
412
|
-
// Pick dominant as the color
|
|
592
|
+
// Pick dominant as the color with the highest chroma (saturation × distance from gray)
|
|
593
|
+
// This selects the most visually prominent color rather than the average
|
|
413
594
|
const hsls = colors.map((c)=>$9d614e7d77fc2947$var$hexToHsl(c));
|
|
414
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
415
595
|
let dominantIdx = 0;
|
|
416
|
-
let
|
|
596
|
+
let maxChroma = -1;
|
|
417
597
|
for(let i = 0; i < hsls.length; i++){
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
598
|
+
// Chroma approximation: saturation × how far lightness is from 50% (gray)
|
|
599
|
+
const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
|
|
600
|
+
const chroma = hsls[i][1] * lightnessVibrancy;
|
|
601
|
+
if (chroma > maxChroma) {
|
|
602
|
+
maxChroma = chroma;
|
|
421
603
|
dominantIdx = i;
|
|
422
604
|
}
|
|
423
605
|
}
|
|
@@ -536,7 +718,8 @@ function $9d614e7d77fc2947$export$703ba40a4347f77a(base, layerRatio, hueShiftPer
|
|
|
536
718
|
return {
|
|
537
719
|
dominant: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.dominant, shift),
|
|
538
720
|
secondary: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.secondary, shift * 0.7),
|
|
539
|
-
accent: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5)
|
|
721
|
+
accent: $9d614e7d77fc2947$export$1793a1bfbe4f6ff5(base.accent, shift * 0.5),
|
|
722
|
+
all: base.all.map((c)=>$9d614e7d77fc2947$export$1793a1bfbe4f6ff5(c, shift * 0.6))
|
|
540
723
|
};
|
|
541
724
|
}
|
|
542
725
|
|
|
@@ -1919,6 +2102,23 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
1919
2102
|
ctx.fill();
|
|
1920
2103
|
ctx.fillStyle = origFill;
|
|
1921
2104
|
ctx.restore();
|
|
2105
|
+
// Pass 4: Organic edge erosion — irregular bites along the boundary
|
|
2106
|
+
if (rng && size > 20) {
|
|
2107
|
+
const erosionBites = 6 + Math.floor(rng() * 8);
|
|
2108
|
+
const edgeRadius = size * 0.45;
|
|
2109
|
+
ctx.save();
|
|
2110
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
2111
|
+
ctx.globalAlpha = 0.6 + rng() * 0.3;
|
|
2112
|
+
for(let eb = 0; eb < erosionBites; eb++){
|
|
2113
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
2114
|
+
const biteDist = edgeRadius * (0.85 + rng() * 0.25);
|
|
2115
|
+
const biteR = size * (0.02 + rng() * 0.04);
|
|
2116
|
+
ctx.beginPath();
|
|
2117
|
+
ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
|
|
2118
|
+
ctx.fill();
|
|
2119
|
+
}
|
|
2120
|
+
ctx.restore();
|
|
2121
|
+
}
|
|
1922
2122
|
ctx.globalAlpha = savedAlpha;
|
|
1923
2123
|
// Soft stroke on top — thinner than normal for delicacy
|
|
1924
2124
|
ctx.globalAlpha *= 0.25;
|
|
@@ -2228,6 +2428,23 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2228
2428
|
ctx.stroke();
|
|
2229
2429
|
ctx.restore();
|
|
2230
2430
|
}
|
|
2431
|
+
// Organic edge erosion — small irregular bites for rough paper feel
|
|
2432
|
+
if (rng && size > 20) {
|
|
2433
|
+
const erosionBites = 4 + Math.floor(rng() * 6);
|
|
2434
|
+
const edgeRadius = size * 0.42;
|
|
2435
|
+
ctx.save();
|
|
2436
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
2437
|
+
ctx.globalAlpha = 0.5 + rng() * 0.3;
|
|
2438
|
+
for(let eb = 0; eb < erosionBites; eb++){
|
|
2439
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
2440
|
+
const biteDist = edgeRadius * (0.9 + rng() * 0.2);
|
|
2441
|
+
const biteR = size * (0.015 + rng() * 0.03);
|
|
2442
|
+
ctx.beginPath();
|
|
2443
|
+
ctx.arc(Math.cos(biteAngle) * biteDist, Math.sin(biteAngle) * biteDist, biteR, 0, Math.PI * 2);
|
|
2444
|
+
ctx.fill();
|
|
2445
|
+
}
|
|
2446
|
+
ctx.restore();
|
|
2447
|
+
}
|
|
2231
2448
|
ctx.globalAlpha = savedAlphaHD;
|
|
2232
2449
|
break;
|
|
2233
2450
|
}
|
|
@@ -2239,12 +2456,20 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
|
|
|
2239
2456
|
}
|
|
2240
2457
|
}
|
|
2241
2458
|
function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
2242
|
-
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;
|
|
2459
|
+
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;
|
|
2243
2460
|
ctx.save();
|
|
2244
2461
|
ctx.translate(x, y);
|
|
2245
2462
|
ctx.rotate(rotation * Math.PI / 180);
|
|
2246
|
-
//
|
|
2247
|
-
if (
|
|
2463
|
+
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
2464
|
+
if (lightAngle !== undefined && size > 10) {
|
|
2465
|
+
const shadowDist = size * 0.035;
|
|
2466
|
+
const shadowBlurR = size * 0.06;
|
|
2467
|
+
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
2468
|
+
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
2469
|
+
ctx.shadowBlur = shadowBlurR;
|
|
2470
|
+
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
2471
|
+
} else if (glowRadius > 0) {
|
|
2472
|
+
// Glow / shadow effect (legacy path)
|
|
2248
2473
|
ctx.shadowBlur = glowRadius;
|
|
2249
2474
|
ctx.shadowColor = glowColor || fillColor;
|
|
2250
2475
|
ctx.shadowOffsetX = 0;
|
|
@@ -2266,8 +2491,39 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2266
2491
|
});
|
|
2267
2492
|
$9beb8f41637c29fd$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
2268
2493
|
}
|
|
2269
|
-
// Reset shadow so patterns aren't double-
|
|
2270
|
-
|
|
2494
|
+
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
2495
|
+
ctx.shadowBlur = 0;
|
|
2496
|
+
ctx.shadowOffsetX = 0;
|
|
2497
|
+
ctx.shadowOffsetY = 0;
|
|
2498
|
+
ctx.shadowColor = "transparent";
|
|
2499
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2500
|
+
if (lightAngle !== undefined && size > 15 && rng) {
|
|
2501
|
+
const hlRadius = size * 0.35;
|
|
2502
|
+
const hlDist = size * 0.15;
|
|
2503
|
+
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2504
|
+
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2505
|
+
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2506
|
+
// Tint highlight warm/cool based on fill color for cohesion
|
|
2507
|
+
// Parse fill to detect warmth — fallback to white for non-parseable
|
|
2508
|
+
let hlBase = "255,255,255";
|
|
2509
|
+
if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
|
|
2510
|
+
const r = parseInt(fillColor.slice(1, 3), 16);
|
|
2511
|
+
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2512
|
+
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2513
|
+
// Blend toward white but keep a hint of the fill's warmth
|
|
2514
|
+
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2515
|
+
}
|
|
2516
|
+
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2517
|
+
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2518
|
+
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2519
|
+
const savedOp = ctx.globalCompositeOperation;
|
|
2520
|
+
ctx.globalCompositeOperation = "soft-light";
|
|
2521
|
+
ctx.fillStyle = hlGrad;
|
|
2522
|
+
ctx.beginPath();
|
|
2523
|
+
ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
|
|
2524
|
+
ctx.fill();
|
|
2525
|
+
ctx.globalCompositeOperation = savedOp;
|
|
2526
|
+
}
|
|
2271
2527
|
// Layer additional patterns if specified
|
|
2272
2528
|
if (patterns.length > 0) (0, $461134e0b6ce0619$export$da2372f11bc66b3f).layerPatterns(ctx, patterns, {
|
|
2273
2529
|
baseSize: size,
|
|
@@ -3321,6 +3577,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3321
3577
|
"watercolor",
|
|
3322
3578
|
"fill-only"
|
|
3323
3579
|
],
|
|
3580
|
+
preferredCompositions: [
|
|
3581
|
+
"clustered",
|
|
3582
|
+
"flow-field",
|
|
3583
|
+
"radial"
|
|
3584
|
+
],
|
|
3324
3585
|
flowLineMultiplier: 2.5,
|
|
3325
3586
|
heroShape: false,
|
|
3326
3587
|
glowMultiplier: 0.5,
|
|
@@ -3342,6 +3603,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3342
3603
|
"stroke-only",
|
|
3343
3604
|
"incomplete"
|
|
3344
3605
|
],
|
|
3606
|
+
preferredCompositions: [
|
|
3607
|
+
"golden-spiral",
|
|
3608
|
+
"grid-subdivision"
|
|
3609
|
+
],
|
|
3345
3610
|
flowLineMultiplier: 0.3,
|
|
3346
3611
|
heroShape: true,
|
|
3347
3612
|
glowMultiplier: 0,
|
|
@@ -3363,6 +3628,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3363
3628
|
"fill-only",
|
|
3364
3629
|
"incomplete"
|
|
3365
3630
|
],
|
|
3631
|
+
preferredCompositions: [
|
|
3632
|
+
"flow-field",
|
|
3633
|
+
"golden-spiral",
|
|
3634
|
+
"spiral"
|
|
3635
|
+
],
|
|
3366
3636
|
flowLineMultiplier: 4,
|
|
3367
3637
|
heroShape: false,
|
|
3368
3638
|
glowMultiplier: 0.3,
|
|
@@ -3385,6 +3655,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3385
3655
|
"double-stroke",
|
|
3386
3656
|
"hatched"
|
|
3387
3657
|
],
|
|
3658
|
+
preferredCompositions: [
|
|
3659
|
+
"grid-subdivision",
|
|
3660
|
+
"radial"
|
|
3661
|
+
],
|
|
3388
3662
|
flowLineMultiplier: 0,
|
|
3389
3663
|
heroShape: false,
|
|
3390
3664
|
glowMultiplier: 0,
|
|
@@ -3406,6 +3680,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3406
3680
|
"incomplete",
|
|
3407
3681
|
"fill-only"
|
|
3408
3682
|
],
|
|
3683
|
+
preferredCompositions: [
|
|
3684
|
+
"golden-spiral",
|
|
3685
|
+
"radial",
|
|
3686
|
+
"spiral"
|
|
3687
|
+
],
|
|
3409
3688
|
flowLineMultiplier: 1.5,
|
|
3410
3689
|
heroShape: true,
|
|
3411
3690
|
glowMultiplier: 2,
|
|
@@ -3426,6 +3705,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3426
3705
|
"fill-and-stroke",
|
|
3427
3706
|
"double-stroke"
|
|
3428
3707
|
],
|
|
3708
|
+
preferredCompositions: [
|
|
3709
|
+
"grid-subdivision",
|
|
3710
|
+
"golden-spiral"
|
|
3711
|
+
],
|
|
3429
3712
|
flowLineMultiplier: 0,
|
|
3430
3713
|
heroShape: true,
|
|
3431
3714
|
glowMultiplier: 0,
|
|
@@ -3447,6 +3730,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3447
3730
|
"double-stroke",
|
|
3448
3731
|
"dashed"
|
|
3449
3732
|
],
|
|
3733
|
+
preferredCompositions: [
|
|
3734
|
+
"radial",
|
|
3735
|
+
"spiral",
|
|
3736
|
+
"clustered"
|
|
3737
|
+
],
|
|
3450
3738
|
flowLineMultiplier: 2,
|
|
3451
3739
|
heroShape: true,
|
|
3452
3740
|
glowMultiplier: 3,
|
|
@@ -3469,6 +3757,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3469
3757
|
"stroke-only",
|
|
3470
3758
|
"dashed"
|
|
3471
3759
|
],
|
|
3760
|
+
preferredCompositions: [
|
|
3761
|
+
"flow-field",
|
|
3762
|
+
"grid-subdivision",
|
|
3763
|
+
"clustered"
|
|
3764
|
+
],
|
|
3472
3765
|
flowLineMultiplier: 1.5,
|
|
3473
3766
|
heroShape: false,
|
|
3474
3767
|
glowMultiplier: 0,
|
|
@@ -3490,6 +3783,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3490
3783
|
"watercolor",
|
|
3491
3784
|
"fill-and-stroke"
|
|
3492
3785
|
],
|
|
3786
|
+
preferredCompositions: [
|
|
3787
|
+
"radial",
|
|
3788
|
+
"spiral",
|
|
3789
|
+
"golden-spiral"
|
|
3790
|
+
],
|
|
3493
3791
|
flowLineMultiplier: 3,
|
|
3494
3792
|
heroShape: true,
|
|
3495
3793
|
glowMultiplier: 2.5,
|
|
@@ -3511,6 +3809,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3511
3809
|
"fill-only",
|
|
3512
3810
|
"incomplete"
|
|
3513
3811
|
],
|
|
3812
|
+
preferredCompositions: [
|
|
3813
|
+
"golden-spiral",
|
|
3814
|
+
"flow-field",
|
|
3815
|
+
"radial"
|
|
3816
|
+
],
|
|
3514
3817
|
flowLineMultiplier: 0.5,
|
|
3515
3818
|
heroShape: false,
|
|
3516
3819
|
glowMultiplier: 0.3,
|
|
@@ -3532,6 +3835,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3532
3835
|
"stroke-only",
|
|
3533
3836
|
"dashed"
|
|
3534
3837
|
],
|
|
3838
|
+
preferredCompositions: [
|
|
3839
|
+
"grid-subdivision",
|
|
3840
|
+
"radial"
|
|
3841
|
+
],
|
|
3535
3842
|
flowLineMultiplier: 0,
|
|
3536
3843
|
heroShape: false,
|
|
3537
3844
|
glowMultiplier: 0,
|
|
@@ -3553,6 +3860,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3553
3860
|
"fill-only",
|
|
3554
3861
|
"double-stroke"
|
|
3555
3862
|
],
|
|
3863
|
+
preferredCompositions: [
|
|
3864
|
+
"grid-subdivision",
|
|
3865
|
+
"clustered"
|
|
3866
|
+
],
|
|
3556
3867
|
flowLineMultiplier: 0,
|
|
3557
3868
|
heroShape: true,
|
|
3558
3869
|
glowMultiplier: 0,
|
|
@@ -3574,6 +3885,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3574
3885
|
"watercolor",
|
|
3575
3886
|
"fill-only"
|
|
3576
3887
|
],
|
|
3888
|
+
preferredCompositions: [
|
|
3889
|
+
"radial",
|
|
3890
|
+
"golden-spiral",
|
|
3891
|
+
"flow-field"
|
|
3892
|
+
],
|
|
3577
3893
|
flowLineMultiplier: 1,
|
|
3578
3894
|
heroShape: true,
|
|
3579
3895
|
glowMultiplier: 1,
|
|
@@ -3595,6 +3911,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3595
3911
|
"stroke-only",
|
|
3596
3912
|
"fill-only"
|
|
3597
3913
|
],
|
|
3914
|
+
preferredCompositions: [
|
|
3915
|
+
"clustered",
|
|
3916
|
+
"grid-subdivision",
|
|
3917
|
+
"radial"
|
|
3918
|
+
],
|
|
3598
3919
|
flowLineMultiplier: 0,
|
|
3599
3920
|
heroShape: false,
|
|
3600
3921
|
glowMultiplier: 0.3,
|
|
@@ -3616,6 +3937,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3616
3937
|
"fill-only",
|
|
3617
3938
|
"incomplete"
|
|
3618
3939
|
],
|
|
3940
|
+
preferredCompositions: [
|
|
3941
|
+
"flow-field",
|
|
3942
|
+
"golden-spiral",
|
|
3943
|
+
"spiral"
|
|
3944
|
+
],
|
|
3619
3945
|
flowLineMultiplier: 3,
|
|
3620
3946
|
heroShape: true,
|
|
3621
3947
|
glowMultiplier: 0.2,
|
|
@@ -3637,6 +3963,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3637
3963
|
"fill-only",
|
|
3638
3964
|
"hatched"
|
|
3639
3965
|
],
|
|
3966
|
+
preferredCompositions: [
|
|
3967
|
+
"radial",
|
|
3968
|
+
"clustered",
|
|
3969
|
+
"flow-field"
|
|
3970
|
+
],
|
|
3640
3971
|
flowLineMultiplier: 0,
|
|
3641
3972
|
heroShape: false,
|
|
3642
3973
|
glowMultiplier: 0,
|
|
@@ -3659,6 +3990,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3659
3990
|
"stroke-only",
|
|
3660
3991
|
"incomplete"
|
|
3661
3992
|
],
|
|
3993
|
+
preferredCompositions: [
|
|
3994
|
+
"spiral",
|
|
3995
|
+
"radial",
|
|
3996
|
+
"golden-spiral"
|
|
3997
|
+
],
|
|
3662
3998
|
flowLineMultiplier: 2,
|
|
3663
3999
|
heroShape: true,
|
|
3664
4000
|
glowMultiplier: 2.5,
|
|
@@ -3682,6 +4018,12 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3682
4018
|
...b.preferredStyles
|
|
3683
4019
|
])
|
|
3684
4020
|
];
|
|
4021
|
+
const mergedCompositions = [
|
|
4022
|
+
...new Set([
|
|
4023
|
+
...a.preferredCompositions,
|
|
4024
|
+
...b.preferredCompositions
|
|
4025
|
+
])
|
|
4026
|
+
];
|
|
3685
4027
|
return {
|
|
3686
4028
|
name: `${a.name}+${b.name}`,
|
|
3687
4029
|
gridSize: Math.round($3faa2521b78398cf$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3693,6 +4035,7 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3693
4035
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3694
4036
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3695
4037
|
preferredStyles: mergedStyles,
|
|
4038
|
+
preferredCompositions: mergedCompositions,
|
|
3696
4039
|
flowLineMultiplier: $3faa2521b78398cf$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3697
4040
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3698
4041
|
glowMultiplier: $3faa2521b78398cf$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3726,12 +4069,14 @@ const $b623126c6e9cbb71$var$SACRED_SHAPES = [
|
|
|
3726
4069
|
"torus",
|
|
3727
4070
|
"eggOfLife"
|
|
3728
4071
|
];
|
|
3729
|
-
|
|
4072
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4073
|
+
const $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES = [
|
|
3730
4074
|
"radial",
|
|
3731
4075
|
"flow-field",
|
|
3732
4076
|
"spiral",
|
|
3733
4077
|
"grid-subdivision",
|
|
3734
|
-
"clustered"
|
|
4078
|
+
"clustered",
|
|
4079
|
+
"golden-spiral"
|
|
3735
4080
|
];
|
|
3736
4081
|
// ── Helper: get position based on composition mode ──────────────────
|
|
3737
4082
|
function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
|
|
@@ -3789,6 +4134,21 @@ function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height,
|
|
|
3789
4134
|
x: rng() * width,
|
|
3790
4135
|
y: rng() * height
|
|
3791
4136
|
};
|
|
4137
|
+
case "golden-spiral":
|
|
4138
|
+
{
|
|
4139
|
+
// Logarithmic spiral: r = a * e^(b*theta), with golden angle spacing
|
|
4140
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4141
|
+
const goldenAngle = 2 * Math.PI / (PHI * PHI); // ~137.5° in radians
|
|
4142
|
+
const t = shapeIndex / totalShapes;
|
|
4143
|
+
const angle = shapeIndex * goldenAngle + rng() * 0.3;
|
|
4144
|
+
const maxR = Math.min(width, height) * 0.44;
|
|
4145
|
+
// Shapes spiral outward with sqrt distribution for even area coverage
|
|
4146
|
+
const r = Math.sqrt(t) * maxR + (rng() - 0.5) * maxR * 0.08;
|
|
4147
|
+
return {
|
|
4148
|
+
x: cx + Math.cos(angle) * r,
|
|
4149
|
+
y: cy + Math.sin(angle) * r
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
3792
4152
|
}
|
|
3793
4153
|
}
|
|
3794
4154
|
// ── Helper: positional color from hierarchy ─────────────────────────
|
|
@@ -3812,7 +4172,69 @@ function $b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones) {
|
|
|
3812
4172
|
}
|
|
3813
4173
|
return false;
|
|
3814
4174
|
}
|
|
3815
|
-
// ──
|
|
4175
|
+
// ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
|
|
4176
|
+
class $b623126c6e9cbb71$var$SpatialGrid {
|
|
4177
|
+
cells;
|
|
4178
|
+
cellSize;
|
|
4179
|
+
constructor(cellSize){
|
|
4180
|
+
this.cells = new Map();
|
|
4181
|
+
this.cellSize = cellSize;
|
|
4182
|
+
}
|
|
4183
|
+
key(cx, cy) {
|
|
4184
|
+
return `${cx},${cy}`;
|
|
4185
|
+
}
|
|
4186
|
+
insert(item) {
|
|
4187
|
+
const cx = Math.floor(item.x / this.cellSize);
|
|
4188
|
+
const cy = Math.floor(item.y / this.cellSize);
|
|
4189
|
+
const k = this.key(cx, cy);
|
|
4190
|
+
const cell = this.cells.get(k);
|
|
4191
|
+
if (cell) cell.push(item);
|
|
4192
|
+
else this.cells.set(k, [
|
|
4193
|
+
item
|
|
4194
|
+
]);
|
|
4195
|
+
}
|
|
4196
|
+
/** Count items within radius of (x, y) */ countNear(x, y, radius) {
|
|
4197
|
+
const r2 = radius * radius;
|
|
4198
|
+
const minCx = Math.floor((x - radius) / this.cellSize);
|
|
4199
|
+
const maxCx = Math.floor((x + radius) / this.cellSize);
|
|
4200
|
+
const minCy = Math.floor((y - radius) / this.cellSize);
|
|
4201
|
+
const maxCy = Math.floor((y + radius) / this.cellSize);
|
|
4202
|
+
let count = 0;
|
|
4203
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4204
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4205
|
+
if (!cell) continue;
|
|
4206
|
+
for (const p of cell){
|
|
4207
|
+
const dx = x - p.x;
|
|
4208
|
+
const dy = y - p.y;
|
|
4209
|
+
if (dx * dx + dy * dy < r2) count++;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
return count;
|
|
4213
|
+
}
|
|
4214
|
+
/** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
|
|
4215
|
+
const minCx = Math.floor((x - searchRadius) / this.cellSize);
|
|
4216
|
+
const maxCx = Math.floor((x + searchRadius) / this.cellSize);
|
|
4217
|
+
const minCy = Math.floor((y - searchRadius) / this.cellSize);
|
|
4218
|
+
const maxCy = Math.floor((y + searchRadius) / this.cellSize);
|
|
4219
|
+
let nearest = null;
|
|
4220
|
+
let bestDist2 = Infinity;
|
|
4221
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4222
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4223
|
+
if (!cell) continue;
|
|
4224
|
+
for (const p of cell){
|
|
4225
|
+
const dx = x - p.x;
|
|
4226
|
+
const dy = y - p.y;
|
|
4227
|
+
const d2 = dx * dx + dy * dy;
|
|
4228
|
+
if (d2 > 0 && d2 < bestDist2) {
|
|
4229
|
+
bestDist2 = d2;
|
|
4230
|
+
nearest = p;
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
return nearest;
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
// ── Helper: density check (legacy wrapper) ──────────────────────────
|
|
3816
4238
|
function $b623126c6e9cbb71$var$localDensity(x, y, positions, radius) {
|
|
3817
4239
|
let count = 0;
|
|
3818
4240
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4111,42 +4533,43 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4111
4533
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4112
4534
|
const patternColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4113
4535
|
if (bgPatternRoll < 0.2) {
|
|
4114
|
-
// Dot grid
|
|
4536
|
+
// Dot grid — batched into a single path
|
|
4115
4537
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4116
4538
|
const dotR = dotSpacing * 0.08;
|
|
4117
4539
|
ctx.globalAlpha = patternOpacity;
|
|
4118
4540
|
ctx.fillStyle = patternColor;
|
|
4541
|
+
ctx.beginPath();
|
|
4119
4542
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4120
|
-
ctx.
|
|
4543
|
+
ctx.moveTo(px + dotR, py);
|
|
4121
4544
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4122
|
-
ctx.fill();
|
|
4123
4545
|
}
|
|
4546
|
+
ctx.fill();
|
|
4124
4547
|
} else if (bgPatternRoll < 0.4) {
|
|
4125
|
-
// Diagonal lines
|
|
4548
|
+
// Diagonal lines — batched into a single path
|
|
4126
4549
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4127
4550
|
ctx.globalAlpha = patternOpacity;
|
|
4128
4551
|
ctx.strokeStyle = patternColor;
|
|
4129
4552
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4130
4553
|
const diag = Math.hypot(width, height);
|
|
4554
|
+
ctx.beginPath();
|
|
4131
4555
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4132
|
-
ctx.beginPath();
|
|
4133
4556
|
ctx.moveTo(d, 0);
|
|
4134
4557
|
ctx.lineTo(d + height, height);
|
|
4135
|
-
ctx.stroke();
|
|
4136
4558
|
}
|
|
4559
|
+
ctx.stroke();
|
|
4137
4560
|
} else {
|
|
4138
|
-
// Tessellation — hexagonal grid
|
|
4561
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4139
4562
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4140
4563
|
const tessH = tessSize * Math.sqrt(3);
|
|
4141
4564
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4142
4565
|
ctx.strokeStyle = patternColor;
|
|
4143
4566
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4567
|
+
ctx.beginPath();
|
|
4144
4568
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4145
4569
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4146
4570
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4147
4571
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4148
4572
|
const hy = row * tessH;
|
|
4149
|
-
ctx.beginPath();
|
|
4150
4573
|
for(let s = 0; s < 6; s++){
|
|
4151
4574
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4152
4575
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4155,18 +4578,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4155
4578
|
else ctx.lineTo(vx, vy);
|
|
4156
4579
|
}
|
|
4157
4580
|
ctx.closePath();
|
|
4158
|
-
ctx.stroke();
|
|
4159
4581
|
}
|
|
4160
4582
|
}
|
|
4583
|
+
ctx.stroke();
|
|
4161
4584
|
}
|
|
4162
4585
|
ctx.restore();
|
|
4163
4586
|
}
|
|
4164
4587
|
ctx.globalCompositeOperation = "source-over";
|
|
4165
|
-
// ── 2. Composition mode
|
|
4166
|
-
const compositionMode = $b623126c6e9cbb71$var$
|
|
4588
|
+
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4589
|
+
const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES.length)];
|
|
4167
4590
|
const symRoll = rng();
|
|
4168
4591
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4169
|
-
// ── 3. Focal points + void zones
|
|
4592
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4170
4593
|
const THIRDS_POINTS = [
|
|
4171
4594
|
{
|
|
4172
4595
|
x: 1 / 3,
|
|
@@ -4199,9 +4622,23 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4199
4622
|
y: height * (0.2 + rng() * 0.6),
|
|
4200
4623
|
strength: 0.3 + rng() * 0.4
|
|
4201
4624
|
});
|
|
4202
|
-
|
|
4625
|
+
// Archetype-aware void zones: dense archetypes get fewer/no voids,
|
|
4626
|
+
// minimal archetypes get golden-ratio positioned voids
|
|
4627
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4628
|
+
const isMinimalArchetype = archetype.gridSize <= 3;
|
|
4629
|
+
const isDenseArchetype = archetype.gridSize >= 8;
|
|
4630
|
+
const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
|
|
4203
4631
|
const voidZones = [];
|
|
4204
|
-
for(let v = 0; v < numVoids; v++)
|
|
4632
|
+
for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
|
|
4633
|
+
// Place voids at golden-ratio positions for intentional negative space
|
|
4634
|
+
const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
|
|
4635
|
+
const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
|
|
4636
|
+
voidZones.push({
|
|
4637
|
+
x: width * (gx + (rng() - 0.5) * 0.05),
|
|
4638
|
+
y: height * (gy + (rng() - 0.5) * 0.05),
|
|
4639
|
+
radius: Math.min(width, height) * (0.08 + rng() * 0.08)
|
|
4640
|
+
});
|
|
4641
|
+
} else voidZones.push({
|
|
4205
4642
|
x: width * (0.15 + rng() * 0.7),
|
|
4206
4643
|
y: height * (0.15 + rng() * 0.7),
|
|
4207
4644
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4257,14 +4694,30 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4257
4694
|
}
|
|
4258
4695
|
}
|
|
4259
4696
|
ctx.globalAlpha = 1;
|
|
4260
|
-
// ── 4. Flow field
|
|
4697
|
+
// ── 4. Flow field — simplex noise for organic variation ─────────
|
|
4698
|
+
// Create a seeded simplex noise field (unique per hash)
|
|
4699
|
+
const noiseFieldRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 333));
|
|
4700
|
+
const simplexNoise = (0, $461134e0b6ce0619$export$bbde7fbaaf9a8d66)(noiseFieldRng);
|
|
4701
|
+
const fbmNoise = (0, $461134e0b6ce0619$export$c81d639e83a19b85)(simplexNoise, 3, 2.0, 0.5);
|
|
4261
4702
|
const fieldAngleBase = rng() * Math.PI * 2;
|
|
4262
|
-
const fieldFreq =
|
|
4703
|
+
const fieldFreq = 1.5 + rng() * 2.5; // noise sampling frequency
|
|
4263
4704
|
function flowAngle(x, y) {
|
|
4264
|
-
|
|
4705
|
+
// Sample FBM noise at the position, scaled by frequency
|
|
4706
|
+
const nx = x / width * fieldFreq;
|
|
4707
|
+
const ny = y / height * fieldFreq;
|
|
4708
|
+
return fieldAngleBase + fbmNoise(nx, ny) * Math.PI;
|
|
4709
|
+
}
|
|
4710
|
+
// Noise-based size modulation — shapes in "high noise" areas get scaled
|
|
4711
|
+
function noiseSizeModulation(x, y) {
|
|
4712
|
+
const n = simplexNoise(x / width * 3, y / height * 3);
|
|
4713
|
+
// Map [-1,1] to [0.7, 1.3] — subtle terrain-like size variation
|
|
4714
|
+
return 0.7 + (n + 1) * 0.3;
|
|
4265
4715
|
}
|
|
4266
4716
|
// Track all placed shapes for density checks and connecting curves
|
|
4267
4717
|
const shapePositions = [];
|
|
4718
|
+
// Spatial grid for O(1) density and nearest-neighbor lookups
|
|
4719
|
+
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4720
|
+
const spatialGrid = new $b623126c6e9cbb71$var$SpatialGrid(densityCheckRadius);
|
|
4268
4721
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4269
4722
|
let heroCenter = null;
|
|
4270
4723
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4295,7 +4748,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4295
4748
|
glowColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(heroStroke, 0.4),
|
|
4296
4749
|
gradientFillEnd: (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(colorHierarchy.secondary, rng, 10, 0.1),
|
|
4297
4750
|
renderStyle: heroStyle,
|
|
4298
|
-
rng: rng
|
|
4751
|
+
rng: rng,
|
|
4752
|
+
lightAngle: lightAngle,
|
|
4753
|
+
scaleFactor: scaleFactor
|
|
4299
4754
|
});
|
|
4300
4755
|
heroCenter = {
|
|
4301
4756
|
x: heroFocal.x,
|
|
@@ -4308,9 +4763,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4308
4763
|
size: heroSize,
|
|
4309
4764
|
shape: heroShape
|
|
4310
4765
|
});
|
|
4766
|
+
spatialGrid.insert({
|
|
4767
|
+
x: heroFocal.x,
|
|
4768
|
+
y: heroFocal.y,
|
|
4769
|
+
size: heroSize,
|
|
4770
|
+
shape: heroShape
|
|
4771
|
+
});
|
|
4311
4772
|
}
|
|
4312
4773
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4313
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4314
4774
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4315
4775
|
for(let layer = 0; layer < layers; layer++){
|
|
4316
4776
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
@@ -4349,12 +4809,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4349
4809
|
if ($b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones)) {
|
|
4350
4810
|
if (rng() < 0.85) continue;
|
|
4351
4811
|
}
|
|
4352
|
-
if (
|
|
4812
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4353
4813
|
if (rng() < 0.6) continue;
|
|
4354
4814
|
}
|
|
4355
4815
|
// Power distribution for size — archetype controls the curve
|
|
4356
4816
|
const sizeT = Math.pow(rng(), archetype.sizePower);
|
|
4357
|
-
const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
|
|
4817
|
+
const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale * noiseSizeModulation(x, y);
|
|
4358
4818
|
// Size fraction for affinity-aware shape selection
|
|
4359
4819
|
const sizeFraction = size / adjustedMaxSize;
|
|
4360
4820
|
// Palette-driven shape selection (replaces naive pickShape)
|
|
@@ -4409,17 +4869,11 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4409
4869
|
let finalX = x;
|
|
4410
4870
|
let finalY = y;
|
|
4411
4871
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4412
|
-
//
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
for (const sp of shapePositions){
|
|
4416
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4417
|
-
if (d < nearestDist && d > 0) {
|
|
4418
|
-
nearestDist = d;
|
|
4419
|
-
nearestPos = sp;
|
|
4420
|
-
}
|
|
4421
|
-
}
|
|
4872
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
4873
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
4874
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4422
4875
|
if (nearestPos) {
|
|
4876
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4423
4877
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4424
4878
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4425
4879
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4457,7 +4911,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4457
4911
|
glowColor: hasGlow ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.6) : shadowDist > 0 ? "rgba(0,0,0,0.08)" : undefined,
|
|
4458
4912
|
gradientFillEnd: gradientEnd,
|
|
4459
4913
|
renderStyle: finalRenderStyle,
|
|
4460
|
-
rng: rng
|
|
4914
|
+
rng: rng,
|
|
4915
|
+
lightAngle: lightAngle,
|
|
4916
|
+
scaleFactor: scaleFactor
|
|
4461
4917
|
};
|
|
4462
4918
|
if (shouldMirror) (0, $9beb8f41637c29fd$export$8bd8bbd1a8e53689)(ctx, shape, finalX, finalY, {
|
|
4463
4919
|
...shapeConfig,
|
|
@@ -4490,6 +4946,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4490
4946
|
size: size,
|
|
4491
4947
|
shape: shape
|
|
4492
4948
|
});
|
|
4949
|
+
spatialGrid.insert({
|
|
4950
|
+
x: finalX,
|
|
4951
|
+
y: finalY,
|
|
4952
|
+
size: size,
|
|
4953
|
+
shape: shape
|
|
4954
|
+
});
|
|
4493
4955
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4494
4956
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4495
4957
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
@@ -4518,6 +4980,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4518
4980
|
size: echoSize,
|
|
4519
4981
|
shape: shape
|
|
4520
4982
|
});
|
|
4983
|
+
spatialGrid.insert({
|
|
4984
|
+
x: echoX,
|
|
4985
|
+
y: echoY,
|
|
4986
|
+
size: echoSize,
|
|
4987
|
+
shape: shape
|
|
4988
|
+
});
|
|
4521
4989
|
}
|
|
4522
4990
|
}
|
|
4523
4991
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
@@ -4582,12 +5050,119 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4582
5050
|
size: member.size,
|
|
4583
5051
|
shape: memberShape
|
|
4584
5052
|
});
|
|
5053
|
+
spatialGrid.insert({
|
|
5054
|
+
x: mx,
|
|
5055
|
+
y: my,
|
|
5056
|
+
size: member.size,
|
|
5057
|
+
shape: memberShape
|
|
5058
|
+
});
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
5062
|
+
// ~12% of medium-large shapes spawn a rhythmic sequence
|
|
5063
|
+
if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
|
|
5064
|
+
const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
|
|
5065
|
+
const rhythmAngle = rng() * Math.PI * 2;
|
|
5066
|
+
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5067
|
+
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5068
|
+
const rhythmShape = shape; // same shape for visual rhythm
|
|
5069
|
+
let rhythmSize = size * 0.6;
|
|
5070
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5071
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5072
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5073
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5074
|
+
if ($b623126c6e9cbb71$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5075
|
+
rhythmSize *= rhythmDecay;
|
|
5076
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5077
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5078
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5079
|
+
const rhythmFill = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$18a34c25ea7e724b)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5080
|
+
(0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5081
|
+
fillColor: rhythmFill,
|
|
5082
|
+
strokeColor: (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5083
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5084
|
+
size: rhythmSize,
|
|
5085
|
+
rotation: rotation + (r + 1) * 12,
|
|
5086
|
+
proportionType: "GOLDEN_RATIO",
|
|
5087
|
+
renderStyle: finalRenderStyle,
|
|
5088
|
+
rng: rng
|
|
5089
|
+
});
|
|
5090
|
+
shapePositions.push({
|
|
5091
|
+
x: rx,
|
|
5092
|
+
y: ry,
|
|
5093
|
+
size: rhythmSize,
|
|
5094
|
+
shape: rhythmShape
|
|
5095
|
+
});
|
|
5096
|
+
spatialGrid.insert({
|
|
5097
|
+
x: rx,
|
|
5098
|
+
y: ry,
|
|
5099
|
+
size: rhythmSize,
|
|
5100
|
+
shape: rhythmShape
|
|
5101
|
+
});
|
|
4585
5102
|
}
|
|
4586
5103
|
}
|
|
4587
5104
|
}
|
|
4588
5105
|
}
|
|
4589
5106
|
// Reset blend mode for post-processing passes
|
|
4590
5107
|
ctx.globalCompositeOperation = "source-over";
|
|
5108
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
5109
|
+
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
5110
|
+
// with a tinted background wash, creating a "peek through" effect.
|
|
5111
|
+
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
5112
|
+
const portalCount = 1 + Math.floor(rng() * 2);
|
|
5113
|
+
for(let p = 0; p < portalCount; p++){
|
|
5114
|
+
// Pick a position biased toward placed shapes
|
|
5115
|
+
const sourceShape = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
5116
|
+
const portalX = sourceShape.x + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
5117
|
+
const portalY = sourceShape.y + (rng() - 0.5) * sourceShape.size * 0.5;
|
|
5118
|
+
const portalSize = adjustedMaxSize * (0.15 + rng() * 0.25);
|
|
5119
|
+
// Pick a portal shape from the palette
|
|
5120
|
+
const portalShape = (0, $24064302523652b1$export$3c37d9a045754d0e)(shapePalette, rng, portalSize / adjustedMaxSize);
|
|
5121
|
+
const portalRotation = rng() * 360;
|
|
5122
|
+
const portalAlpha = 0.6 + rng() * 0.35;
|
|
5123
|
+
ctx.save();
|
|
5124
|
+
ctx.translate(portalX, portalY);
|
|
5125
|
+
ctx.rotate(portalRotation * Math.PI / 180);
|
|
5126
|
+
// Step 1: Clip to the portal shape and fill with background wash
|
|
5127
|
+
ctx.beginPath();
|
|
5128
|
+
(0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize);
|
|
5129
|
+
ctx.clip();
|
|
5130
|
+
// Fill the clipped region with a radial gradient from background colors
|
|
5131
|
+
const portalColor = (0, $9d614e7d77fc2947$export$18a34c25ea7e724b)(bgStart, rng, 15, 0.1);
|
|
5132
|
+
const portalGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, portalSize);
|
|
5133
|
+
portalGrad.addColorStop(0, portalColor);
|
|
5134
|
+
portalGrad.addColorStop(1, bgEnd);
|
|
5135
|
+
ctx.globalAlpha = portalAlpha;
|
|
5136
|
+
ctx.fillStyle = portalGrad;
|
|
5137
|
+
ctx.fillRect(-portalSize, -portalSize, portalSize * 2, portalSize * 2);
|
|
5138
|
+
// Optional: subtle inner texture — a few tiny dots inside the portal
|
|
5139
|
+
if (rng() < 0.5) {
|
|
5140
|
+
const dotCount = 3 + Math.floor(rng() * 5);
|
|
5141
|
+
ctx.globalAlpha = portalAlpha * 0.3;
|
|
5142
|
+
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.2);
|
|
5143
|
+
for(let d = 0; d < dotCount; d++){
|
|
5144
|
+
const dx = (rng() - 0.5) * portalSize * 1.4;
|
|
5145
|
+
const dy = (rng() - 0.5) * portalSize * 1.4;
|
|
5146
|
+
const dr = (1 + rng() * 3) * scaleFactor;
|
|
5147
|
+
ctx.beginPath();
|
|
5148
|
+
ctx.arc(dx, dy, dr, 0, Math.PI * 2);
|
|
5149
|
+
ctx.fill();
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
ctx.restore();
|
|
5153
|
+
// Step 2: Draw a border ring around the portal (outside the clip)
|
|
5154
|
+
ctx.save();
|
|
5155
|
+
ctx.translate(portalX, portalY);
|
|
5156
|
+
ctx.rotate(portalRotation * Math.PI / 180);
|
|
5157
|
+
ctx.globalAlpha = 0.15 + rng() * 0.2;
|
|
5158
|
+
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)((0, $9d614e7d77fc2947$export$b49f62f0a99da0e8)(colorHierarchy, rng), 0.5);
|
|
5159
|
+
ctx.lineWidth = (1.5 + rng() * 2.5) * scaleFactor;
|
|
5160
|
+
ctx.beginPath();
|
|
5161
|
+
(0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[portalShape]?.(ctx, portalSize * 1.06);
|
|
5162
|
+
ctx.stroke();
|
|
5163
|
+
ctx.restore();
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
4591
5166
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
4592
5167
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
4593
5168
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
@@ -4611,6 +5186,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4611
5186
|
fx += Math.cos(angle) * stepLen;
|
|
4612
5187
|
fy += Math.sin(angle) * stepLen;
|
|
4613
5188
|
if (fx < 0 || fx > width || fy < 0 || fy > height) break;
|
|
5189
|
+
// Skip segments that pass through void zones
|
|
5190
|
+
if ($b623126c6e9cbb71$var$isInVoidZone(fx, fy, voidZones)) {
|
|
5191
|
+
prevX = fx;
|
|
5192
|
+
prevY = fy;
|
|
5193
|
+
continue;
|
|
5194
|
+
}
|
|
4614
5195
|
const t = s / steps;
|
|
4615
5196
|
// Taper + pressure
|
|
4616
5197
|
const taper = 1 - t * 0.8;
|
|
@@ -4708,30 +5289,60 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4708
5289
|
}
|
|
4709
5290
|
ctx.restore();
|
|
4710
5291
|
}
|
|
4711
|
-
// ── 7. Noise texture overlay
|
|
5292
|
+
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
4712
5293
|
const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
|
|
4713
5294
|
const noiseDensity = Math.floor(width * height / 800);
|
|
4714
|
-
|
|
4715
|
-
const
|
|
4716
|
-
const
|
|
4717
|
-
const
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
5295
|
+
try {
|
|
5296
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5297
|
+
const data = imageData.data;
|
|
5298
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5299
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5300
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5301
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5302
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5303
|
+
const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
|
|
5304
|
+
// Write a small block of pixels for scale
|
|
5305
|
+
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5306
|
+
const idx = ((ny + dy) * width + (nx + dx)) * 4;
|
|
5307
|
+
// Alpha-blend the noise dot onto existing pixel data
|
|
5308
|
+
const srcA = alpha / 255;
|
|
5309
|
+
const invA = 1 - srcA;
|
|
5310
|
+
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5311
|
+
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5312
|
+
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5313
|
+
// Keep existing alpha
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
ctx.putImageData(imageData, 0, 0);
|
|
5317
|
+
} catch {
|
|
5318
|
+
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5319
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5320
|
+
const nx = noiseRng() * width;
|
|
5321
|
+
const ny = noiseRng() * height;
|
|
5322
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5323
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5324
|
+
ctx.globalAlpha = alpha;
|
|
5325
|
+
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5326
|
+
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5327
|
+
}
|
|
4722
5328
|
}
|
|
4723
5329
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
4724
5330
|
ctx.globalAlpha = 1;
|
|
4725
5331
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
4726
5332
|
const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
|
|
5333
|
+
// Tint vignette based on background: warm sepia for light, cool blue for dark
|
|
5334
|
+
const isLightBg = bgLum > 0.5;
|
|
5335
|
+
const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
|
|
5336
|
+
: `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
|
|
4727
5337
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
4728
5338
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
4729
|
-
vigGrad.addColorStop(1,
|
|
5339
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
4730
5340
|
ctx.fillStyle = vigGrad;
|
|
4731
5341
|
ctx.fillRect(0, 0, width, height);
|
|
4732
|
-
// ── 9. Organic connecting curves
|
|
5342
|
+
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
4733
5343
|
if (shapePositions.length > 1) {
|
|
4734
5344
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5345
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
4735
5346
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
4736
5347
|
for(let i = 0; i < numCurves; i++){
|
|
4737
5348
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
@@ -4739,11 +5350,13 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4739
5350
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
4740
5351
|
const a = shapePositions[idxA];
|
|
4741
5352
|
const b = shapePositions[idxB];
|
|
4742
|
-
const mx = (a.x + b.x) / 2;
|
|
4743
|
-
const my = (a.y + b.y) / 2;
|
|
4744
5353
|
const dx = b.x - a.x;
|
|
4745
5354
|
const dy = b.y - a.y;
|
|
4746
5355
|
const dist = Math.hypot(dx, dy);
|
|
5356
|
+
// Skip connections between distant shapes
|
|
5357
|
+
if (dist > maxCurveDist) continue;
|
|
5358
|
+
const mx = (a.x + b.x) / 2;
|
|
5359
|
+
const my = (a.y + b.y) / 2;
|
|
4747
5360
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
4748
5361
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
4749
5362
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
@@ -4799,13 +5412,211 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4799
5412
|
ctx.restore();
|
|
4800
5413
|
ctx.globalCompositeOperation = "source-over";
|
|
4801
5414
|
}
|
|
4802
|
-
//
|
|
5415
|
+
// 10d. Gradient map — map luminance through a two-color gradient
|
|
5416
|
+
// Uses dominant→accent as the dark→light ramp for a cohesive tonal look
|
|
5417
|
+
if (rng() < 0.35) {
|
|
5418
|
+
const gmDark = colorHierarchy.dominant;
|
|
5419
|
+
const gmLight = colorHierarchy.accent;
|
|
5420
|
+
ctx.globalAlpha = 0.06 + rng() * 0.06; // very subtle: 6-12%
|
|
5421
|
+
ctx.globalCompositeOperation = "color";
|
|
5422
|
+
// Paint a linear gradient from dark color (top) to light color (bottom)
|
|
5423
|
+
const gmGrad = ctx.createLinearGradient(0, 0, 0, height);
|
|
5424
|
+
gmGrad.addColorStop(0, gmDark);
|
|
5425
|
+
gmGrad.addColorStop(1, gmLight);
|
|
5426
|
+
ctx.fillStyle = gmGrad;
|
|
5427
|
+
ctx.fillRect(0, 0, width, height);
|
|
5428
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5429
|
+
}
|
|
5430
|
+
// ── 10e. Generative borders — archetype-driven decorative frames ──
|
|
5431
|
+
{
|
|
5432
|
+
ctx.save();
|
|
5433
|
+
ctx.globalAlpha = 1;
|
|
5434
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5435
|
+
const borderRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 314));
|
|
5436
|
+
const borderPad = Math.min(width, height) * 0.025;
|
|
5437
|
+
const borderColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
|
|
5438
|
+
const borderColorSolid = colorHierarchy.accent;
|
|
5439
|
+
const archName = archetype.name;
|
|
5440
|
+
if (archName.includes("geometric") || archName.includes("op-art") || archName.includes("shattered")) {
|
|
5441
|
+
// Clean ruled lines with corner ornaments
|
|
5442
|
+
ctx.strokeStyle = borderColor;
|
|
5443
|
+
ctx.lineWidth = Math.max(1, 1.5 * scaleFactor);
|
|
5444
|
+
ctx.globalAlpha = 0.18 + borderRng() * 0.1;
|
|
5445
|
+
// Outer rule
|
|
5446
|
+
ctx.strokeRect(borderPad, borderPad, width - borderPad * 2, height - borderPad * 2);
|
|
5447
|
+
// Inner rule (thinner, offset)
|
|
5448
|
+
const innerPad = borderPad * 1.8;
|
|
5449
|
+
ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
|
|
5450
|
+
ctx.globalAlpha *= 0.7;
|
|
5451
|
+
ctx.strokeRect(innerPad, innerPad, width - innerPad * 2, height - innerPad * 2);
|
|
5452
|
+
// Corner ornaments — small squares at each corner
|
|
5453
|
+
const ornSize = borderPad * 0.6;
|
|
5454
|
+
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(borderColorSolid, 0.12);
|
|
5455
|
+
const corners = [
|
|
5456
|
+
[
|
|
5457
|
+
borderPad,
|
|
5458
|
+
borderPad
|
|
5459
|
+
],
|
|
5460
|
+
[
|
|
5461
|
+
width - borderPad - ornSize,
|
|
5462
|
+
borderPad
|
|
5463
|
+
],
|
|
5464
|
+
[
|
|
5465
|
+
borderPad,
|
|
5466
|
+
height - borderPad - ornSize
|
|
5467
|
+
],
|
|
5468
|
+
[
|
|
5469
|
+
width - borderPad - ornSize,
|
|
5470
|
+
height - borderPad - ornSize
|
|
5471
|
+
]
|
|
5472
|
+
];
|
|
5473
|
+
for (const [cx2, cy2] of corners){
|
|
5474
|
+
ctx.fillRect(cx2, cy2, ornSize, ornSize);
|
|
5475
|
+
// Diagonal cross inside ornament
|
|
5476
|
+
ctx.beginPath();
|
|
5477
|
+
ctx.moveTo(cx2, cy2);
|
|
5478
|
+
ctx.lineTo(cx2 + ornSize, cy2 + ornSize);
|
|
5479
|
+
ctx.moveTo(cx2 + ornSize, cy2);
|
|
5480
|
+
ctx.lineTo(cx2, cy2 + ornSize);
|
|
5481
|
+
ctx.stroke();
|
|
5482
|
+
}
|
|
5483
|
+
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
5484
|
+
// Vine tendrils — organic curving lines along edges
|
|
5485
|
+
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.15);
|
|
5486
|
+
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
5487
|
+
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
5488
|
+
ctx.lineCap = "round";
|
|
5489
|
+
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
5490
|
+
for(let t = 0; t < tendrilCount; t++){
|
|
5491
|
+
// Start from a random edge point
|
|
5492
|
+
const edge = Math.floor(borderRng() * 4);
|
|
5493
|
+
let tx, ty;
|
|
5494
|
+
if (edge === 0) {
|
|
5495
|
+
tx = borderRng() * width;
|
|
5496
|
+
ty = borderPad;
|
|
5497
|
+
} else if (edge === 1) {
|
|
5498
|
+
tx = borderRng() * width;
|
|
5499
|
+
ty = height - borderPad;
|
|
5500
|
+
} else if (edge === 2) {
|
|
5501
|
+
tx = borderPad;
|
|
5502
|
+
ty = borderRng() * height;
|
|
5503
|
+
} else {
|
|
5504
|
+
tx = width - borderPad;
|
|
5505
|
+
ty = borderRng() * height;
|
|
5506
|
+
}
|
|
5507
|
+
ctx.beginPath();
|
|
5508
|
+
ctx.moveTo(tx, ty);
|
|
5509
|
+
const segs = 3 + Math.floor(borderRng() * 4);
|
|
5510
|
+
for(let s = 0; s < segs; s++){
|
|
5511
|
+
const inward = borderPad * (1 + borderRng() * 2);
|
|
5512
|
+
// Curl inward from edge
|
|
5513
|
+
const cpx2 = tx + (borderRng() - 0.5) * borderPad * 4;
|
|
5514
|
+
const cpy2 = ty + (edge < 2 ? edge === 0 ? inward : -inward : 0);
|
|
5515
|
+
const cpx3 = tx + (edge >= 2 ? edge === 2 ? inward : -inward : (borderRng() - 0.5) * borderPad * 3);
|
|
5516
|
+
const cpy3 = ty + (borderRng() - 0.5) * borderPad * 3;
|
|
5517
|
+
tx = cpx3;
|
|
5518
|
+
ty = cpy3;
|
|
5519
|
+
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
5520
|
+
}
|
|
5521
|
+
ctx.stroke();
|
|
5522
|
+
// Small leaf/dot at tendril end
|
|
5523
|
+
if (borderRng() < 0.6) {
|
|
5524
|
+
ctx.beginPath();
|
|
5525
|
+
ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
|
|
5526
|
+
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.secondary, 0.08);
|
|
5527
|
+
ctx.fill();
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
5531
|
+
// Star-studded arcs along edges
|
|
5532
|
+
ctx.globalAlpha = 0.1 + borderRng() * 0.08;
|
|
5533
|
+
ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.2);
|
|
5534
|
+
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.12);
|
|
5535
|
+
ctx.lineWidth = Math.max(0.5, 0.7 * scaleFactor);
|
|
5536
|
+
// Subtle arc along top and bottom
|
|
5537
|
+
ctx.beginPath();
|
|
5538
|
+
ctx.arc(cx, -height * 0.3, height * 0.6, 0.3, Math.PI - 0.3);
|
|
5539
|
+
ctx.stroke();
|
|
5540
|
+
ctx.beginPath();
|
|
5541
|
+
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
5542
|
+
ctx.stroke();
|
|
5543
|
+
// Scatter small stars along the border region
|
|
5544
|
+
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
5545
|
+
for(let s = 0; s < starCount; s++){
|
|
5546
|
+
const edge = Math.floor(borderRng() * 4);
|
|
5547
|
+
let sx, sy;
|
|
5548
|
+
if (edge === 0) {
|
|
5549
|
+
sx = borderRng() * width;
|
|
5550
|
+
sy = borderPad * (0.5 + borderRng());
|
|
5551
|
+
} else if (edge === 1) {
|
|
5552
|
+
sx = borderRng() * width;
|
|
5553
|
+
sy = height - borderPad * (0.5 + borderRng());
|
|
5554
|
+
} else if (edge === 2) {
|
|
5555
|
+
sx = borderPad * (0.5 + borderRng());
|
|
5556
|
+
sy = borderRng() * height;
|
|
5557
|
+
} else {
|
|
5558
|
+
sx = width - borderPad * (0.5 + borderRng());
|
|
5559
|
+
sy = borderRng() * height;
|
|
5560
|
+
}
|
|
5561
|
+
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
5562
|
+
// 4-point star
|
|
5563
|
+
ctx.beginPath();
|
|
5564
|
+
for(let p = 0; p < 8; p++){
|
|
5565
|
+
const a = p / 8 * Math.PI * 2;
|
|
5566
|
+
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
5567
|
+
const px2 = sx + Math.cos(a) * r;
|
|
5568
|
+
const py2 = sy + Math.sin(a) * r;
|
|
5569
|
+
if (p === 0) ctx.moveTo(px2, py2);
|
|
5570
|
+
else ctx.lineTo(px2, py2);
|
|
5571
|
+
}
|
|
5572
|
+
ctx.closePath();
|
|
5573
|
+
ctx.fill();
|
|
5574
|
+
}
|
|
5575
|
+
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
5576
|
+
// Thin single rule — understated elegance
|
|
5577
|
+
ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.1);
|
|
5578
|
+
ctx.lineWidth = Math.max(0.5, 0.6 * scaleFactor);
|
|
5579
|
+
ctx.globalAlpha = 0.1 + borderRng() * 0.06;
|
|
5580
|
+
ctx.strokeRect(borderPad * 1.5, borderPad * 1.5, width - borderPad * 3, height - borderPad * 3);
|
|
5581
|
+
}
|
|
5582
|
+
// Other archetypes: no border (intentional — not every image needs one)
|
|
5583
|
+
ctx.restore();
|
|
5584
|
+
}
|
|
5585
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
4803
5586
|
{
|
|
4804
5587
|
const sigRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
|
|
4805
5588
|
const sigSize = Math.min(width, height) * 0.025;
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
const
|
|
5589
|
+
const sigMargin = sigSize * 2.5;
|
|
5590
|
+
// Find the corner with the lowest local density
|
|
5591
|
+
const cornerCandidates = [
|
|
5592
|
+
{
|
|
5593
|
+
x: sigMargin,
|
|
5594
|
+
y: sigMargin
|
|
5595
|
+
},
|
|
5596
|
+
{
|
|
5597
|
+
x: width - sigMargin,
|
|
5598
|
+
y: sigMargin
|
|
5599
|
+
},
|
|
5600
|
+
{
|
|
5601
|
+
x: sigMargin,
|
|
5602
|
+
y: height - sigMargin
|
|
5603
|
+
},
|
|
5604
|
+
{
|
|
5605
|
+
x: width - sigMargin,
|
|
5606
|
+
y: height - sigMargin
|
|
5607
|
+
}
|
|
5608
|
+
];
|
|
5609
|
+
let bestCorner = cornerCandidates[3]; // default: bottom-right
|
|
5610
|
+
let minDensity = Infinity;
|
|
5611
|
+
for (const corner of cornerCandidates){
|
|
5612
|
+
const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
|
|
5613
|
+
if (density < minDensity) {
|
|
5614
|
+
minDensity = density;
|
|
5615
|
+
bestCorner = corner;
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
const sigX = bestCorner.x;
|
|
5619
|
+
const sigY = bestCorner.y;
|
|
4809
5620
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
4810
5621
|
const sigColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
4811
5622
|
ctx.save();
|