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/module.js
CHANGED
|
@@ -589,15 +589,17 @@ function $9d614e7d77fc2947$export$fabac4600b87056(colors, rng) {
|
|
|
589
589
|
accent: colors[colors.length - 1] || "#888888",
|
|
590
590
|
all: colors
|
|
591
591
|
};
|
|
592
|
-
// 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
|
|
593
594
|
const hsls = colors.map((c)=>$9d614e7d77fc2947$var$hexToHsl(c));
|
|
594
|
-
const avgHue = hsls.reduce((s, h)=>s + h[0], 0) / hsls.length;
|
|
595
595
|
let dominantIdx = 0;
|
|
596
|
-
let
|
|
596
|
+
let maxChroma = -1;
|
|
597
597
|
for(let i = 0; i < hsls.length; i++){
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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;
|
|
601
603
|
dominantIdx = i;
|
|
602
604
|
}
|
|
603
605
|
}
|
|
@@ -2494,16 +2496,26 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
|
|
|
2494
2496
|
ctx.shadowOffsetX = 0;
|
|
2495
2497
|
ctx.shadowOffsetY = 0;
|
|
2496
2498
|
ctx.shadowColor = "transparent";
|
|
2497
|
-
// ── Specular highlight —
|
|
2499
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
2498
2500
|
if (lightAngle !== undefined && size > 15 && rng) {
|
|
2499
2501
|
const hlRadius = size * 0.35;
|
|
2500
2502
|
const hlDist = size * 0.15;
|
|
2501
2503
|
const hlX = Math.cos(lightAngle) * hlDist;
|
|
2502
2504
|
const hlY = Math.sin(lightAngle) * hlDist;
|
|
2503
2505
|
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
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)`);
|
|
2507
2519
|
const savedOp = ctx.globalCompositeOperation;
|
|
2508
2520
|
ctx.globalCompositeOperation = "soft-light";
|
|
2509
2521
|
ctx.fillStyle = hlGrad;
|
|
@@ -3565,6 +3577,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3565
3577
|
"watercolor",
|
|
3566
3578
|
"fill-only"
|
|
3567
3579
|
],
|
|
3580
|
+
preferredCompositions: [
|
|
3581
|
+
"clustered",
|
|
3582
|
+
"flow-field",
|
|
3583
|
+
"radial"
|
|
3584
|
+
],
|
|
3568
3585
|
flowLineMultiplier: 2.5,
|
|
3569
3586
|
heroShape: false,
|
|
3570
3587
|
glowMultiplier: 0.5,
|
|
@@ -3586,6 +3603,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3586
3603
|
"stroke-only",
|
|
3587
3604
|
"incomplete"
|
|
3588
3605
|
],
|
|
3606
|
+
preferredCompositions: [
|
|
3607
|
+
"golden-spiral",
|
|
3608
|
+
"grid-subdivision"
|
|
3609
|
+
],
|
|
3589
3610
|
flowLineMultiplier: 0.3,
|
|
3590
3611
|
heroShape: true,
|
|
3591
3612
|
glowMultiplier: 0,
|
|
@@ -3607,6 +3628,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3607
3628
|
"fill-only",
|
|
3608
3629
|
"incomplete"
|
|
3609
3630
|
],
|
|
3631
|
+
preferredCompositions: [
|
|
3632
|
+
"flow-field",
|
|
3633
|
+
"golden-spiral",
|
|
3634
|
+
"spiral"
|
|
3635
|
+
],
|
|
3610
3636
|
flowLineMultiplier: 4,
|
|
3611
3637
|
heroShape: false,
|
|
3612
3638
|
glowMultiplier: 0.3,
|
|
@@ -3629,6 +3655,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3629
3655
|
"double-stroke",
|
|
3630
3656
|
"hatched"
|
|
3631
3657
|
],
|
|
3658
|
+
preferredCompositions: [
|
|
3659
|
+
"grid-subdivision",
|
|
3660
|
+
"radial"
|
|
3661
|
+
],
|
|
3632
3662
|
flowLineMultiplier: 0,
|
|
3633
3663
|
heroShape: false,
|
|
3634
3664
|
glowMultiplier: 0,
|
|
@@ -3650,6 +3680,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3650
3680
|
"incomplete",
|
|
3651
3681
|
"fill-only"
|
|
3652
3682
|
],
|
|
3683
|
+
preferredCompositions: [
|
|
3684
|
+
"golden-spiral",
|
|
3685
|
+
"radial",
|
|
3686
|
+
"spiral"
|
|
3687
|
+
],
|
|
3653
3688
|
flowLineMultiplier: 1.5,
|
|
3654
3689
|
heroShape: true,
|
|
3655
3690
|
glowMultiplier: 2,
|
|
@@ -3670,6 +3705,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3670
3705
|
"fill-and-stroke",
|
|
3671
3706
|
"double-stroke"
|
|
3672
3707
|
],
|
|
3708
|
+
preferredCompositions: [
|
|
3709
|
+
"grid-subdivision",
|
|
3710
|
+
"golden-spiral"
|
|
3711
|
+
],
|
|
3673
3712
|
flowLineMultiplier: 0,
|
|
3674
3713
|
heroShape: true,
|
|
3675
3714
|
glowMultiplier: 0,
|
|
@@ -3691,6 +3730,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3691
3730
|
"double-stroke",
|
|
3692
3731
|
"dashed"
|
|
3693
3732
|
],
|
|
3733
|
+
preferredCompositions: [
|
|
3734
|
+
"radial",
|
|
3735
|
+
"spiral",
|
|
3736
|
+
"clustered"
|
|
3737
|
+
],
|
|
3694
3738
|
flowLineMultiplier: 2,
|
|
3695
3739
|
heroShape: true,
|
|
3696
3740
|
glowMultiplier: 3,
|
|
@@ -3713,6 +3757,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3713
3757
|
"stroke-only",
|
|
3714
3758
|
"dashed"
|
|
3715
3759
|
],
|
|
3760
|
+
preferredCompositions: [
|
|
3761
|
+
"flow-field",
|
|
3762
|
+
"grid-subdivision",
|
|
3763
|
+
"clustered"
|
|
3764
|
+
],
|
|
3716
3765
|
flowLineMultiplier: 1.5,
|
|
3717
3766
|
heroShape: false,
|
|
3718
3767
|
glowMultiplier: 0,
|
|
@@ -3734,6 +3783,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3734
3783
|
"watercolor",
|
|
3735
3784
|
"fill-and-stroke"
|
|
3736
3785
|
],
|
|
3786
|
+
preferredCompositions: [
|
|
3787
|
+
"radial",
|
|
3788
|
+
"spiral",
|
|
3789
|
+
"golden-spiral"
|
|
3790
|
+
],
|
|
3737
3791
|
flowLineMultiplier: 3,
|
|
3738
3792
|
heroShape: true,
|
|
3739
3793
|
glowMultiplier: 2.5,
|
|
@@ -3755,6 +3809,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3755
3809
|
"fill-only",
|
|
3756
3810
|
"incomplete"
|
|
3757
3811
|
],
|
|
3812
|
+
preferredCompositions: [
|
|
3813
|
+
"golden-spiral",
|
|
3814
|
+
"flow-field",
|
|
3815
|
+
"radial"
|
|
3816
|
+
],
|
|
3758
3817
|
flowLineMultiplier: 0.5,
|
|
3759
3818
|
heroShape: false,
|
|
3760
3819
|
glowMultiplier: 0.3,
|
|
@@ -3776,6 +3835,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3776
3835
|
"stroke-only",
|
|
3777
3836
|
"dashed"
|
|
3778
3837
|
],
|
|
3838
|
+
preferredCompositions: [
|
|
3839
|
+
"grid-subdivision",
|
|
3840
|
+
"radial"
|
|
3841
|
+
],
|
|
3779
3842
|
flowLineMultiplier: 0,
|
|
3780
3843
|
heroShape: false,
|
|
3781
3844
|
glowMultiplier: 0,
|
|
@@ -3797,6 +3860,10 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3797
3860
|
"fill-only",
|
|
3798
3861
|
"double-stroke"
|
|
3799
3862
|
],
|
|
3863
|
+
preferredCompositions: [
|
|
3864
|
+
"grid-subdivision",
|
|
3865
|
+
"clustered"
|
|
3866
|
+
],
|
|
3800
3867
|
flowLineMultiplier: 0,
|
|
3801
3868
|
heroShape: true,
|
|
3802
3869
|
glowMultiplier: 0,
|
|
@@ -3818,6 +3885,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3818
3885
|
"watercolor",
|
|
3819
3886
|
"fill-only"
|
|
3820
3887
|
],
|
|
3888
|
+
preferredCompositions: [
|
|
3889
|
+
"radial",
|
|
3890
|
+
"golden-spiral",
|
|
3891
|
+
"flow-field"
|
|
3892
|
+
],
|
|
3821
3893
|
flowLineMultiplier: 1,
|
|
3822
3894
|
heroShape: true,
|
|
3823
3895
|
glowMultiplier: 1,
|
|
@@ -3839,6 +3911,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3839
3911
|
"stroke-only",
|
|
3840
3912
|
"fill-only"
|
|
3841
3913
|
],
|
|
3914
|
+
preferredCompositions: [
|
|
3915
|
+
"clustered",
|
|
3916
|
+
"grid-subdivision",
|
|
3917
|
+
"radial"
|
|
3918
|
+
],
|
|
3842
3919
|
flowLineMultiplier: 0,
|
|
3843
3920
|
heroShape: false,
|
|
3844
3921
|
glowMultiplier: 0.3,
|
|
@@ -3860,6 +3937,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3860
3937
|
"fill-only",
|
|
3861
3938
|
"incomplete"
|
|
3862
3939
|
],
|
|
3940
|
+
preferredCompositions: [
|
|
3941
|
+
"flow-field",
|
|
3942
|
+
"golden-spiral",
|
|
3943
|
+
"spiral"
|
|
3944
|
+
],
|
|
3863
3945
|
flowLineMultiplier: 3,
|
|
3864
3946
|
heroShape: true,
|
|
3865
3947
|
glowMultiplier: 0.2,
|
|
@@ -3881,6 +3963,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3881
3963
|
"fill-only",
|
|
3882
3964
|
"hatched"
|
|
3883
3965
|
],
|
|
3966
|
+
preferredCompositions: [
|
|
3967
|
+
"radial",
|
|
3968
|
+
"clustered",
|
|
3969
|
+
"flow-field"
|
|
3970
|
+
],
|
|
3884
3971
|
flowLineMultiplier: 0,
|
|
3885
3972
|
heroShape: false,
|
|
3886
3973
|
glowMultiplier: 0,
|
|
@@ -3903,6 +3990,11 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3903
3990
|
"stroke-only",
|
|
3904
3991
|
"incomplete"
|
|
3905
3992
|
],
|
|
3993
|
+
preferredCompositions: [
|
|
3994
|
+
"spiral",
|
|
3995
|
+
"radial",
|
|
3996
|
+
"golden-spiral"
|
|
3997
|
+
],
|
|
3906
3998
|
flowLineMultiplier: 2,
|
|
3907
3999
|
heroShape: true,
|
|
3908
4000
|
glowMultiplier: 2.5,
|
|
@@ -3926,6 +4018,12 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3926
4018
|
...b.preferredStyles
|
|
3927
4019
|
])
|
|
3928
4020
|
];
|
|
4021
|
+
const mergedCompositions = [
|
|
4022
|
+
...new Set([
|
|
4023
|
+
...a.preferredCompositions,
|
|
4024
|
+
...b.preferredCompositions
|
|
4025
|
+
])
|
|
4026
|
+
];
|
|
3929
4027
|
return {
|
|
3930
4028
|
name: `${a.name}+${b.name}`,
|
|
3931
4029
|
gridSize: Math.round($3faa2521b78398cf$var$lerpNum(a.gridSize, b.gridSize, t)),
|
|
@@ -3937,6 +4035,7 @@ const $3faa2521b78398cf$var$ARCHETYPES = [
|
|
|
3937
4035
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
3938
4036
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
3939
4037
|
preferredStyles: mergedStyles,
|
|
4038
|
+
preferredCompositions: mergedCompositions,
|
|
3940
4039
|
flowLineMultiplier: $3faa2521b78398cf$var$lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
3941
4040
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
3942
4041
|
glowMultiplier: $3faa2521b78398cf$var$lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
|
@@ -3970,7 +4069,8 @@ const $b623126c6e9cbb71$var$SACRED_SHAPES = [
|
|
|
3970
4069
|
"torus",
|
|
3971
4070
|
"eggOfLife"
|
|
3972
4071
|
];
|
|
3973
|
-
|
|
4072
|
+
// ── Composition modes ───────────────────────────────────────────────
|
|
4073
|
+
const $b623126c6e9cbb71$var$ALL_COMPOSITION_MODES = [
|
|
3974
4074
|
"radial",
|
|
3975
4075
|
"flow-field",
|
|
3976
4076
|
"spiral",
|
|
@@ -4072,7 +4172,69 @@ function $b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones) {
|
|
|
4072
4172
|
}
|
|
4073
4173
|
return false;
|
|
4074
4174
|
}
|
|
4075
|
-
// ──
|
|
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) ──────────────────────────
|
|
4076
4238
|
function $b623126c6e9cbb71$var$localDensity(x, y, positions, radius) {
|
|
4077
4239
|
let count = 0;
|
|
4078
4240
|
for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
|
|
@@ -4371,42 +4533,43 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4371
4533
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
4372
4534
|
const patternColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.dominant, 0.15);
|
|
4373
4535
|
if (bgPatternRoll < 0.2) {
|
|
4374
|
-
// Dot grid
|
|
4536
|
+
// Dot grid — batched into a single path
|
|
4375
4537
|
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
4376
4538
|
const dotR = dotSpacing * 0.08;
|
|
4377
4539
|
ctx.globalAlpha = patternOpacity;
|
|
4378
4540
|
ctx.fillStyle = patternColor;
|
|
4541
|
+
ctx.beginPath();
|
|
4379
4542
|
for(let px = 0; px < width; px += dotSpacing)for(let py = 0; py < height; py += dotSpacing){
|
|
4380
|
-
ctx.
|
|
4543
|
+
ctx.moveTo(px + dotR, py);
|
|
4381
4544
|
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
4382
|
-
ctx.fill();
|
|
4383
4545
|
}
|
|
4546
|
+
ctx.fill();
|
|
4384
4547
|
} else if (bgPatternRoll < 0.4) {
|
|
4385
|
-
// Diagonal lines
|
|
4548
|
+
// Diagonal lines — batched into a single path
|
|
4386
4549
|
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
4387
4550
|
ctx.globalAlpha = patternOpacity;
|
|
4388
4551
|
ctx.strokeStyle = patternColor;
|
|
4389
4552
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
4390
4553
|
const diag = Math.hypot(width, height);
|
|
4554
|
+
ctx.beginPath();
|
|
4391
4555
|
for(let d = -diag; d < diag; d += lineSpacing){
|
|
4392
|
-
ctx.beginPath();
|
|
4393
4556
|
ctx.moveTo(d, 0);
|
|
4394
4557
|
ctx.lineTo(d + height, height);
|
|
4395
|
-
ctx.stroke();
|
|
4396
4558
|
}
|
|
4559
|
+
ctx.stroke();
|
|
4397
4560
|
} else {
|
|
4398
|
-
// Tessellation — hexagonal grid
|
|
4561
|
+
// Tessellation — hexagonal grid, batched into a single path
|
|
4399
4562
|
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
4400
4563
|
const tessH = tessSize * Math.sqrt(3);
|
|
4401
4564
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
4402
4565
|
ctx.strokeStyle = patternColor;
|
|
4403
4566
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
4567
|
+
ctx.beginPath();
|
|
4404
4568
|
for(let row = 0; row * tessH < height + tessH; row++){
|
|
4405
4569
|
const offsetX = row % 2 * tessSize * 0.75;
|
|
4406
4570
|
for(let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++){
|
|
4407
4571
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
4408
4572
|
const hy = row * tessH;
|
|
4409
|
-
ctx.beginPath();
|
|
4410
4573
|
for(let s = 0; s < 6; s++){
|
|
4411
4574
|
const angle = Math.PI / 3 * s - Math.PI / 6;
|
|
4412
4575
|
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
@@ -4415,18 +4578,18 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4415
4578
|
else ctx.lineTo(vx, vy);
|
|
4416
4579
|
}
|
|
4417
4580
|
ctx.closePath();
|
|
4418
|
-
ctx.stroke();
|
|
4419
4581
|
}
|
|
4420
4582
|
}
|
|
4583
|
+
ctx.stroke();
|
|
4421
4584
|
}
|
|
4422
4585
|
ctx.restore();
|
|
4423
4586
|
}
|
|
4424
4587
|
ctx.globalCompositeOperation = "source-over";
|
|
4425
|
-
// ── 2. Composition mode
|
|
4426
|
-
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)];
|
|
4427
4590
|
const symRoll = rng();
|
|
4428
4591
|
const symmetryMode = symRoll < 0.10 ? "bilateral-x" : symRoll < 0.20 ? "bilateral-y" : symRoll < 0.25 ? "quad" : "none";
|
|
4429
|
-
// ── 3. Focal points + void zones
|
|
4592
|
+
// ── 3. Focal points + void zones (archetype-aware) ───────────────
|
|
4430
4593
|
const THIRDS_POINTS = [
|
|
4431
4594
|
{
|
|
4432
4595
|
x: 1 / 3,
|
|
@@ -4459,9 +4622,23 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4459
4622
|
y: height * (0.2 + rng() * 0.6),
|
|
4460
4623
|
strength: 0.3 + rng() * 0.4
|
|
4461
4624
|
});
|
|
4462
|
-
|
|
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;
|
|
4463
4631
|
const voidZones = [];
|
|
4464
|
-
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({
|
|
4465
4642
|
x: width * (0.15 + rng() * 0.7),
|
|
4466
4643
|
y: height * (0.15 + rng() * 0.7),
|
|
4467
4644
|
radius: Math.min(width, height) * (0.06 + rng() * 0.1)
|
|
@@ -4538,6 +4715,9 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4538
4715
|
}
|
|
4539
4716
|
// Track all placed shapes for density checks and connecting curves
|
|
4540
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);
|
|
4541
4721
|
// Hero avoidance radius — shapes near the hero orient toward it
|
|
4542
4722
|
let heroCenter = null;
|
|
4543
4723
|
// ── 4b. Hero shape — a dominant focal element ───────────────────
|
|
@@ -4583,9 +4763,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4583
4763
|
size: heroSize,
|
|
4584
4764
|
shape: heroShape
|
|
4585
4765
|
});
|
|
4766
|
+
spatialGrid.insert({
|
|
4767
|
+
x: heroFocal.x,
|
|
4768
|
+
y: heroFocal.y,
|
|
4769
|
+
size: heroSize,
|
|
4770
|
+
shape: heroShape
|
|
4771
|
+
});
|
|
4586
4772
|
}
|
|
4587
4773
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
4588
|
-
const densityCheckRadius = Math.min(width, height) * 0.08;
|
|
4589
4774
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
4590
4775
|
for(let layer = 0; layer < layers; layer++){
|
|
4591
4776
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
@@ -4624,7 +4809,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4624
4809
|
if ($b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones)) {
|
|
4625
4810
|
if (rng() < 0.85) continue;
|
|
4626
4811
|
}
|
|
4627
|
-
if (
|
|
4812
|
+
if (spatialGrid.countNear(x, y, densityCheckRadius) > maxLocalDensity) {
|
|
4628
4813
|
if (rng() < 0.6) continue;
|
|
4629
4814
|
}
|
|
4630
4815
|
// Power distribution for size — archetype controls the curve
|
|
@@ -4684,17 +4869,11 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4684
4869
|
let finalX = x;
|
|
4685
4870
|
let finalY = y;
|
|
4686
4871
|
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
4687
|
-
//
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
for (const sp of shapePositions){
|
|
4691
|
-
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
4692
|
-
if (d < nearestDist && d > 0) {
|
|
4693
|
-
nearestDist = d;
|
|
4694
|
-
nearestPos = sp;
|
|
4695
|
-
}
|
|
4696
|
-
}
|
|
4872
|
+
// Use spatial grid for O(1) nearest-neighbor lookup
|
|
4873
|
+
const searchRadius = adjustedMaxSize * 3;
|
|
4874
|
+
const nearestPos = spatialGrid.findNearest(x, y, searchRadius);
|
|
4697
4875
|
if (nearestPos) {
|
|
4876
|
+
const nearestDist = Math.hypot(x - nearestPos.x, y - nearestPos.y);
|
|
4698
4877
|
// Target distance: edges kissing (sum of half-sizes)
|
|
4699
4878
|
const targetDist = (size + nearestPos.size) * 0.5;
|
|
4700
4879
|
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
@@ -4767,6 +4946,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4767
4946
|
size: size,
|
|
4768
4947
|
shape: shape
|
|
4769
4948
|
});
|
|
4949
|
+
spatialGrid.insert({
|
|
4950
|
+
x: finalX,
|
|
4951
|
+
y: finalY,
|
|
4952
|
+
size: size,
|
|
4953
|
+
shape: shape
|
|
4954
|
+
});
|
|
4770
4955
|
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
4771
4956
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
4772
4957
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
@@ -4795,6 +4980,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4795
4980
|
size: echoSize,
|
|
4796
4981
|
shape: shape
|
|
4797
4982
|
});
|
|
4983
|
+
spatialGrid.insert({
|
|
4984
|
+
x: echoX,
|
|
4985
|
+
y: echoY,
|
|
4986
|
+
size: echoSize,
|
|
4987
|
+
shape: shape
|
|
4988
|
+
});
|
|
4798
4989
|
}
|
|
4799
4990
|
}
|
|
4800
4991
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
@@ -4859,13 +5050,62 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4859
5050
|
size: member.size,
|
|
4860
5051
|
shape: memberShape
|
|
4861
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
|
+
});
|
|
4862
5102
|
}
|
|
4863
5103
|
}
|
|
4864
5104
|
}
|
|
4865
5105
|
}
|
|
4866
5106
|
// Reset blend mode for post-processing passes
|
|
4867
5107
|
ctx.globalCompositeOperation = "source-over";
|
|
4868
|
-
// ──
|
|
5108
|
+
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
4869
5109
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
4870
5110
|
// with a tinted background wash, creating a "peek through" effect.
|
|
4871
5111
|
if (rng() < 0.18 && shapePositions.length > 3) {
|
|
@@ -4946,6 +5186,12 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
4946
5186
|
fx += Math.cos(angle) * stepLen;
|
|
4947
5187
|
fy += Math.sin(angle) * stepLen;
|
|
4948
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
|
+
}
|
|
4949
5195
|
const t = s / steps;
|
|
4950
5196
|
// Taper + pressure
|
|
4951
5197
|
const taper = 1 - t * 0.8;
|
|
@@ -5043,30 +5289,60 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5043
5289
|
}
|
|
5044
5290
|
ctx.restore();
|
|
5045
5291
|
}
|
|
5046
|
-
// ── 7. Noise texture overlay
|
|
5292
|
+
// ── 7. Noise texture overlay — batched via ImageData ─────────────
|
|
5047
5293
|
const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
|
|
5048
5294
|
const noiseDensity = Math.floor(width * height / 800);
|
|
5049
|
-
|
|
5050
|
-
const
|
|
5051
|
-
const
|
|
5052
|
-
const
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
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
|
+
}
|
|
5057
5328
|
}
|
|
5058
5329
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
5059
5330
|
ctx.globalAlpha = 1;
|
|
5060
5331
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
5061
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
|
|
5062
5337
|
vigGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
5063
5338
|
vigGrad.addColorStop(0.6, "rgba(0,0,0,0)");
|
|
5064
|
-
vigGrad.addColorStop(1,
|
|
5339
|
+
vigGrad.addColorStop(1, vignetteColor);
|
|
5065
5340
|
ctx.fillStyle = vigGrad;
|
|
5066
5341
|
ctx.fillRect(0, 0, width, height);
|
|
5067
|
-
// ── 9. Organic connecting curves
|
|
5342
|
+
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
5068
5343
|
if (shapePositions.length > 1) {
|
|
5069
5344
|
const numCurves = Math.floor(8 * (width * height) / 1048576);
|
|
5345
|
+
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
5070
5346
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
5071
5347
|
for(let i = 0; i < numCurves; i++){
|
|
5072
5348
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
@@ -5074,11 +5350,13 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5074
5350
|
const idxB = (idxA + offset) % shapePositions.length;
|
|
5075
5351
|
const a = shapePositions[idxA];
|
|
5076
5352
|
const b = shapePositions[idxB];
|
|
5077
|
-
const mx = (a.x + b.x) / 2;
|
|
5078
|
-
const my = (a.y + b.y) / 2;
|
|
5079
5353
|
const dx = b.x - a.x;
|
|
5080
5354
|
const dy = b.y - a.y;
|
|
5081
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;
|
|
5082
5360
|
const bulge = (rng() - 0.5) * dist * 0.4;
|
|
5083
5361
|
const cpx = mx + -dy / (dist || 1) * bulge;
|
|
5084
5362
|
const cpy = my + dx / (dist || 1) * bulge;
|
|
@@ -5304,13 +5582,41 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
|
|
|
5304
5582
|
// Other archetypes: no border (intentional — not every image needs one)
|
|
5305
5583
|
ctx.restore();
|
|
5306
5584
|
}
|
|
5307
|
-
// ── 11. Signature mark —
|
|
5585
|
+
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
5308
5586
|
{
|
|
5309
5587
|
const sigRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
|
|
5310
5588
|
const sigSize = Math.min(width, height) * 0.025;
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
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;
|
|
5314
5620
|
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
5315
5621
|
const sigColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colorHierarchy.accent, 0.15);
|
|
5316
5622
|
ctx.save();
|