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/browser.js
CHANGED
|
@@ -580,15 +580,17 @@ function $b5a262d09b87e373$export$fabac4600b87056(colors, rng) {
|
|
|
580
580
|
accent: colors[colors.length - 1] || "#888888",
|
|
581
581
|
all: colors
|
|
582
582
|
};
|
|
583
|
-
// Pick dominant as the color
|
|
583
|
+
// Pick dominant as the color with the highest chroma (saturation × distance from gray)
|
|
584
|
+
// This selects the most visually prominent color rather than the average
|
|
584
585
|
const hsls = colors.map((c)=>$b5a262d09b87e373$var$hexToHsl(c));
|
|
585
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
586
586
|
let dominantIdx = 0;
|
|
587
|
-
let
|
|
587
|
+
let maxChroma = -1;
|
|
588
588
|
for(let i = 0; i < hsls.length; i++){
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
589
|
+
// Chroma approximation: saturation × how far lightness is from 50% (gray)
|
|
590
|
+
const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
|
|
591
|
+
const chroma = hsls[i][1] * lightnessVibrancy;
|
|
592
|
+
if (chroma > maxChroma) {
|
|
593
|
+
maxChroma = chroma;
|
|
592
594
|
dominantIdx = i;
|
|
593
595
|
}
|
|
594
596
|
}
|
|
@@ -2485,16 +2487,26 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2485
2487
|
ctx.shadowOffsetX = 0;
|
|
2486
2488
|
ctx.shadowOffsetY = 0;
|
|
2487
2489
|
ctx.shadowColor = "transparent";
|
|
2488
|
-
// ── Specular highlight —
|
|
2490
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2489
2491
|
if (lightAngle !== undefined && size > 15 && rng) {
|
|
2490
2492
|
const hlRadius = size * 0.35;
|
|
2491
2493
|
const hlDist = size * 0.15;
|
|
2492
2494
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2493
2495
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2494
2496
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2497
|
+
// Tint highlight warm/cool based on fill color for cohesion
|
|
2498
|
+
// Parse fill to detect warmth — fallback to white for non-parseable
|
|
2499
|
+
let hlBase = "255,255,255";
|
|
2500
|
+
if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
|
|
2501
|
+
const r = parseInt(fillColor.slice(1, 3), 16);
|
|
2502
|
+
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
2503
|
+
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
2504
|
+
// Blend toward white but keep a hint of the fill's warmth
|
|
2505
|
+
hlBase = `${Math.round(r * 0.15 + 216.75)},${Math.round(g * 0.15 + 216.75)},${Math.round(b * 0.15 + 216.75)}`;
|
|
2506
|
+
}
|
|
2507
|
+
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
2508
|
+
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
2509
|
+
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
2498
2510
|
const savedOp = ctx.globalCompositeOperation;
|
|
2499
2511
|
ctx.globalCompositeOperation = "soft-light";
|
|
2500
2512
|
ctx.fillStyle = hlGrad;
|
|
@@ -3556,6 +3568,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3556
3568
|
"watercolor",
|
|
3557
3569
|
"fill-only"
|
|
3558
3570
|
],
|
|
3571
|
+
preferredCompositions: [
|
|
3572
|
+
"clustered",
|
|
3573
|
+
"flow-field",
|
|
3574
|
+
"radial"
|
|
3575
|
+
],
|
|
3559
3576
|
flowLineMultiplier: 2.5,
|
|
3560
3577
|
heroShape: false,
|
|
3561
3578
|
glowMultiplier: 0.5,
|
|
@@ -3577,6 +3594,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3577
3594
|
"stroke-only",
|
|
3578
3595
|
"incomplete"
|
|
3579
3596
|
],
|
|
3597
|
+
preferredCompositions: [
|
|
3598
|
+
"golden-spiral",
|
|
3599
|
+
"grid-subdivision"
|
|
3600
|
+
],
|
|
3580
3601
|
flowLineMultiplier: 0.3,
|
|
3581
3602
|
heroShape: true,
|
|
3582
3603
|
glowMultiplier: 0,
|
|
@@ -3598,6 +3619,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3598
3619
|
"fill-only",
|
|
3599
3620
|
"incomplete"
|
|
3600
3621
|
],
|
|
3622
|
+
preferredCompositions: [
|
|
3623
|
+
"flow-field",
|
|
3624
|
+
"golden-spiral",
|
|
3625
|
+
"spiral"
|
|
3626
|
+
],
|
|
3601
3627
|
flowLineMultiplier: 4,
|
|
3602
3628
|
heroShape: false,
|
|
3603
3629
|
glowMultiplier: 0.3,
|
|
@@ -3620,6 +3646,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3620
3646
|
"double-stroke",
|
|
3621
3647
|
"hatched"
|
|
3622
3648
|
],
|
|
3649
|
+
preferredCompositions: [
|
|
3650
|
+
"grid-subdivision",
|
|
3651
|
+
"radial"
|
|
3652
|
+
],
|
|
3623
3653
|
flowLineMultiplier: 0,
|
|
3624
3654
|
heroShape: false,
|
|
3625
3655
|
glowMultiplier: 0,
|
|
@@ -3641,6 +3671,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3641
3671
|
"incomplete",
|
|
3642
3672
|
"fill-only"
|
|
3643
3673
|
],
|
|
3674
|
+
preferredCompositions: [
|
|
3675
|
+
"golden-spiral",
|
|
3676
|
+
"radial",
|
|
3677
|
+
"spiral"
|
|
3678
|
+
],
|
|
3644
3679
|
flowLineMultiplier: 1.5,
|
|
3645
3680
|
heroShape: true,
|
|
3646
3681
|
glowMultiplier: 2,
|
|
@@ -3661,6 +3696,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3661
3696
|
"fill-and-stroke",
|
|
3662
3697
|
"double-stroke"
|
|
3663
3698
|
],
|
|
3699
|
+
preferredCompositions: [
|
|
3700
|
+
"grid-subdivision",
|
|
3701
|
+
"golden-spiral"
|
|
3702
|
+
],
|
|
3664
3703
|
flowLineMultiplier: 0,
|
|
3665
3704
|
heroShape: true,
|
|
3666
3705
|
glowMultiplier: 0,
|
|
@@ -3682,6 +3721,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3682
3721
|
"double-stroke",
|
|
3683
3722
|
"dashed"
|
|
3684
3723
|
],
|
|
3724
|
+
preferredCompositions: [
|
|
3725
|
+
"radial",
|
|
3726
|
+
"spiral",
|
|
3727
|
+
"clustered"
|
|
3728
|
+
],
|
|
3685
3729
|
flowLineMultiplier: 2,
|
|
3686
3730
|
heroShape: true,
|
|
3687
3731
|
glowMultiplier: 3,
|
|
@@ -3704,6 +3748,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3704
3748
|
"stroke-only",
|
|
3705
3749
|
"dashed"
|
|
3706
3750
|
],
|
|
3751
|
+
preferredCompositions: [
|
|
3752
|
+
"flow-field",
|
|
3753
|
+
"grid-subdivision",
|
|
3754
|
+
"clustered"
|
|
3755
|
+
],
|
|
3707
3756
|
flowLineMultiplier: 1.5,
|
|
3708
3757
|
heroShape: false,
|
|
3709
3758
|
glowMultiplier: 0,
|
|
@@ -3725,6 +3774,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3725
3774
|
"watercolor",
|
|
3726
3775
|
"fill-and-stroke"
|
|
3727
3776
|
],
|
|
3777
|
+
preferredCompositions: [
|
|
3778
|
+
"radial",
|
|
3779
|
+
"spiral",
|
|
3780
|
+
"golden-spiral"
|
|
3781
|
+
],
|
|
3728
3782
|
flowLineMultiplier: 3,
|
|
3729
3783
|
heroShape: true,
|
|
3730
3784
|
glowMultiplier: 2.5,
|
|
@@ -3746,6 +3800,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3746
3800
|
"fill-only",
|
|
3747
3801
|
"incomplete"
|
|
3748
3802
|
],
|
|
3803
|
+
preferredCompositions: [
|
|
3804
|
+
"golden-spiral",
|
|
3805
|
+
"flow-field",
|
|
3806
|
+
"radial"
|
|
3807
|
+
],
|
|
3749
3808
|
flowLineMultiplier: 0.5,
|
|
3750
3809
|
heroShape: false,
|
|
3751
3810
|
glowMultiplier: 0.3,
|
|
@@ -3767,6 +3826,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3767
3826
|
"stroke-only",
|
|
3768
3827
|
"dashed"
|
|
3769
3828
|
],
|
|
3829
|
+
preferredCompositions: [
|
|
3830
|
+
"grid-subdivision",
|
|
3831
|
+
"radial"
|
|
3832
|
+
],
|
|
3770
3833
|
flowLineMultiplier: 0,
|
|
3771
3834
|
heroShape: false,
|
|
3772
3835
|
glowMultiplier: 0,
|
|
@@ -3788,6 +3851,10 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3788
3851
|
"fill-only",
|
|
3789
3852
|
"double-stroke"
|
|
3790
3853
|
],
|
|
3854
|
+
preferredCompositions: [
|
|
3855
|
+
"grid-subdivision",
|
|
3856
|
+
"clustered"
|
|
3857
|
+
],
|
|
3791
3858
|
flowLineMultiplier: 0,
|
|
3792
3859
|
heroShape: true,
|
|
3793
3860
|
glowMultiplier: 0,
|
|
@@ -3809,6 +3876,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3809
3876
|
"watercolor",
|
|
3810
3877
|
"fill-only"
|
|
3811
3878
|
],
|
|
3879
|
+
preferredCompositions: [
|
|
3880
|
+
"radial",
|
|
3881
|
+
"golden-spiral",
|
|
3882
|
+
"flow-field"
|
|
3883
|
+
],
|
|
3812
3884
|
flowLineMultiplier: 1,
|
|
3813
3885
|
heroShape: true,
|
|
3814
3886
|
glowMultiplier: 1,
|
|
@@ -3830,6 +3902,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3830
3902
|
"stroke-only",
|
|
3831
3903
|
"fill-only"
|
|
3832
3904
|
],
|
|
3905
|
+
preferredCompositions: [
|
|
3906
|
+
"clustered",
|
|
3907
|
+
"grid-subdivision",
|
|
3908
|
+
"radial"
|
|
3909
|
+
],
|
|
3833
3910
|
flowLineMultiplier: 0,
|
|
3834
3911
|
heroShape: false,
|
|
3835
3912
|
glowMultiplier: 0.3,
|
|
@@ -3851,6 +3928,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3851
3928
|
"fill-only",
|
|
3852
3929
|
"incomplete"
|
|
3853
3930
|
],
|
|
3931
|
+
preferredCompositions: [
|
|
3932
|
+
"flow-field",
|
|
3933
|
+
"golden-spiral",
|
|
3934
|
+
"spiral"
|
|
3935
|
+
],
|
|
3854
3936
|
flowLineMultiplier: 3,
|
|
3855
3937
|
heroShape: true,
|
|
3856
3938
|
glowMultiplier: 0.2,
|
|
@@ -3872,6 +3954,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3872
3954
|
"fill-only",
|
|
3873
3955
|
"hatched"
|
|
3874
3956
|
],
|
|
3957
|
+
preferredCompositions: [
|
|
3958
|
+
"radial",
|
|
3959
|
+
"clustered",
|
|
3960
|
+
"flow-field"
|
|
3961
|
+
],
|
|
3875
3962
|
flowLineMultiplier: 0,
|
|
3876
3963
|
heroShape: false,
|
|
3877
3964
|
glowMultiplier: 0,
|
|
@@ -3894,6 +3981,11 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3894
3981
|
"stroke-only",
|
|
3895
3982
|
"incomplete"
|
|
3896
3983
|
],
|
|
3984
|
+
preferredCompositions: [
|
|
3985
|
+
"spiral",
|
|
3986
|
+
"radial",
|
|
3987
|
+
"golden-spiral"
|
|
3988
|
+
],
|
|
3897
3989
|
flowLineMultiplier: 2,
|
|
3898
3990
|
heroShape: true,
|
|
3899
3991
|
glowMultiplier: 2.5,
|
|
@@ -3917,6 +4009,12 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3917
4009
|
...b.preferredStyles
|
|
3918
4010
|
])
|
|
3919
4011
|
];
|
|
4012
|
+
const mergedCompositions = [
|
|
4013
|
+
...new Set([
|
|
4014
|
+
...a.preferredCompositions,
|
|
4015
|
+
...b.preferredCompositions
|
|
4016
|
+
])
|
|
4017
|
+
];
|
|
3920
4018
|
return {
|
|
3921
4019
|
name: `${a.name}+${b.name}`,
|
|
3922
4020
|
gridSize: Math.round($68a238ccd77f2bcd$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3928,6 +4026,7 @@ const $68a238ccd77f2bcd$var$ARCHETYPES = [
|
|
|
3928
4026
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3929
4027
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3930
4028
|
preferredStyles: mergedStyles,
|
|
4029
|
+
preferredCompositions: mergedCompositions,
|
|
3931
4030
|
flowLineMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3932
4031
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3933
4032
|
glowMultiplier: $68a238ccd77f2bcd$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3961,7 +4060,8 @@ const $1f63dc64b5593c73$var$SACRED_SHAPES = [
|
|
|
3961
4060
|
"torus",
|
|
3962
4061
|
"eggOfLife"
|
|
3963
4062
|
];
|
|
3964
|
-
|
|
4063
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4064
|
+
const $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES = [
|
|
3965
4065
|
"radial",
|
|
3966
4066
|
"flow-field",
|
|
3967
4067
|
"spiral",
|
|
@@ -4063,7 +4163,67 @@ function $1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones) {
|
|
|
4063
4163
|
}
|
|
4064
4164
|
return false;
|
|
4065
4165
|
}
|
|
4066
|
-
// ──
|
|
4166
|
+
// ── Spatial hash grid for O(1) density checks and nearest-neighbor ──
|
|
4167
|
+
class $1f63dc64b5593c73$var$SpatialGrid {
|
|
4168
|
+
constructor(cellSize){
|
|
4169
|
+
this.cells = new Map();
|
|
4170
|
+
this.cellSize = cellSize;
|
|
4171
|
+
}
|
|
4172
|
+
key(cx, cy) {
|
|
4173
|
+
return `${cx},${cy}`;
|
|
4174
|
+
}
|
|
4175
|
+
insert(item) {
|
|
4176
|
+
const cx = Math.floor(item.x / this.cellSize);
|
|
4177
|
+
const cy = Math.floor(item.y / this.cellSize);
|
|
4178
|
+
const k = this.key(cx, cy);
|
|
4179
|
+
const cell = this.cells.get(k);
|
|
4180
|
+
if (cell) cell.push(item);
|
|
4181
|
+
else this.cells.set(k, [
|
|
4182
|
+
item
|
|
4183
|
+
]);
|
|
4184
|
+
}
|
|
4185
|
+
/** Count items within radius of (x, y) */ countNear(x, y, radius) {
|
|
4186
|
+
const r2 = radius * radius;
|
|
4187
|
+
const minCx = Math.floor((x - radius) / this.cellSize);
|
|
4188
|
+
const maxCx = Math.floor((x + radius) / this.cellSize);
|
|
4189
|
+
const minCy = Math.floor((y - radius) / this.cellSize);
|
|
4190
|
+
const maxCy = Math.floor((y + radius) / this.cellSize);
|
|
4191
|
+
let count = 0;
|
|
4192
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4193
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4194
|
+
if (!cell) continue;
|
|
4195
|
+
for (const p of cell){
|
|
4196
|
+
const dx = x - p.x;
|
|
4197
|
+
const dy = y - p.y;
|
|
4198
|
+
if (dx * dx + dy * dy < r2) count++;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
return count;
|
|
4202
|
+
}
|
|
4203
|
+
/** Find nearest item to (x, y) */ findNearest(x, y, searchRadius) {
|
|
4204
|
+
const minCx = Math.floor((x - searchRadius) / this.cellSize);
|
|
4205
|
+
const maxCx = Math.floor((x + searchRadius) / this.cellSize);
|
|
4206
|
+
const minCy = Math.floor((y - searchRadius) / this.cellSize);
|
|
4207
|
+
const maxCy = Math.floor((y + searchRadius) / this.cellSize);
|
|
4208
|
+
let nearest = null;
|
|
4209
|
+
let bestDist2 = Infinity;
|
|
4210
|
+
for(let cx = minCx; cx <= maxCx; cx++)for(let cy = minCy; cy <= maxCy; cy++){
|
|
4211
|
+
const cell = this.cells.get(this.key(cx, cy));
|
|
4212
|
+
if (!cell) continue;
|
|
4213
|
+
for (const p of cell){
|
|
4214
|
+
const dx = x - p.x;
|
|
4215
|
+
const dy = y - p.y;
|
|
4216
|
+
const d2 = dx * dx + dy * dy;
|
|
4217
|
+
if (d2 > 0 && d2 < bestDist2) {
|
|
4218
|
+
bestDist2 = d2;
|
|
4219
|
+
nearest = p;
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
return nearest;
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
// ── Helper: density check (legacy wrapper) ──────────────────────────
|
|
4067
4227
|
function $1f63dc64b5593c73$var$localDensity(x, y, positions, radius) {
|
|
4068
4228
|
let count = 0;
|
|
4069
4229
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4362,42 +4522,43 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4362
4522
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4363
4523
|
const patternColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4364
4524
|
if (bgPatternRoll < 0.2) {
|
|
4365
|
-
// Dot grid
|
|
4525
|
+
// Dot grid — batched into a single path
|
|
4366
4526
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4367
4527
|
const dotR = dotSpacing * 0.08;
|
|
4368
4528
|
ctx.globalAlpha = patternOpacity;
|
|
4369
4529
|
ctx.fillStyle = patternColor;
|
|
4530
|
+
ctx.beginPath();
|
|
4370
4531
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4371
|
-
ctx.
|
|
4532
|
+
ctx.moveTo(px + dotR, py);
|
|
4372
4533
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4373
|
-
ctx.fill();
|
|
4374
4534
|
}
|
|
4535
|
+
ctx.fill();
|
|
4375
4536
|
} else if (bgPatternRoll < 0.4) {
|
|
4376
|
-
// Diagonal lines
|
|
4537
|
+
// Diagonal lines — batched into a single path
|
|
4377
4538
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4378
4539
|
ctx.globalAlpha = patternOpacity;
|
|
4379
4540
|
ctx.strokeStyle = patternColor;
|
|
4380
4541
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4381
4542
|
const diag = Math.hypot(width, height);
|
|
4543
|
+
ctx.beginPath();
|
|
4382
4544
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4383
|
-
ctx.beginPath();
|
|
4384
4545
|
ctx.moveTo(d, 0);
|
|
4385
4546
|
ctx.lineTo(d + height, height);
|
|
4386
|
-
ctx.stroke();
|
|
4387
4547
|
}
|
|
4548
|
+
ctx.stroke();
|
|
4388
4549
|
} else {
|
|
4389
|
-
// Tessellation — hexagonal grid
|
|
4550
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4390
4551
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4391
4552
|
const tessH = tessSize * Math.sqrt(3);
|
|
4392
4553
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4393
4554
|
ctx.strokeStyle = patternColor;
|
|
4394
4555
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4556
|
+
ctx.beginPath();
|
|
4395
4557
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4396
4558
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4397
4559
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4398
4560
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4399
4561
|
const hy = row * tessH;
|
|
4400
|
-
ctx.beginPath();
|
|
4401
4562
|
for(let s = 0; s < 6; s++){
|
|
4402
4563
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4403
4564
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4406,18 +4567,18 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4406
4567
|
else ctx.lineTo(vx, vy);
|
|
4407
4568
|
}
|
|
4408
4569
|
ctx.closePath();
|
|
4409
|
-
ctx.stroke();
|
|
4410
4570
|
}
|
|
4411
4571
|
}
|
|
4572
|
+
ctx.stroke();
|
|
4412
4573
|
}
|
|
4413
4574
|
ctx.restore();
|
|
4414
4575
|
}
|
|
4415
4576
|
ctx.globalCompositeOperation = "source-over";
|
|
4416
|
-
// ── 2. Composition mode
|
|
4417
|
-
const compositionMode = $1f63dc64b5593c73$var$
|
|
4577
|
+
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
4578
|
+
const compositionMode = rng() < 0.7 ? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)] : $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$ALL_COMPOSITION_MODES.length)];
|
|
4418
4579
|
const symRoll = rng();
|
|
4419
4580
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4420
|
-
// ── 3. Focal points + void zones
|
|
4581
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4421
4582
|
const THIRDS_POINTS = [
|
|
4422
4583
|
{
|
|
4423
4584
|
x: 1 / 3,
|
|
@@ -4450,9 +4611,23 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4450
4611
|
y: height * (0.2 + rng() * 0.6),
|
|
4451
4612
|
strength: 0.3 + rng() * 0.4
|
|
4452
4613
|
});
|
|
4453
|
-
|
|
4614
|
+
// Archetype-aware void zones: dense archetypes get fewer/no voids,
|
|
4615
|
+
// minimal archetypes get golden-ratio positioned voids
|
|
4616
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
4617
|
+
const isMinimalArchetype = archetype.gridSize <= 3;
|
|
4618
|
+
const isDenseArchetype = archetype.gridSize >= 8;
|
|
4619
|
+
const numVoids = isDenseArchetype ? 0 : Math.floor(rng() * 2) + 1;
|
|
4454
4620
|
const voidZones = [];
|
|
4455
|
-
for(let v = 0; v < numVoids; v++)
|
|
4621
|
+
for(let v = 0; v < numVoids; v++)if (isMinimalArchetype) {
|
|
4622
|
+
// Place voids at golden-ratio positions for intentional negative space
|
|
4623
|
+
const gx = v === 0 ? 1 / PHI : 1 - 1 / PHI;
|
|
4624
|
+
const gy = v === 0 ? 1 - 1 / PHI : 1 / PHI;
|
|
4625
|
+
voidZones.push({
|
|
4626
|
+
x: width * (gx + (rng() - 0.5) * 0.05),
|
|
4627
|
+
y: height * (gy + (rng() - 0.5) * 0.05),
|
|
4628
|
+
radius: Math.min(width, height) * (0.08 + rng() * 0.08)
|
|
4629
|
+
});
|
|
4630
|
+
} else voidZones.push({
|
|
4456
4631
|
x: width * (0.15 + rng() * 0.7),
|
|
4457
4632
|
y: height * (0.15 + rng() * 0.7),
|
|
4458
4633
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4529,6 +4704,9 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4529
4704
|
}
|
|
4530
4705
|
// Track all placed shapes for density checks and connecting curves
|
|
4531
4706
|
const shapePositions = [];
|
|
4707
|
+
// Spatial grid for O(1) density and nearest-neighbor lookups
|
|
4708
|
+
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4709
|
+
const spatialGrid = new $1f63dc64b5593c73$var$SpatialGrid(densityCheckRadius);
|
|
4532
4710
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4533
4711
|
let heroCenter = null;
|
|
4534
4712
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4574,9 +4752,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4574
4752
|
size: heroSize,
|
|
4575
4753
|
shape: heroShape
|
|
4576
4754
|
});
|
|
4755
|
+
spatialGrid.insert({
|
|
4756
|
+
x: heroFocal.x,
|
|
4757
|
+
y: heroFocal.y,
|
|
4758
|
+
size: heroSize,
|
|
4759
|
+
shape: heroShape
|
|
4760
|
+
});
|
|
4577
4761
|
}
|
|
4578
4762
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4579
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4580
4763
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4581
4764
|
for(let layer = 0; layer < layers; layer++){
|
|
4582
4765
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
@@ -4615,7 +4798,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4615
4798
|
if ($1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones)) {
|
|
4616
4799
|
if (rng() < 0.85) continue;
|
|
4617
4800
|
}
|
|
4618
|
-
if (
|
|
4801
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4619
4802
|
if (rng() < 0.6) continue;
|
|
4620
4803
|
}
|
|
4621
4804
|
// Power distribution for size — archetype controls the curve
|
|
@@ -4675,17 +4858,11 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4675
4858
|
let finalX = x;
|
|
4676
4859
|
let finalY = y;
|
|
4677
4860
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4678
|
-
//
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
for (const sp of shapePositions){
|
|
4682
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4683
|
-
if (d < nearestDist && d > 0) {
|
|
4684
|
-
nearestDist = d;
|
|
4685
|
-
nearestPos = sp;
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
4861
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
4862
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
4863
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4688
4864
|
if (nearestPos) {
|
|
4865
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4689
4866
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4690
4867
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4691
4868
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4758,6 +4935,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4758
4935
|
size: size,
|
|
4759
4936
|
shape: shape
|
|
4760
4937
|
});
|
|
4938
|
+
spatialGrid.insert({
|
|
4939
|
+
x: finalX,
|
|
4940
|
+
y: finalY,
|
|
4941
|
+
size: size,
|
|
4942
|
+
shape: shape
|
|
4943
|
+
});
|
|
4761
4944
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4762
4945
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4763
4946
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
@@ -4786,6 +4969,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4786
4969
|
size: echoSize,
|
|
4787
4970
|
shape: shape
|
|
4788
4971
|
});
|
|
4972
|
+
spatialGrid.insert({
|
|
4973
|
+
x: echoX,
|
|
4974
|
+
y: echoY,
|
|
4975
|
+
size: echoSize,
|
|
4976
|
+
shape: shape
|
|
4977
|
+
});
|
|
4789
4978
|
}
|
|
4790
4979
|
}
|
|
4791
4980
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
@@ -4850,13 +5039,62 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4850
5039
|
size: member.size,
|
|
4851
5040
|
shape: memberShape
|
|
4852
5041
|
});
|
|
5042
|
+
spatialGrid.insert({
|
|
5043
|
+
x: mx,
|
|
5044
|
+
y: my,
|
|
5045
|
+
size: member.size,
|
|
5046
|
+
shape: memberShape
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
// ── 5f. Rhythm placement — deliberate geometric progressions ──
|
|
5051
|
+
// ~12% of medium-large shapes spawn a rhythmic sequence
|
|
5052
|
+
if (size > adjustedMaxSize * 0.25 && rng() < 0.12) {
|
|
5053
|
+
const rhythmCount = 3 + Math.floor(rng() * 4); // 3-6 shapes
|
|
5054
|
+
const rhythmAngle = rng() * Math.PI * 2;
|
|
5055
|
+
const rhythmSpacing = size * (0.8 + rng() * 0.6);
|
|
5056
|
+
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
5057
|
+
const rhythmShape = shape; // same shape for visual rhythm
|
|
5058
|
+
let rhythmSize = size * 0.6;
|
|
5059
|
+
for(let r = 0; r < rhythmCount; r++){
|
|
5060
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5061
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
5062
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
5063
|
+
if ($1f63dc64b5593c73$var$isInVoidZone(rx, ry, voidZones)) break;
|
|
5064
|
+
rhythmSize *= rhythmDecay;
|
|
5065
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
5066
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
5067
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
5068
|
+
const rhythmFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$18a34c25ea7e724b)((0, $b5a262d09b87e373$export$b49f62f0a99da0e8)(layerHierarchy, rng), rng, 5, 0.04), fillAlpha * 0.7);
|
|
5069
|
+
(0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, rhythmShape, rx, ry, {
|
|
5070
|
+
fillColor: rhythmFill,
|
|
5071
|
+
strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
|
|
5072
|
+
strokeWidth: strokeWidth * 0.7,
|
|
5073
|
+
size: rhythmSize,
|
|
5074
|
+
rotation: rotation + (r + 1) * 12,
|
|
5075
|
+
proportionType: "GOLDEN_RATIO",
|
|
5076
|
+
renderStyle: finalRenderStyle,
|
|
5077
|
+
rng: rng
|
|
5078
|
+
});
|
|
5079
|
+
shapePositions.push({
|
|
5080
|
+
x: rx,
|
|
5081
|
+
y: ry,
|
|
5082
|
+
size: rhythmSize,
|
|
5083
|
+
shape: rhythmShape
|
|
5084
|
+
});
|
|
5085
|
+
spatialGrid.insert({
|
|
5086
|
+
x: rx,
|
|
5087
|
+
y: ry,
|
|
5088
|
+
size: rhythmSize,
|
|
5089
|
+
shape: rhythmShape
|
|
5090
|
+
});
|
|
4853
5091
|
}
|
|
4854
5092
|
}
|
|
4855
5093
|
}
|
|
4856
5094
|
}
|
|
4857
5095
|
// Reset blend mode for post-processing passes
|
|
4858
5096
|
ctx.globalCompositeOperation = "source-over";
|
|
4859
|
-
// ──
|
|
5097
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
4860
5098
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
4861
5099
|
// with a tinted background wash, creating a "peek through" effect.
|
|
4862
5100
|
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
@@ -4937,6 +5175,12 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4937
5175
|
fx += Math.cos(angle) * stepLen;
|
|
4938
5176
|
fy += Math.sin(angle) * stepLen;
|
|
4939
5177
|
if (fx < 0 || fx > width || fy < 0 || fy > height) break;
|
|
5178
|
+
// Skip segments that pass through void zones
|
|
5179
|
+
if ($1f63dc64b5593c73$var$isInVoidZone(fx, fy, voidZones)) {
|
|
5180
|
+
prevX = fx;
|
|
5181
|
+
prevY = fy;
|
|
5182
|
+
continue;
|
|
5183
|
+
}
|
|
4940
5184
|
const t = s / steps;
|
|
4941
5185
|
// Taper + pressure
|
|
4942
5186
|
const taper = 1 - t * 0.8;
|
|
@@ -5034,30 +5278,60 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5034
5278
|
}
|
|
5035
5279
|
ctx.restore();
|
|
5036
5280
|
}
|
|
5037
|
-
// ── 7. Noise texture overlay
|
|
5281
|
+
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
5038
5282
|
const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
|
|
5039
5283
|
const noiseDensity = Math.floor(width * height / 800);
|
|
5040
|
-
|
|
5041
|
-
const
|
|
5042
|
-
const
|
|
5043
|
-
const
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5284
|
+
try {
|
|
5285
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
5286
|
+
const data = imageData.data;
|
|
5287
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
5288
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5289
|
+
const nx = Math.floor(noiseRng() * width);
|
|
5290
|
+
const ny = Math.floor(noiseRng() * height);
|
|
5291
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5292
|
+
const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
|
|
5293
|
+
// Write a small block of pixels for scale
|
|
5294
|
+
for(let dy = 0; dy < pixelScale && ny + dy < height; dy++)for(let dx = 0; dx < pixelScale && nx + dx < width; dx++){
|
|
5295
|
+
const idx = ((ny + dy) * width + (nx + dx)) * 4;
|
|
5296
|
+
// Alpha-blend the noise dot onto existing pixel data
|
|
5297
|
+
const srcA = alpha / 255;
|
|
5298
|
+
const invA = 1 - srcA;
|
|
5299
|
+
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
5300
|
+
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
5301
|
+
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
5302
|
+
// Keep existing alpha
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
ctx.putImageData(imageData, 0, 0);
|
|
5306
|
+
} catch {
|
|
5307
|
+
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
5308
|
+
for(let i = 0; i < noiseDensity; i++){
|
|
5309
|
+
const nx = noiseRng() * width;
|
|
5310
|
+
const ny = noiseRng() * height;
|
|
5311
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
5312
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
5313
|
+
ctx.globalAlpha = alpha;
|
|
5314
|
+
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
5315
|
+
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
5316
|
+
}
|
|
5048
5317
|
}
|
|
5049
5318
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
5050
5319
|
ctx.globalAlpha = 1;
|
|
5051
5320
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
5052
5321
|
const vigGrad = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.3, cx, cy, bgRadius);
|
|
5322
|
+
// Tint vignette based on background: warm sepia for light, cool blue for dark
|
|
5323
|
+
const isLightBg = bgLum > 0.5;
|
|
5324
|
+
const vignetteColor = isLightBg ? `rgba(80,60,30,${vignetteStrength.toFixed(3)})` // warm sepia
|
|
5325
|
+
: `rgba(0,0,0,${vignetteStrength.toFixed(3)})`; // classic dark
|
|
5053
5326
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
5054
5327
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
5055
|
-
vigGrad.addColorStop(1,
|
|
5328
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
5056
5329
|
ctx.fillStyle = vigGrad;
|
|
5057
5330
|
ctx.fillRect(0, 0, width, height);
|
|
5058
|
-
// ── 9. Organic connecting curves
|
|
5331
|
+
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5059
5332
|
if (shapePositions.length > 1) {
|
|
5060
5333
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5334
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5061
5335
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5062
5336
|
for(let i = 0; i < numCurves; i++){
|
|
5063
5337
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
@@ -5065,11 +5339,13 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5065
5339
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
5066
5340
|
const a = shapePositions[idxA];
|
|
5067
5341
|
const b = shapePositions[idxB];
|
|
5068
|
-
const mx = (a.x + b.x) / 2;
|
|
5069
|
-
const my = (a.y + b.y) / 2;
|
|
5070
5342
|
const dx = b.x - a.x;
|
|
5071
5343
|
const dy = b.y - a.y;
|
|
5072
5344
|
const dist = Math.hypot(dx, dy);
|
|
5345
|
+
// Skip connections between distant shapes
|
|
5346
|
+
if (dist > maxCurveDist) continue;
|
|
5347
|
+
const mx = (a.x + b.x) / 2;
|
|
5348
|
+
const my = (a.y + b.y) / 2;
|
|
5073
5349
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5074
5350
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5075
5351
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
@@ -5295,13 +5571,41 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5295
5571
|
// Other archetypes: no border (intentional — not every image needs one)
|
|
5296
5572
|
ctx.restore();
|
|
5297
5573
|
}
|
|
5298
|
-
// ── 11. Signature mark —
|
|
5574
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
5299
5575
|
{
|
|
5300
5576
|
const sigRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
|
|
5301
5577
|
const sigSize = Math.min(width, height) * 0.025;
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
const
|
|
5578
|
+
const sigMargin = sigSize * 2.5;
|
|
5579
|
+
// Find the corner with the lowest local density
|
|
5580
|
+
const cornerCandidates = [
|
|
5581
|
+
{
|
|
5582
|
+
x: sigMargin,
|
|
5583
|
+
y: sigMargin
|
|
5584
|
+
},
|
|
5585
|
+
{
|
|
5586
|
+
x: width - sigMargin,
|
|
5587
|
+
y: sigMargin
|
|
5588
|
+
},
|
|
5589
|
+
{
|
|
5590
|
+
x: sigMargin,
|
|
5591
|
+
y: height - sigMargin
|
|
5592
|
+
},
|
|
5593
|
+
{
|
|
5594
|
+
x: width - sigMargin,
|
|
5595
|
+
y: height - sigMargin
|
|
5596
|
+
}
|
|
5597
|
+
];
|
|
5598
|
+
let bestCorner = cornerCandidates[3]; // default: bottom-right
|
|
5599
|
+
let minDensity = Infinity;
|
|
5600
|
+
for (const corner of cornerCandidates){
|
|
5601
|
+
const density = spatialGrid.countNear(corner.x, corner.y, sigSize * 5);
|
|
5602
|
+
if (density < minDensity) {
|
|
5603
|
+
minDensity = density;
|
|
5604
|
+
bestCorner = corner;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
const sigX = bestCorner.x;
|
|
5608
|
+
const sigY = bestCorner.y;
|
|
5305
5609
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
5306
5610
|
const sigColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
5307
5611
|
ctx.save();
|