git-hash-art 0.10.1 → 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 +13 -3
- package/CHANGELOG.md +10 -0
- package/dist/browser.js +360 -56
- package/dist/browser.js.map +1 -1
- package/dist/main.js +362 -56
- package/dist/main.js.map +1 -1
- package/dist/module.js +362 -56
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/archetypes.ts +29 -0
- package/src/lib/canvas/colors.ts +7 -5
- package/src/lib/canvas/draw.ts +14 -4
- package/src/lib/render.ts +249 -60
package/dist/main.js
CHANGED
|
@@ -603,15 +603,17 @@ function $d016ad53434219a1$export$fabac4600b87056(colors, rng) {
|
|
|
603
603
|
accent: colors[colors.length - 1] || "#888888",
|
|
604
604
|
all: colors
|
|
605
605
|
};
|
|
606
|
-
// 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
|
|
607
608
|
const hsls = colors.map((c)=>$d016ad53434219a1$var$hexToHsl(c));
|
|
608
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
609
609
|
let dominantIdx = 0;
|
|
610
|
-
let
|
|
610
|
+
let maxChroma = -1;
|
|
611
611
|
for(let i = 0; i < hsls.length; i++){
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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;
|
|
615
617
|
dominantIdx = i;
|
|
616
618
|
}
|
|
617
619
|
}
|
|
@@ -2508,16 +2510,26 @@ function $c3de8257a8baa3b0$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2508
2510
|
ctx.shadowOffsetX = 0;
|
|
2509
2511
|
ctx.shadowOffsetY = 0;
|
|
2510
2512
|
ctx.shadowColor = "transparent";
|
|
2511
|
-
// ── Specular highlight —
|
|
2513
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2512
2514
|
if (lightAngle !== undefined && size > 15 && rng) {
|
|
2513
2515
|
const hlRadius = size * 0.35;
|
|
2514
2516
|
const hlDist = size * 0.15;
|
|
2515
2517
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2516
2518
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2517
2519
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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)`);
|
|
2521
2533
|
const savedOp = ctx.globalCompositeOperation;
|
|
2522
2534
|
ctx.globalCompositeOperation = "soft-light";
|
|
2523
2535
|
ctx.fillStyle = hlGrad;
|
|
@@ -3579,6 +3591,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3579
3591
|
"watercolor",
|
|
3580
3592
|
"fill-only"
|
|
3581
3593
|
],
|
|
3594
|
+
preferredCompositions: [
|
|
3595
|
+
"clustered",
|
|
3596
|
+
"flow-field",
|
|
3597
|
+
"radial"
|
|
3598
|
+
],
|
|
3582
3599
|
flowLineMultiplier: 2.5,
|
|
3583
3600
|
heroShape: false,
|
|
3584
3601
|
glowMultiplier: 0.5,
|
|
@@ -3600,6 +3617,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3600
3617
|
"stroke-only",
|
|
3601
3618
|
"incomplete"
|
|
3602
3619
|
],
|
|
3620
|
+
preferredCompositions: [
|
|
3621
|
+
"golden-spiral",
|
|
3622
|
+
"grid-subdivision"
|
|
3623
|
+
],
|
|
3603
3624
|
flowLineMultiplier: 0.3,
|
|
3604
3625
|
heroShape: true,
|
|
3605
3626
|
glowMultiplier: 0,
|
|
@@ -3621,6 +3642,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3621
3642
|
"fill-only",
|
|
3622
3643
|
"incomplete"
|
|
3623
3644
|
],
|
|
3645
|
+
preferredCompositions: [
|
|
3646
|
+
"flow-field",
|
|
3647
|
+
"golden-spiral",
|
|
3648
|
+
"spiral"
|
|
3649
|
+
],
|
|
3624
3650
|
flowLineMultiplier: 4,
|
|
3625
3651
|
heroShape: false,
|
|
3626
3652
|
glowMultiplier: 0.3,
|
|
@@ -3643,6 +3669,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3643
3669
|
"double-stroke",
|
|
3644
3670
|
"hatched"
|
|
3645
3671
|
],
|
|
3672
|
+
preferredCompositions: [
|
|
3673
|
+
"grid-subdivision",
|
|
3674
|
+
"radial"
|
|
3675
|
+
],
|
|
3646
3676
|
flowLineMultiplier: 0,
|
|
3647
3677
|
heroShape: false,
|
|
3648
3678
|
glowMultiplier: 0,
|
|
@@ -3664,6 +3694,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3664
3694
|
"incomplete",
|
|
3665
3695
|
"fill-only"
|
|
3666
3696
|
],
|
|
3697
|
+
preferredCompositions: [
|
|
3698
|
+
"golden-spiral",
|
|
3699
|
+
"radial",
|
|
3700
|
+
"spiral"
|
|
3701
|
+
],
|
|
3667
3702
|
flowLineMultiplier: 1.5,
|
|
3668
3703
|
heroShape: true,
|
|
3669
3704
|
glowMultiplier: 2,
|
|
@@ -3684,6 +3719,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3684
3719
|
"fill-and-stroke",
|
|
3685
3720
|
"double-stroke"
|
|
3686
3721
|
],
|
|
3722
|
+
preferredCompositions: [
|
|
3723
|
+
"grid-subdivision",
|
|
3724
|
+
"golden-spiral"
|
|
3725
|
+
],
|
|
3687
3726
|
flowLineMultiplier: 0,
|
|
3688
3727
|
heroShape: true,
|
|
3689
3728
|
glowMultiplier: 0,
|
|
@@ -3705,6 +3744,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3705
3744
|
"double-stroke",
|
|
3706
3745
|
"dashed"
|
|
3707
3746
|
],
|
|
3747
|
+
preferredCompositions: [
|
|
3748
|
+
"radial",
|
|
3749
|
+
"spiral",
|
|
3750
|
+
"clustered"
|
|
3751
|
+
],
|
|
3708
3752
|
flowLineMultiplier: 2,
|
|
3709
3753
|
heroShape: true,
|
|
3710
3754
|
glowMultiplier: 3,
|
|
@@ -3727,6 +3771,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3727
3771
|
"stroke-only",
|
|
3728
3772
|
"dashed"
|
|
3729
3773
|
],
|
|
3774
|
+
preferredCompositions: [
|
|
3775
|
+
"flow-field",
|
|
3776
|
+
"grid-subdivision",
|
|
3777
|
+
"clustered"
|
|
3778
|
+
],
|
|
3730
3779
|
flowLineMultiplier: 1.5,
|
|
3731
3780
|
heroShape: false,
|
|
3732
3781
|
glowMultiplier: 0,
|
|
@@ -3748,6 +3797,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3748
3797
|
"watercolor",
|
|
3749
3798
|
"fill-and-stroke"
|
|
3750
3799
|
],
|
|
3800
|
+
preferredCompositions: [
|
|
3801
|
+
"radial",
|
|
3802
|
+
"spiral",
|
|
3803
|
+
"golden-spiral"
|
|
3804
|
+
],
|
|
3751
3805
|
flowLineMultiplier: 3,
|
|
3752
3806
|
heroShape: true,
|
|
3753
3807
|
glowMultiplier: 2.5,
|
|
@@ -3769,6 +3823,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3769
3823
|
"fill-only",
|
|
3770
3824
|
"incomplete"
|
|
3771
3825
|
],
|
|
3826
|
+
preferredCompositions: [
|
|
3827
|
+
"golden-spiral",
|
|
3828
|
+
"flow-field",
|
|
3829
|
+
"radial"
|
|
3830
|
+
],
|
|
3772
3831
|
flowLineMultiplier: 0.5,
|
|
3773
3832
|
heroShape: false,
|
|
3774
3833
|
glowMultiplier: 0.3,
|
|
@@ -3790,6 +3849,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3790
3849
|
"stroke-only",
|
|
3791
3850
|
"dashed"
|
|
3792
3851
|
],
|
|
3852
|
+
preferredCompositions: [
|
|
3853
|
+
"grid-subdivision",
|
|
3854
|
+
"radial"
|
|
3855
|
+
],
|
|
3793
3856
|
flowLineMultiplier: 0,
|
|
3794
3857
|
heroShape: false,
|
|
3795
3858
|
glowMultiplier: 0,
|
|
@@ -3811,6 +3874,10 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3811
3874
|
"fill-only",
|
|
3812
3875
|
"double-stroke"
|
|
3813
3876
|
],
|
|
3877
|
+
preferredCompositions: [
|
|
3878
|
+
"grid-subdivision",
|
|
3879
|
+
"clustered"
|
|
3880
|
+
],
|
|
3814
3881
|
flowLineMultiplier: 0,
|
|
3815
3882
|
heroShape: true,
|
|
3816
3883
|
glowMultiplier: 0,
|
|
@@ -3832,6 +3899,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3832
3899
|
"watercolor",
|
|
3833
3900
|
"fill-only"
|
|
3834
3901
|
],
|
|
3902
|
+
preferredCompositions: [
|
|
3903
|
+
"radial",
|
|
3904
|
+
"golden-spiral",
|
|
3905
|
+
"flow-field"
|
|
3906
|
+
],
|
|
3835
3907
|
flowLineMultiplier: 1,
|
|
3836
3908
|
heroShape: true,
|
|
3837
3909
|
glowMultiplier: 1,
|
|
@@ -3853,6 +3925,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3853
3925
|
"stroke-only",
|
|
3854
3926
|
"fill-only"
|
|
3855
3927
|
],
|
|
3928
|
+
preferredCompositions: [
|
|
3929
|
+
"clustered",
|
|
3930
|
+
"grid-subdivision",
|
|
3931
|
+
"radial"
|
|
3932
|
+
],
|
|
3856
3933
|
flowLineMultiplier: 0,
|
|
3857
3934
|
heroShape: false,
|
|
3858
3935
|
glowMultiplier: 0.3,
|
|
@@ -3874,6 +3951,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3874
3951
|
"fill-only",
|
|
3875
3952
|
"incomplete"
|
|
3876
3953
|
],
|
|
3954
|
+
preferredCompositions: [
|
|
3955
|
+
"flow-field",
|
|
3956
|
+
"golden-spiral",
|
|
3957
|
+
"spiral"
|
|
3958
|
+
],
|
|
3877
3959
|
flowLineMultiplier: 3,
|
|
3878
3960
|
heroShape: true,
|
|
3879
3961
|
glowMultiplier: 0.2,
|
|
@@ -3895,6 +3977,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3895
3977
|
"fill-only",
|
|
3896
3978
|
"hatched"
|
|
3897
3979
|
],
|
|
3980
|
+
preferredCompositions: [
|
|
3981
|
+
"radial",
|
|
3982
|
+
"clustered",
|
|
3983
|
+
"flow-field"
|
|
3984
|
+
],
|
|
3898
3985
|
flowLineMultiplier: 0,
|
|
3899
3986
|
heroShape: false,
|
|
3900
3987
|
glowMultiplier: 0,
|
|
@@ -3917,6 +4004,11 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3917
4004
|
"stroke-only",
|
|
3918
4005
|
"incomplete"
|
|
3919
4006
|
],
|
|
4007
|
+
preferredCompositions: [
|
|
4008
|
+
"spiral",
|
|
4009
|
+
"radial",
|
|
4010
|
+
"golden-spiral"
|
|
4011
|
+
],
|
|
3920
4012
|
flowLineMultiplier: 2,
|
|
3921
4013
|
heroShape: true,
|
|
3922
4014
|
glowMultiplier: 2.5,
|
|
@@ -3940,6 +4032,12 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3940
4032
|
...b.preferredStyles
|
|
3941
4033
|
])
|
|
3942
4034
|
];
|
|
4035
|
+
const mergedCompositions = [
|
|
4036
|
+
...new Set([
|
|
4037
|
+
...a.preferredCompositions,
|
|
4038
|
+
...b.preferredCompositions
|
|
4039
|
+
])
|
|
4040
|
+
];
|
|
3943
4041
|
return {
|
|
3944
4042
|
name: `${a.name}+${b.name}`,
|
|
3945
4043
|
gridSize: Math.round($f89bc858f7202849$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3951,6 +4049,7 @@ const $f89bc858f7202849$var$ARCHETYPES = [
|
|
|
3951
4049
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3952
4050
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3953
4051
|
preferredStyles: mergedStyles,
|
|
4052
|
+
preferredCompositions: mergedCompositions,
|
|
3954
4053
|
flowLineMultiplier: $f89bc858f7202849$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3955
4054
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3956
4055
|
glowMultiplier: $f89bc858f7202849$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3984,7 +4083,8 @@ const $4f72c5a314eddf25$var$SACRED_SHAPES = [
|
|
|
3984
4083
|
"torus",
|
|
3985
4084
|
"eggOfLife"
|
|
3986
4085
|
];
|
|
3987
|
-
|
|
4086
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4087
|
+
const $4f72c5a314eddf25$var$ALL_COMPOSITION_MODES = [
|
|
3988
4088
|
"radial",
|
|
3989
4089
|
"flow-field",
|
|
3990
4090
|
"spiral",
|
|
@@ -4086,7 +4186,69 @@ function $4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones) {
|
|
|
4086
4186
|
}
|
|
4087
4187
|
return false;
|
|
4088
4188
|
}
|
|
4089
|
-
// ──
|
|
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) ──────────────────────────
|
|
4090
4252
|
function $4f72c5a314eddf25$var$localDensity(x, y, positions, radius) {
|
|
4091
4253
|
let count = 0;
|
|
4092
4254
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4385,42 +4547,43 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4385
4547
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4386
4548
|
const patternColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4387
4549
|
if (bgPatternRoll < 0.2) {
|
|
4388
|
-
// Dot grid
|
|
4550
|
+
// Dot grid — batched into a single path
|
|
4389
4551
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4390
4552
|
const dotR = dotSpacing * 0.08;
|
|
4391
4553
|
ctx.globalAlpha = patternOpacity;
|
|
4392
4554
|
ctx.fillStyle = patternColor;
|
|
4555
|
+
ctx.beginPath();
|
|
4393
4556
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4394
|
-
ctx.
|
|
4557
|
+
ctx.moveTo(px + dotR, py);
|
|
4395
4558
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4396
|
-
ctx.fill();
|
|
4397
4559
|
}
|
|
4560
|
+
ctx.fill();
|
|
4398
4561
|
} else if (bgPatternRoll < 0.4) {
|
|
4399
|
-
// Diagonal lines
|
|
4562
|
+
// Diagonal lines — batched into a single path
|
|
4400
4563
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4401
4564
|
ctx.globalAlpha = patternOpacity;
|
|
4402
4565
|
ctx.strokeStyle = patternColor;
|
|
4403
4566
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4404
4567
|
const diag = Math.hypot(width, height);
|
|
4568
|
+
ctx.beginPath();
|
|
4405
4569
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4406
|
-
ctx.beginPath();
|
|
4407
4570
|
ctx.moveTo(d, 0);
|
|
4408
4571
|
ctx.lineTo(d + height, height);
|
|
4409
|
-
ctx.stroke();
|
|
4410
4572
|
}
|
|
4573
|
+
ctx.stroke();
|
|
4411
4574
|
} else {
|
|
4412
|
-
// Tessellation — hexagonal grid
|
|
4575
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4413
4576
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4414
4577
|
const tessH = tessSize * Math.sqrt(3);
|
|
4415
4578
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4416
4579
|
ctx.strokeStyle = patternColor;
|
|
4417
4580
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4581
|
+
ctx.beginPath();
|
|
4418
4582
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4419
4583
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4420
4584
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4421
4585
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4422
4586
|
const hy = row * tessH;
|
|
4423
|
-
ctx.beginPath();
|
|
4424
4587
|
for(let s = 0; s < 6; s++){
|
|
4425
4588
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4426
4589
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4429,18 +4592,18 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4429
4592
|
else ctx.lineTo(vx, vy);
|
|
4430
4593
|
}
|
|
4431
4594
|
ctx.closePath();
|
|
4432
|
-
ctx.stroke();
|
|
4433
4595
|
}
|
|
4434
4596
|
}
|
|
4597
|
+
ctx.stroke();
|
|
4435
4598
|
}
|
|
4436
4599
|
ctx.restore();
|
|
4437
4600
|
}
|
|
4438
4601
|
ctx.globalCompositeOperation = "source-over";
|
|
4439
|
-
// ── 2. Composition mode
|
|
4440
|
-
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)];
|
|
4441
4604
|
const symRoll = rng();
|
|
4442
4605
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4443
|
-
// ── 3. Focal points + void zones
|
|
4606
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4444
4607
|
const THIRDS_POINTS = [
|
|
4445
4608
|
{
|
|
4446
4609
|
x: 1 / 3,
|
|
@@ -4473,9 +4636,23 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4473
4636
|
y: height * (0.2 + rng() * 0.6),
|
|
4474
4637
|
strength: 0.3 + rng() * 0.4
|
|
4475
4638
|
});
|
|
4476
|
-
|
|
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;
|
|
4477
4645
|
const voidZones = [];
|
|
4478
|
-
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({
|
|
4479
4656
|
x: width * (0.15 + rng() * 0.7),
|
|
4480
4657
|
y: height * (0.15 + rng() * 0.7),
|
|
4481
4658
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4552,6 +4729,9 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4552
4729
|
}
|
|
4553
4730
|
// Track all placed shapes for density checks and connecting curves
|
|
4554
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);
|
|
4555
4735
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4556
4736
|
let heroCenter = null;
|
|
4557
4737
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4597,9 +4777,14 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4597
4777
|
size: heroSize,
|
|
4598
4778
|
shape: heroShape
|
|
4599
4779
|
});
|
|
4780
|
+
spatialGrid.insert({
|
|
4781
|
+
x: heroFocal.x,
|
|
4782
|
+
y: heroFocal.y,
|
|
4783
|
+
size: heroSize,
|
|
4784
|
+
shape: heroShape
|
|
4785
|
+
});
|
|
4600
4786
|
}
|
|
4601
4787
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4602
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4603
4788
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4604
4789
|
for(let layer = 0; layer < layers; layer++){
|
|
4605
4790
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
@@ -4638,7 +4823,7 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4638
4823
|
if ($4f72c5a314eddf25$var$isInVoidZone(x, y, voidZones)) {
|
|
4639
4824
|
if (rng() < 0.85) continue;
|
|
4640
4825
|
}
|
|
4641
|
-
if (
|
|
4826
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4642
4827
|
if (rng() < 0.6) continue;
|
|
4643
4828
|
}
|
|
4644
4829
|
// Power distribution for size — archetype controls the curve
|
|
@@ -4698,17 +4883,11 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4698
4883
|
let finalX = x;
|
|
4699
4884
|
let finalY = y;
|
|
4700
4885
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4701
|
-
//
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
for (const sp of shapePositions){
|
|
4705
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4706
|
-
if (d < nearestDist && d > 0) {
|
|
4707
|
-
nearestDist = d;
|
|
4708
|
-
nearestPos = sp;
|
|
4709
|
-
}
|
|
4710
|
-
}
|
|
4886
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
4887
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
4888
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4711
4889
|
if (nearestPos) {
|
|
4890
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4712
4891
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4713
4892
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4714
4893
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4781,6 +4960,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4781
4960
|
size: size,
|
|
4782
4961
|
shape: shape
|
|
4783
4962
|
});
|
|
4963
|
+
spatialGrid.insert({
|
|
4964
|
+
x: finalX,
|
|
4965
|
+
y: finalY,
|
|
4966
|
+
size: size,
|
|
4967
|
+
shape: shape
|
|
4968
|
+
});
|
|
4784
4969
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4785
4970
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4786
4971
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
@@ -4809,6 +4994,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4809
4994
|
size: echoSize,
|
|
4810
4995
|
shape: shape
|
|
4811
4996
|
});
|
|
4997
|
+
spatialGrid.insert({
|
|
4998
|
+
x: echoX,
|
|
4999
|
+
y: echoY,
|
|
5000
|
+
size: echoSize,
|
|
5001
|
+
shape: shape
|
|
5002
|
+
});
|
|
4812
5003
|
}
|
|
4813
5004
|
}
|
|
4814
5005
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
@@ -4873,13 +5064,62 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4873
5064
|
size: member.size,
|
|
4874
5065
|
shape: memberShape
|
|
4875
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
|
+
});
|
|
4876
5116
|
}
|
|
4877
5117
|
}
|
|
4878
5118
|
}
|
|
4879
5119
|
}
|
|
4880
5120
|
// Reset blend mode for post-processing passes
|
|
4881
5121
|
ctx.globalCompositeOperation = "source-over";
|
|
4882
|
-
// ──
|
|
5122
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
4883
5123
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
4884
5124
|
// with a tinted background wash, creating a "peek through" effect.
|
|
4885
5125
|
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
@@ -4960,6 +5200,12 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4960
5200
|
fx += Math.cos(angle) * stepLen;
|
|
4961
5201
|
fy += Math.sin(angle) * stepLen;
|
|
4962
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
|
+
}
|
|
4963
5209
|
const t = s / steps;
|
|
4964
5210
|
// Taper + pressure
|
|
4965
5211
|
const taper = 1 - t * 0.8;
|
|
@@ -5057,30 +5303,60 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5057
5303
|
}
|
|
5058
5304
|
ctx.restore();
|
|
5059
5305
|
}
|
|
5060
|
-
// ── 7. Noise texture overlay
|
|
5306
|
+
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
5061
5307
|
const noiseRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 777));
|
|
5062
5308
|
const noiseDensity = Math.floor(width * height / 800);
|
|
5063
|
-
|
|
5064
|
-
const
|
|
5065
|
-
const
|
|
5066
|
-
const
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
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
|
+
}
|
|
5071
5342
|
}
|
|
5072
5343
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
5073
5344
|
ctx.globalAlpha = 1;
|
|
5074
5345
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
5075
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
|
|
5076
5351
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
5077
5352
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
5078
|
-
vigGrad.addColorStop(1,
|
|
5353
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
5079
5354
|
ctx.fillStyle = vigGrad;
|
|
5080
5355
|
ctx.fillRect(0, 0, width, height);
|
|
5081
|
-
// ── 9. Organic connecting curves
|
|
5356
|
+
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5082
5357
|
if (shapePositions.length > 1) {
|
|
5083
5358
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5359
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5084
5360
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5085
5361
|
for(let i = 0; i < numCurves; i++){
|
|
5086
5362
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
@@ -5088,11 +5364,13 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5088
5364
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
5089
5365
|
const a = shapePositions[idxA];
|
|
5090
5366
|
const b = shapePositions[idxB];
|
|
5091
|
-
const mx = (a.x + b.x) / 2;
|
|
5092
|
-
const my = (a.y + b.y) / 2;
|
|
5093
5367
|
const dx = b.x - a.x;
|
|
5094
5368
|
const dy = b.y - a.y;
|
|
5095
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;
|
|
5096
5374
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5097
5375
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5098
5376
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
@@ -5318,13 +5596,41 @@ function $4f72c5a314eddf25$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5318
5596
|
// Other archetypes: no border (intentional — not every image needs one)
|
|
5319
5597
|
ctx.restore();
|
|
5320
5598
|
}
|
|
5321
|
-
// ── 11. Signature mark —
|
|
5599
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
5322
5600
|
{
|
|
5323
5601
|
const sigRng = (0, $e4b03e131ed2a289$export$eaf9227667332084)((0, $e4b03e131ed2a289$export$e9cc707de01b7042)(gitHash, 42));
|
|
5324
5602
|
const sigSize = Math.min(width, height) * 0.025;
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
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;
|
|
5328
5634
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
5329
5635
|
const sigColor = (0, $d016ad53434219a1$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
5330
5636
|
ctx.save();
|