@fieldnotes/core 0.65.0 → 0.67.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/README.md +742 -706
- package/dist/index.cjs +1627 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +243 -19
- package/dist/index.d.ts +243 -19
- package/dist/index.js +1612 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,12 @@ __export(index_exports, {
|
|
|
31
31
|
ElementStore: () => ElementStore,
|
|
32
32
|
EraserTool: () => EraserTool,
|
|
33
33
|
FOCUS_PRESENCE_KIND: () => FOCUS_PRESENCE_KIND,
|
|
34
|
+
FOG_MAX_TILES: () => FOG_MAX_TILES,
|
|
35
|
+
FOG_STATE_VERSION: () => FOG_STATE_VERSION,
|
|
36
|
+
FOG_TILE_CELLS: () => FOG_TILE_CELLS,
|
|
37
|
+
FogManager: () => FogManager,
|
|
38
|
+
FogRenderer: () => FogRenderer,
|
|
39
|
+
FogTool: () => FogTool,
|
|
34
40
|
HandTool: () => HandTool,
|
|
35
41
|
HistoryStack: () => HistoryStack,
|
|
36
42
|
HtmlPainterMissingError: () => HtmlPainterMissingError,
|
|
@@ -74,6 +80,7 @@ __export(index_exports, {
|
|
|
74
80
|
attachAwareness: () => attachAwareness,
|
|
75
81
|
boundsIntersect: () => boundsIntersect,
|
|
76
82
|
cameraOriginForView: () => cameraOriginForView,
|
|
83
|
+
canonicalizeFogTile: () => canonicalizeFogTile,
|
|
77
84
|
captureCameraView: () => captureCameraView,
|
|
78
85
|
computeElementRects: () => computeElementRects,
|
|
79
86
|
createArrow: () => createArrow,
|
|
@@ -91,6 +98,8 @@ __export(index_exports, {
|
|
|
91
98
|
exportImage: () => exportImage,
|
|
92
99
|
exportSvg: () => exportSvg,
|
|
93
100
|
fitZoomForView: () => fitZoomForView,
|
|
101
|
+
fogDecodeBase64: () => decodeBase64,
|
|
102
|
+
fogEncodeBase64: () => encodeBase64,
|
|
94
103
|
footprintFromSize: () => footprintFromSize,
|
|
95
104
|
getActiveFormats: () => getActiveFormats,
|
|
96
105
|
getArrowBounds: () => getArrowBounds,
|
|
@@ -116,6 +125,8 @@ __export(index_exports, {
|
|
|
116
125
|
isPathPresence: () => isPathPresence,
|
|
117
126
|
isPingPresence: () => isPingPresence,
|
|
118
127
|
pathDistanceCells: () => pathDistanceCells,
|
|
128
|
+
recommendedFogCellSize: () => recommendedFogCellSize,
|
|
129
|
+
resolveFogStyle: () => resolveFogStyle,
|
|
119
130
|
resolveHtmlRouting: () => resolveHtmlRouting,
|
|
120
131
|
setFontSize: () => setFontSize,
|
|
121
132
|
smartSnap: () => smartSnap,
|
|
@@ -132,7 +143,10 @@ __export(index_exports, {
|
|
|
132
143
|
toggleBold: () => toggleBold,
|
|
133
144
|
toggleItalic: () => toggleItalic,
|
|
134
145
|
toggleStrikethrough: () => toggleStrikethrough,
|
|
135
|
-
toggleUnderline: () => toggleUnderline
|
|
146
|
+
toggleUnderline: () => toggleUnderline,
|
|
147
|
+
validateFogDefinition: () => validateFogDefinition,
|
|
148
|
+
validateFogState: () => validateFogState,
|
|
149
|
+
validateFogTile: () => validateFogTile
|
|
136
150
|
});
|
|
137
151
|
module.exports = __toCommonJS(index_exports);
|
|
138
152
|
|
|
@@ -592,8 +606,464 @@ function sanitizeAttributes(el, tag) {
|
|
|
592
606
|
}
|
|
593
607
|
}
|
|
594
608
|
|
|
609
|
+
// src/fog/types.ts
|
|
610
|
+
var FOG_STATE_VERSION = 1;
|
|
611
|
+
var FOG_TILE_CELLS = 128;
|
|
612
|
+
var FOG_MAX_TILES = 256;
|
|
613
|
+
|
|
614
|
+
// src/fog/tile-codec.ts
|
|
615
|
+
var TILE_BYTES = FOG_TILE_CELLS * FOG_TILE_CELLS / 8;
|
|
616
|
+
var CANONICAL_B64_LENGTH = Math.ceil(TILE_BYTES / 3) * 4;
|
|
617
|
+
var B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
618
|
+
var B64_LOOKUP = new Uint8Array(128);
|
|
619
|
+
for (let i = 0; i < B64_CHARS.length; i++) B64_LOOKUP[B64_CHARS.charCodeAt(i)] = i;
|
|
620
|
+
function encodeBase64(bytes) {
|
|
621
|
+
let result = "";
|
|
622
|
+
const len = bytes.length;
|
|
623
|
+
for (let i = 0; i < len; i += 3) {
|
|
624
|
+
const a = bytes[i];
|
|
625
|
+
const b = i + 1 < len ? bytes[i + 1] : 0;
|
|
626
|
+
const c = i + 2 < len ? bytes[i + 2] : 0;
|
|
627
|
+
result += B64_CHARS[a >> 2 & 63];
|
|
628
|
+
result += B64_CHARS[(a << 4 | b >> 4) & 63];
|
|
629
|
+
result += i + 1 < len ? B64_CHARS[(b << 2 | c >> 6) & 63] : "=";
|
|
630
|
+
result += i + 2 < len ? B64_CHARS[c & 63] : "=";
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
function decodeBase64(str) {
|
|
635
|
+
if (str.length % 4 !== 0) throw new Error("Invalid base64 length");
|
|
636
|
+
let padCount = 0;
|
|
637
|
+
if (str.length >= 2 && str[str.length - 1] === "=") {
|
|
638
|
+
padCount++;
|
|
639
|
+
if (str[str.length - 2] === "=") padCount++;
|
|
640
|
+
}
|
|
641
|
+
const byteLen = str.length / 4 * 3 - padCount;
|
|
642
|
+
const bytes = new Uint8Array(byteLen);
|
|
643
|
+
let j = 0;
|
|
644
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
645
|
+
const a = B64_LOOKUP[str.charCodeAt(i)];
|
|
646
|
+
const b = B64_LOOKUP[str.charCodeAt(i + 1)];
|
|
647
|
+
const c = str[i + 2] === "=" ? 0 : B64_LOOKUP[str.charCodeAt(i + 2)];
|
|
648
|
+
const d = str[i + 3] === "=" ? 0 : B64_LOOKUP[str.charCodeAt(i + 3)];
|
|
649
|
+
bytes[j++] = a << 2 | b >> 4;
|
|
650
|
+
if (j < byteLen) bytes[j++] = (b << 4 | c >> 2) & 255;
|
|
651
|
+
if (j < byteLen) bytes[j++] = (c << 6 | d) & 255;
|
|
652
|
+
}
|
|
653
|
+
return bytes;
|
|
654
|
+
}
|
|
655
|
+
function createTileBytes(fill) {
|
|
656
|
+
const bytes = new Uint8Array(TILE_BYTES);
|
|
657
|
+
if (fill) bytes.fill(255);
|
|
658
|
+
return bytes;
|
|
659
|
+
}
|
|
660
|
+
function setBit(bytes, col, row, value) {
|
|
661
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
662
|
+
const byteIndex = index >> 3;
|
|
663
|
+
const bitIndex = 7 - (index & 7);
|
|
664
|
+
if (value) {
|
|
665
|
+
bytes[byteIndex] = bytes[byteIndex] | 1 << bitIndex;
|
|
666
|
+
} else {
|
|
667
|
+
bytes[byteIndex] = bytes[byteIndex] & ~(1 << bitIndex);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function isTileAllValue(bytes, value) {
|
|
671
|
+
const expected = value ? 255 : 0;
|
|
672
|
+
for (let i = 0; i < TILE_BYTES; i++) {
|
|
673
|
+
if (bytes[i] !== expected) return false;
|
|
674
|
+
}
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
function isBaseValue(base) {
|
|
678
|
+
return base === "revealed";
|
|
679
|
+
}
|
|
680
|
+
function isTileBase(bytes, base) {
|
|
681
|
+
return isTileAllValue(bytes, isBaseValue(base));
|
|
682
|
+
}
|
|
683
|
+
function canonicalizeEdgePadding(bytes, def, tileX, tileY) {
|
|
684
|
+
const baseVal = isBaseValue(def.base);
|
|
685
|
+
const worldX = tileX * FOG_TILE_CELLS * def.cellSize;
|
|
686
|
+
const worldY = tileY * FOG_TILE_CELLS * def.cellSize;
|
|
687
|
+
const boundsRight = def.bounds.x + def.bounds.w;
|
|
688
|
+
const boundsBottom = def.bounds.y + def.bounds.h;
|
|
689
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
690
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
691
|
+
const cellWorldX = worldX + col * def.cellSize;
|
|
692
|
+
const cellWorldY = worldY + row * def.cellSize;
|
|
693
|
+
const outside = cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= boundsRight || cellWorldY >= boundsBottom;
|
|
694
|
+
if (outside) {
|
|
695
|
+
setBit(bytes, col, row, baseVal);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function canonicalizeFogTile(tile, def) {
|
|
701
|
+
const bytes = decodeBase64(tile.data);
|
|
702
|
+
canonicalizeEdgePadding(bytes, def, tile.x, tile.y);
|
|
703
|
+
if (isTileBase(bytes, def.base)) return null;
|
|
704
|
+
return { x: tile.x, y: tile.y, data: encodeBase64(bytes) };
|
|
705
|
+
}
|
|
706
|
+
function isCanonicalBase64(str) {
|
|
707
|
+
if (str.length !== CANONICAL_B64_LENGTH) return false;
|
|
708
|
+
if (str[str.length - 1] !== "=" || str[str.length - 2] === "=") return false;
|
|
709
|
+
for (let i = 0; i < str.length - 1; i++) {
|
|
710
|
+
if (!B64_CHARS.includes(str[i])) return false;
|
|
711
|
+
}
|
|
712
|
+
const finalSextet = B64_CHARS.indexOf(str[str.length - 2]);
|
|
713
|
+
return finalSextet >= 0 && (finalSextet & 3) === 0;
|
|
714
|
+
}
|
|
715
|
+
function isSafeInteger(n2) {
|
|
716
|
+
return typeof n2 === "number" && Number.isSafeInteger(n2);
|
|
717
|
+
}
|
|
718
|
+
function isFinitePositive(n2) {
|
|
719
|
+
return typeof n2 === "number" && Number.isFinite(n2) && n2 > 0;
|
|
720
|
+
}
|
|
721
|
+
function isFiniteBounds(b) {
|
|
722
|
+
if (typeof b !== "object" || b === null) return false;
|
|
723
|
+
const r = b;
|
|
724
|
+
return typeof r["x"] === "number" && Number.isFinite(r["x"]) && typeof r["y"] === "number" && Number.isFinite(r["y"]) && isFinitePositive(r["w"]) && isFinitePositive(r["h"]);
|
|
725
|
+
}
|
|
726
|
+
function tileIntersectsBounds(x, y, def) {
|
|
727
|
+
const tileWorldX = x * FOG_TILE_CELLS * def.cellSize;
|
|
728
|
+
const tileWorldY = y * FOG_TILE_CELLS * def.cellSize;
|
|
729
|
+
const tileWorldW = FOG_TILE_CELLS * def.cellSize;
|
|
730
|
+
const tileWorldH = FOG_TILE_CELLS * def.cellSize;
|
|
731
|
+
return !(tileWorldX + tileWorldW <= def.bounds.x || tileWorldY + tileWorldH <= def.bounds.y || tileWorldX >= def.bounds.x + def.bounds.w || tileWorldY >= def.bounds.y + def.bounds.h);
|
|
732
|
+
}
|
|
733
|
+
function validateFogDefinition(def) {
|
|
734
|
+
if (typeof def !== "object" || def === null) {
|
|
735
|
+
throw new Error("Invalid fog definition: expected an object");
|
|
736
|
+
}
|
|
737
|
+
const d = def;
|
|
738
|
+
if (d["version"] !== 1) throw new Error("Invalid fog definition: unsupported version");
|
|
739
|
+
if (typeof d["generation"] !== "string" || d["generation"].length === 0 || d["generation"].length > 128 || !/^[\x20-\x7e]+$/.test(d["generation"])) {
|
|
740
|
+
throw new Error("Invalid fog definition: invalid generation");
|
|
741
|
+
}
|
|
742
|
+
if (!isFiniteBounds(d["bounds"])) {
|
|
743
|
+
throw new Error("Invalid fog definition: invalid bounds");
|
|
744
|
+
}
|
|
745
|
+
if (!isFinitePositive(d["cellSize"])) {
|
|
746
|
+
throw new Error("Invalid fog definition: invalid cellSize");
|
|
747
|
+
}
|
|
748
|
+
if (d["tileCells"] !== 128) {
|
|
749
|
+
throw new Error("Invalid fog definition: tileCells must be 128");
|
|
750
|
+
}
|
|
751
|
+
if (d["base"] !== "covered" && d["base"] !== "revealed") {
|
|
752
|
+
throw new Error("Invalid fog definition: invalid base");
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function validateFogTile(tile, def) {
|
|
756
|
+
if (typeof tile !== "object" || tile === null) {
|
|
757
|
+
throw new Error("Invalid fog tile: expected an object");
|
|
758
|
+
}
|
|
759
|
+
const t = tile;
|
|
760
|
+
if (!isSafeInteger(t["x"]) || !isSafeInteger(t["y"])) {
|
|
761
|
+
throw new Error("Invalid fog tile: coordinates must be safe integers");
|
|
762
|
+
}
|
|
763
|
+
if (!tileIntersectsBounds(t["x"], t["y"], def)) {
|
|
764
|
+
throw new Error("Invalid fog tile: coordinates outside bounds");
|
|
765
|
+
}
|
|
766
|
+
if (typeof t["data"] !== "string" || !isCanonicalBase64(t["data"])) {
|
|
767
|
+
throw new Error("Invalid fog tile: invalid data");
|
|
768
|
+
}
|
|
769
|
+
const decoded = decodeBase64(t["data"]);
|
|
770
|
+
if (decoded.length !== TILE_BYTES) {
|
|
771
|
+
throw new Error("Invalid fog tile: decoded data wrong length");
|
|
772
|
+
}
|
|
773
|
+
if (isTileBase(decoded, def.base)) {
|
|
774
|
+
throw new Error("Invalid fog tile: base-value tiles must be omitted");
|
|
775
|
+
}
|
|
776
|
+
const canonical = new Uint8Array(decoded);
|
|
777
|
+
canonicalizeEdgePadding(canonical, def, t["x"], t["y"]);
|
|
778
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
779
|
+
if (decoded[i] !== canonical[i]) {
|
|
780
|
+
throw new Error("Invalid fog tile: non-canonical edge padding");
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function validateFogState(state) {
|
|
785
|
+
if (typeof state !== "object" || state === null) {
|
|
786
|
+
throw new Error("Invalid fog state: expected an object");
|
|
787
|
+
}
|
|
788
|
+
const s = state;
|
|
789
|
+
validateFogDefinition(s["definition"]);
|
|
790
|
+
const def = s["definition"];
|
|
791
|
+
if (!Array.isArray(s["tiles"])) {
|
|
792
|
+
throw new Error("Invalid fog state: tiles must be an array");
|
|
793
|
+
}
|
|
794
|
+
const tiles = s["tiles"];
|
|
795
|
+
if (tiles.length > FOG_MAX_TILES) {
|
|
796
|
+
throw new Error(`Invalid fog state: too many tiles (${tiles.length} > ${FOG_MAX_TILES})`);
|
|
797
|
+
}
|
|
798
|
+
const seen = /* @__PURE__ */ new Set();
|
|
799
|
+
for (const tile of tiles) {
|
|
800
|
+
validateFogTile(tile, def);
|
|
801
|
+
const t = tile;
|
|
802
|
+
const key = `${t.x},${t.y}`;
|
|
803
|
+
if (seen.has(key)) {
|
|
804
|
+
throw new Error(`Invalid fog state: duplicate tile at (${t.x}, ${t.y})`);
|
|
805
|
+
}
|
|
806
|
+
seen.add(key);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function recommendedFogCellSize(bounds) {
|
|
810
|
+
let cellSize = 1;
|
|
811
|
+
while (true) {
|
|
812
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
813
|
+
const minTX = Math.floor(bounds.x / tileWorldSize);
|
|
814
|
+
const minTY = Math.floor(bounds.y / tileWorldSize);
|
|
815
|
+
const maxTX = Math.ceil((bounds.x + bounds.w) / tileWorldSize) - 1;
|
|
816
|
+
const maxTY = Math.ceil((bounds.y + bounds.h) / tileWorldSize) - 1;
|
|
817
|
+
const tw = maxTX - minTX + 1;
|
|
818
|
+
const th = maxTY - minTY + 1;
|
|
819
|
+
if (tw * th <= FOG_MAX_TILES) return cellSize;
|
|
820
|
+
cellSize++;
|
|
821
|
+
if (cellSize > 1e4) return cellSize;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function tileKey(x, y) {
|
|
825
|
+
return `${x},${y}`;
|
|
826
|
+
}
|
|
827
|
+
function worldToCell(worldX, worldY, cellSize) {
|
|
828
|
+
const cellX = Math.floor(worldX / cellSize);
|
|
829
|
+
const cellY = Math.floor(worldY / cellSize);
|
|
830
|
+
const tx = Math.floor(cellX / FOG_TILE_CELLS);
|
|
831
|
+
const ty = Math.floor(cellY / FOG_TILE_CELLS);
|
|
832
|
+
let col = cellX - tx * FOG_TILE_CELLS;
|
|
833
|
+
let row = cellY - ty * FOG_TILE_CELLS;
|
|
834
|
+
if (col < 0) col += FOG_TILE_CELLS;
|
|
835
|
+
if (row < 0) row += FOG_TILE_CELLS;
|
|
836
|
+
return { tx, ty, col, row };
|
|
837
|
+
}
|
|
838
|
+
function rasterizeRegion(state, region, operation) {
|
|
839
|
+
const { definition } = state;
|
|
840
|
+
const bitValue = operation === "reveal";
|
|
841
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
842
|
+
for (const tile of state.tiles) {
|
|
843
|
+
tileMap.set(tileKey(tile.x, tile.y), decodeBase64(tile.data));
|
|
844
|
+
}
|
|
845
|
+
const baseFill = isBaseValue(definition.base);
|
|
846
|
+
const affectedTiles = /* @__PURE__ */ new Set();
|
|
847
|
+
const setCellIfInBounds = (worldX, worldY) => {
|
|
848
|
+
if (worldX < definition.bounds.x || worldY < definition.bounds.y || worldX >= definition.bounds.x + definition.bounds.w || worldY >= definition.bounds.y + definition.bounds.h) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const { tx, ty, col, row } = worldToCell(worldX, worldY, definition.cellSize);
|
|
852
|
+
if (!tileIntersectsBounds(tx, ty, definition)) return;
|
|
853
|
+
const key = tileKey(tx, ty);
|
|
854
|
+
let bytes = tileMap.get(key);
|
|
855
|
+
if (!bytes) {
|
|
856
|
+
bytes = createTileBytes(baseFill);
|
|
857
|
+
tileMap.set(key, bytes);
|
|
858
|
+
} else if (!affectedTiles.has(key)) {
|
|
859
|
+
const clone = new Uint8Array(bytes);
|
|
860
|
+
tileMap.set(key, clone);
|
|
861
|
+
bytes = clone;
|
|
862
|
+
}
|
|
863
|
+
affectedTiles.add(key);
|
|
864
|
+
setBit(bytes, col, row, bitValue);
|
|
865
|
+
};
|
|
866
|
+
switch (region.kind) {
|
|
867
|
+
case "brush":
|
|
868
|
+
rasterizeBrush(region.points, region.radius, definition, setCellIfInBounds);
|
|
869
|
+
break;
|
|
870
|
+
case "rectangle":
|
|
871
|
+
rasterizeRectangle(region.from, region.to, definition, setCellIfInBounds);
|
|
872
|
+
break;
|
|
873
|
+
case "polygon":
|
|
874
|
+
rasterizePolygon(region.points, definition, setCellIfInBounds);
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
if (affectedTiles.size === 0) return { changed: [], noop: true };
|
|
878
|
+
const changedTiles = [];
|
|
879
|
+
let hasChange = false;
|
|
880
|
+
for (const key of affectedTiles) {
|
|
881
|
+
const bytes = tileMap.get(key);
|
|
882
|
+
const [txStr, tyStr] = key.split(",");
|
|
883
|
+
const tx = Number(txStr);
|
|
884
|
+
const ty = Number(tyStr);
|
|
885
|
+
canonicalizeEdgePadding(bytes, definition, tx, ty);
|
|
886
|
+
const newData = encodeBase64(bytes);
|
|
887
|
+
const originalTile = state.tiles.find((t) => t.x === tx && t.y === ty);
|
|
888
|
+
if (originalTile) {
|
|
889
|
+
if (originalTile.data !== newData) {
|
|
890
|
+
hasChange = true;
|
|
891
|
+
changedTiles.push({ x: tx, y: ty, data: newData });
|
|
892
|
+
}
|
|
893
|
+
} else if (!isTileBase(bytes, definition.base)) {
|
|
894
|
+
hasChange = true;
|
|
895
|
+
changedTiles.push({ x: tx, y: ty, data: newData });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (!hasChange) return { changed: [], noop: true };
|
|
899
|
+
return { changed: changedTiles, noop: false };
|
|
900
|
+
}
|
|
901
|
+
function applyRasterResult(state, result) {
|
|
902
|
+
if (result.noop) return state;
|
|
903
|
+
const changedMap = /* @__PURE__ */ new Map();
|
|
904
|
+
for (const t of result.changed) changedMap.set(tileKey(t.x, t.y), t);
|
|
905
|
+
const tiles = [];
|
|
906
|
+
const seen = /* @__PURE__ */ new Set();
|
|
907
|
+
for (const tile of state.tiles) {
|
|
908
|
+
const key = tileKey(tile.x, tile.y);
|
|
909
|
+
seen.add(key);
|
|
910
|
+
const changed = changedMap.get(key);
|
|
911
|
+
if (changed) {
|
|
912
|
+
const bytes = decodeBase64(changed.data);
|
|
913
|
+
if (!isTileBase(bytes, state.definition.base)) {
|
|
914
|
+
tiles.push(changed);
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
tiles.push(tile);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
for (const t of result.changed) {
|
|
921
|
+
const key = tileKey(t.x, t.y);
|
|
922
|
+
if (seen.has(key)) continue;
|
|
923
|
+
const bytes = decodeBase64(t.data);
|
|
924
|
+
if (!isTileBase(bytes, state.definition.base)) {
|
|
925
|
+
tiles.push(t);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (tiles.length > FOG_MAX_TILES) {
|
|
929
|
+
throw new Error(`Fog tile cap exceeded: ${tiles.length} > ${FOG_MAX_TILES}`);
|
|
930
|
+
}
|
|
931
|
+
return { definition: state.definition, tiles };
|
|
932
|
+
}
|
|
933
|
+
function sampleAndSimplify(points, tolerance) {
|
|
934
|
+
if (points.length <= 2) return [...points];
|
|
935
|
+
const sampled = [points[0]];
|
|
936
|
+
let lastSampled = points[0];
|
|
937
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
938
|
+
const p = points[i];
|
|
939
|
+
const dx = p.x - lastSampled.x;
|
|
940
|
+
const dy = p.y - lastSampled.y;
|
|
941
|
+
if (dx * dx + dy * dy >= tolerance * tolerance) {
|
|
942
|
+
sampled.push(p);
|
|
943
|
+
lastSampled = p;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
sampled.push(points[points.length - 1]);
|
|
947
|
+
return sampled;
|
|
948
|
+
}
|
|
949
|
+
function rasterizeBrush(points, radius, def, setCell) {
|
|
950
|
+
if (points.length === 0) return;
|
|
951
|
+
const simplified = sampleAndSimplify(points, def.cellSize * 0.5);
|
|
952
|
+
for (let i = 0; i < simplified.length; i++) {
|
|
953
|
+
const p = simplified[i];
|
|
954
|
+
rasterizeDisc(p.x, p.y, radius, def, setCell);
|
|
955
|
+
if (i > 0) {
|
|
956
|
+
const prev = simplified[i - 1];
|
|
957
|
+
rasterizeCapsuleSegment(prev.x, prev.y, p.x, p.y, radius, def, setCell);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function rasterizeDisc(cx, cy, radius, def, setCell) {
|
|
962
|
+
const minX = Math.max(def.bounds.x, cx - radius);
|
|
963
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, cx + radius);
|
|
964
|
+
const minY = Math.max(def.bounds.y, cy - radius);
|
|
965
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, cy + radius);
|
|
966
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
967
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
968
|
+
const r2 = radius * radius;
|
|
969
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
970
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
971
|
+
const cellCenterX = wx + def.cellSize * 0.5;
|
|
972
|
+
const cellCenterY = wy + def.cellSize * 0.5;
|
|
973
|
+
const dx = cellCenterX - cx;
|
|
974
|
+
const dy = cellCenterY - cy;
|
|
975
|
+
if (dx * dx + dy * dy <= r2) {
|
|
976
|
+
setCell(wx, wy);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function rasterizeCapsuleSegment(x1, y1, x2, y2, radius, def, setCell) {
|
|
982
|
+
const segDx = x2 - x1;
|
|
983
|
+
const segDy = y2 - y1;
|
|
984
|
+
const segLen2 = segDx * segDx + segDy * segDy;
|
|
985
|
+
if (segLen2 === 0) return;
|
|
986
|
+
const minX = Math.max(def.bounds.x, Math.min(x1, x2) - radius);
|
|
987
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, Math.max(x1, x2) + radius);
|
|
988
|
+
const minY = Math.max(def.bounds.y, Math.min(y1, y2) - radius);
|
|
989
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, Math.max(y1, y2) + radius);
|
|
990
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
991
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
992
|
+
const r2 = radius * radius;
|
|
993
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
994
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
995
|
+
const cellCenterX = wx + def.cellSize * 0.5;
|
|
996
|
+
const cellCenterY = wy + def.cellSize * 0.5;
|
|
997
|
+
const t = Math.max(
|
|
998
|
+
0,
|
|
999
|
+
Math.min(1, ((cellCenterX - x1) * segDx + (cellCenterY - y1) * segDy) / segLen2)
|
|
1000
|
+
);
|
|
1001
|
+
const projX = x1 + t * segDx;
|
|
1002
|
+
const projY = y1 + t * segDy;
|
|
1003
|
+
const dx = cellCenterX - projX;
|
|
1004
|
+
const dy = cellCenterY - projY;
|
|
1005
|
+
if (dx * dx + dy * dy <= r2) {
|
|
1006
|
+
setCell(wx, wy);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function rasterizeRectangle(from, to, def, setCell) {
|
|
1012
|
+
const minX = Math.max(def.bounds.x, Math.min(from.x, to.x));
|
|
1013
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, Math.max(from.x, to.x));
|
|
1014
|
+
const minY = Math.max(def.bounds.y, Math.min(from.y, to.y));
|
|
1015
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, Math.max(from.y, to.y));
|
|
1016
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
1017
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
1018
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
1019
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
1020
|
+
setCell(wx, wy);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function rasterizePolygon(points, def, setCell) {
|
|
1025
|
+
if (points.length < 3) return;
|
|
1026
|
+
let minX = Infinity;
|
|
1027
|
+
let maxX = -Infinity;
|
|
1028
|
+
let minY = Infinity;
|
|
1029
|
+
let maxY = -Infinity;
|
|
1030
|
+
for (const p of points) {
|
|
1031
|
+
if (p.x < minX) minX = p.x;
|
|
1032
|
+
if (p.x > maxX) maxX = p.x;
|
|
1033
|
+
if (p.y < minY) minY = p.y;
|
|
1034
|
+
if (p.y > maxY) maxY = p.y;
|
|
1035
|
+
}
|
|
1036
|
+
minX = Math.max(def.bounds.x, minX);
|
|
1037
|
+
maxX = Math.min(def.bounds.x + def.bounds.w - 1, maxX);
|
|
1038
|
+
minY = Math.max(def.bounds.y, minY);
|
|
1039
|
+
maxY = Math.min(def.bounds.y + def.bounds.h - 1, maxY);
|
|
1040
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
1041
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
1042
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
1043
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
1044
|
+
const cx = wx + def.cellSize * 0.5;
|
|
1045
|
+
const cy = wy + def.cellSize * 0.5;
|
|
1046
|
+
if (pointInPolygon(cx, cy, points)) {
|
|
1047
|
+
setCell(wx, wy);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function pointInPolygon(x, y, polygon) {
|
|
1053
|
+
let inside = false;
|
|
1054
|
+
const n2 = polygon.length;
|
|
1055
|
+
for (let i = 0, j = n2 - 1; i < n2; j = i++) {
|
|
1056
|
+
const pi = polygon[i];
|
|
1057
|
+
const pj = polygon[j];
|
|
1058
|
+
if (pi.y > y !== pj.y > y && x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x) {
|
|
1059
|
+
inside = !inside;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return inside;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
595
1065
|
// src/core/state-serializer.ts
|
|
596
|
-
var CURRENT_VERSION =
|
|
1066
|
+
var CURRENT_VERSION = 3;
|
|
597
1067
|
var ELEMENT_TYPES = [
|
|
598
1068
|
"stroke",
|
|
599
1069
|
"note",
|
|
@@ -605,7 +1075,7 @@ var ELEMENT_TYPES = [
|
|
|
605
1075
|
"grid",
|
|
606
1076
|
"template"
|
|
607
1077
|
];
|
|
608
|
-
function exportState(elements, camera, layers = [], activeLayerId) {
|
|
1078
|
+
function exportState(elements, camera, layers = [], activeLayerId, fog) {
|
|
609
1079
|
const state = {
|
|
610
1080
|
version: CURRENT_VERSION,
|
|
611
1081
|
camera: {
|
|
@@ -622,6 +1092,7 @@ function exportState(elements, camera, layers = [], activeLayerId) {
|
|
|
622
1092
|
layers: layers.map((l) => ({ ...l }))
|
|
623
1093
|
};
|
|
624
1094
|
if (activeLayerId) state.activeLayerId = activeLayerId;
|
|
1095
|
+
if (fog) state.fog = structuredClone(fog);
|
|
625
1096
|
return state;
|
|
626
1097
|
}
|
|
627
1098
|
function parseState(json) {
|
|
@@ -699,6 +1170,9 @@ function validateState(data) {
|
|
|
699
1170
|
}
|
|
700
1171
|
}
|
|
701
1172
|
cleanBindings(elements);
|
|
1173
|
+
if (obj["fog"] !== void 0 && obj["fog"] !== null) {
|
|
1174
|
+
validateFogState(obj["fog"]);
|
|
1175
|
+
}
|
|
702
1176
|
}
|
|
703
1177
|
function validateElement(el) {
|
|
704
1178
|
if (!isRecord(el)) {
|
|
@@ -844,12 +1318,14 @@ var AutoSave = class {
|
|
|
844
1318
|
this.key = options.key ?? DEFAULT_KEY;
|
|
845
1319
|
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
846
1320
|
this.layerManager = options.layerManager;
|
|
1321
|
+
this.fogManager = options.fogManager;
|
|
847
1322
|
this.adapter = options.adapter ?? new LocalStorageAdapter();
|
|
848
1323
|
this.onError = options.onError;
|
|
849
1324
|
}
|
|
850
1325
|
key;
|
|
851
1326
|
debounceMs;
|
|
852
1327
|
layerManager;
|
|
1328
|
+
fogManager;
|
|
853
1329
|
adapter;
|
|
854
1330
|
timerId = null;
|
|
855
1331
|
unsubscribers = [];
|
|
@@ -867,6 +1343,9 @@ var AutoSave = class {
|
|
|
867
1343
|
if (this.layerManager) {
|
|
868
1344
|
this.unsubscribers.push(this.layerManager.on("change", schedule));
|
|
869
1345
|
}
|
|
1346
|
+
if (this.fogManager) {
|
|
1347
|
+
this.unsubscribers.push(this.fogManager.on("change", schedule));
|
|
1348
|
+
}
|
|
870
1349
|
}
|
|
871
1350
|
stop() {
|
|
872
1351
|
this.cancelPending();
|
|
@@ -903,7 +1382,8 @@ var AutoSave = class {
|
|
|
903
1382
|
this.saving = true;
|
|
904
1383
|
try {
|
|
905
1384
|
const layers = this.layerManager?.snapshot() ?? [];
|
|
906
|
-
const
|
|
1385
|
+
const fog = this.fogManager?.getState() ?? void 0;
|
|
1386
|
+
const state = exportState(this.store.snapshot(), this.camera, layers, void 0, fog);
|
|
907
1387
|
await this.adapter.save(this.key, JSON.stringify(state));
|
|
908
1388
|
} catch (e) {
|
|
909
1389
|
this.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -5235,6 +5715,8 @@ var MinimapController = class {
|
|
|
5235
5715
|
// read the `viewport` parameter. Assigned in the constructor body instead.
|
|
5236
5716
|
htmlPainters;
|
|
5237
5717
|
scene = null;
|
|
5718
|
+
fogRenderer = null;
|
|
5719
|
+
fogUnsub = null;
|
|
5238
5720
|
frameId = null;
|
|
5239
5721
|
debounceTimer = null;
|
|
5240
5722
|
dragging = false;
|
|
@@ -5249,6 +5731,15 @@ var MinimapController = class {
|
|
|
5249
5731
|
this.renderScene();
|
|
5250
5732
|
this.requestDraw();
|
|
5251
5733
|
}
|
|
5734
|
+
setFogRenderer(renderer) {
|
|
5735
|
+
if (this.disposed) return;
|
|
5736
|
+
if (this.fogUnsub) {
|
|
5737
|
+
this.fogUnsub();
|
|
5738
|
+
this.fogUnsub = null;
|
|
5739
|
+
}
|
|
5740
|
+
this.fogRenderer = renderer;
|
|
5741
|
+
this.invalidateScene();
|
|
5742
|
+
}
|
|
5252
5743
|
requestDraw() {
|
|
5253
5744
|
if (this.disposed || this.frameId !== null) return;
|
|
5254
5745
|
this.frameId = this.requestFrame(this.draw);
|
|
@@ -5310,8 +5801,15 @@ var MinimapController = class {
|
|
|
5310
5801
|
}
|
|
5311
5802
|
currentMapping() {
|
|
5312
5803
|
const viewportRect = this.viewport.getVisibleRect();
|
|
5313
|
-
|
|
5314
|
-
|
|
5804
|
+
let mapping = getElementsBoundingBox(this.sceneElements());
|
|
5805
|
+
mapping = mapping ? unionBounds(mapping, viewportRect) : viewportRect;
|
|
5806
|
+
if (this.fogRenderer?.isVisible()) {
|
|
5807
|
+
const fogState = this.fogRenderer.getState();
|
|
5808
|
+
if (fogState) {
|
|
5809
|
+
mapping = unionBounds(mapping, fogState.definition.bounds);
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
return mapping;
|
|
5315
5813
|
}
|
|
5316
5814
|
onViewChanged() {
|
|
5317
5815
|
if (this.disposed) return;
|
|
@@ -5369,6 +5867,23 @@ var MinimapController = class {
|
|
|
5369
5867
|
ctx.drawImage(layerCanvas, 0, 0);
|
|
5370
5868
|
ctx.restore();
|
|
5371
5869
|
}
|
|
5870
|
+
if (this.fogRenderer?.isVisible()) {
|
|
5871
|
+
const fogState = this.fogRenderer.getState();
|
|
5872
|
+
const fogMode = this.fogRenderer.getViewMode();
|
|
5873
|
+
if (fogState && (fogMode === "editor" || fogMode === "player")) {
|
|
5874
|
+
ctx.save();
|
|
5875
|
+
ctx.setTransform(
|
|
5876
|
+
dpr * transform.scale,
|
|
5877
|
+
0,
|
|
5878
|
+
0,
|
|
5879
|
+
dpr * transform.scale,
|
|
5880
|
+
dpr * transform.offsetX,
|
|
5881
|
+
dpr * transform.offsetY
|
|
5882
|
+
);
|
|
5883
|
+
this.fogRenderer.renderForExport(ctx, fogState, fogMode);
|
|
5884
|
+
ctx.restore();
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5372
5887
|
this.scene = { canvas: sceneCanvas, transform, mapping };
|
|
5373
5888
|
}
|
|
5374
5889
|
renderLayerElements(ctx, elements, t, dpr) {
|
|
@@ -5481,9 +5996,15 @@ var Minimap = class {
|
|
|
5481
5996
|
this.canvas = canvas;
|
|
5482
5997
|
this.controller = new MinimapController(viewport, canvas, { width: WIDTH, height: HEIGHT });
|
|
5483
5998
|
}
|
|
5999
|
+
setFogRenderer(renderer) {
|
|
6000
|
+
this.controller.setFogRenderer(renderer);
|
|
6001
|
+
}
|
|
5484
6002
|
scheduleDraw() {
|
|
5485
6003
|
this.controller.requestDraw();
|
|
5486
6004
|
}
|
|
6005
|
+
invalidateScene() {
|
|
6006
|
+
this.controller.invalidateScene();
|
|
6007
|
+
}
|
|
5487
6008
|
destroy() {
|
|
5488
6009
|
this.controller.dispose();
|
|
5489
6010
|
this.canvas.remove();
|
|
@@ -6805,6 +7326,548 @@ async function renderHtmlElements(elements, options) {
|
|
|
6805
7326
|
return sources;
|
|
6806
7327
|
}
|
|
6807
7328
|
|
|
7329
|
+
// src/fog/fog-style.ts
|
|
7330
|
+
var DEFAULT_PROCEDURAL_OPACITY = 0.6;
|
|
7331
|
+
var DEFAULT_PROCEDURAL_SCALE = 256;
|
|
7332
|
+
var DEFAULT_PROCEDURAL_SEED = 0;
|
|
7333
|
+
var DEFAULT_PROCEDURAL_DETAIL = 2;
|
|
7334
|
+
var DEFAULT_PROCEDURAL_TINT = "#ffffff";
|
|
7335
|
+
var MIN_SCALE = 64;
|
|
7336
|
+
var MAX_SCALE = 1024;
|
|
7337
|
+
var MIN_DETAIL = 1;
|
|
7338
|
+
var MAX_DETAIL = 4;
|
|
7339
|
+
var MAX_SEED = 65535;
|
|
7340
|
+
function clamp(value, min, max) {
|
|
7341
|
+
return Math.max(min, Math.min(max, value));
|
|
7342
|
+
}
|
|
7343
|
+
function finiteOrDefault(value, defaultValue) {
|
|
7344
|
+
return value !== void 0 && Number.isFinite(value) ? value : defaultValue;
|
|
7345
|
+
}
|
|
7346
|
+
function resolveFogStyle(style, legacyColor, defaultColor) {
|
|
7347
|
+
if (style && style.kind === "procedural") {
|
|
7348
|
+
const opacity = clamp(finiteOrDefault(style.opacity, DEFAULT_PROCEDURAL_OPACITY), 0, 1);
|
|
7349
|
+
const scale = clamp(
|
|
7350
|
+
finiteOrDefault(style.scale, DEFAULT_PROCEDURAL_SCALE),
|
|
7351
|
+
MIN_SCALE,
|
|
7352
|
+
MAX_SCALE
|
|
7353
|
+
);
|
|
7354
|
+
const seed = clamp(
|
|
7355
|
+
Math.floor(finiteOrDefault(style.seed, DEFAULT_PROCEDURAL_SEED)),
|
|
7356
|
+
0,
|
|
7357
|
+
MAX_SEED
|
|
7358
|
+
);
|
|
7359
|
+
const detail = clamp(
|
|
7360
|
+
Math.floor(finiteOrDefault(style.detail, DEFAULT_PROCEDURAL_DETAIL)),
|
|
7361
|
+
MIN_DETAIL,
|
|
7362
|
+
MAX_DETAIL
|
|
7363
|
+
);
|
|
7364
|
+
return {
|
|
7365
|
+
kind: "procedural",
|
|
7366
|
+
backdrop: style.backdrop,
|
|
7367
|
+
// `tint` is required for typed callers. Keep a visible runtime fallback for
|
|
7368
|
+
// untyped/older JavaScript hosts instead of producing a flat same-color overlay.
|
|
7369
|
+
tint: style.tint || DEFAULT_PROCEDURAL_TINT,
|
|
7370
|
+
opacity,
|
|
7371
|
+
scale,
|
|
7372
|
+
seed,
|
|
7373
|
+
detail
|
|
7374
|
+
};
|
|
7375
|
+
}
|
|
7376
|
+
if (style && (!style.kind || style.kind === "solid")) {
|
|
7377
|
+
return { kind: "solid", color: style.color };
|
|
7378
|
+
}
|
|
7379
|
+
return { kind: "solid", color: legacyColor ?? defaultColor };
|
|
7380
|
+
}
|
|
7381
|
+
|
|
7382
|
+
// src/fog/fog-procedural-tile.ts
|
|
7383
|
+
var TILE_PX = 128;
|
|
7384
|
+
function xorshift32(state) {
|
|
7385
|
+
state ^= state << 13;
|
|
7386
|
+
state ^= state >>> 17;
|
|
7387
|
+
state ^= state << 5;
|
|
7388
|
+
return state >>> 0;
|
|
7389
|
+
}
|
|
7390
|
+
function seedState(seed) {
|
|
7391
|
+
return seed * 2654435761 + 1 >>> 0 || 1;
|
|
7392
|
+
}
|
|
7393
|
+
function smoothstep(t) {
|
|
7394
|
+
return t * t * (3 - 2 * t);
|
|
7395
|
+
}
|
|
7396
|
+
function lerp(a, b, t) {
|
|
7397
|
+
return a + (b - a) * t;
|
|
7398
|
+
}
|
|
7399
|
+
function generateGradients(size, prngState) {
|
|
7400
|
+
const count = size * size;
|
|
7401
|
+
const gx = new Float32Array(count);
|
|
7402
|
+
const gy = new Float32Array(count);
|
|
7403
|
+
let s = prngState;
|
|
7404
|
+
for (let i = 0; i < count; i++) {
|
|
7405
|
+
s = xorshift32(s);
|
|
7406
|
+
const angle = (s >>> 0) / 4294967296 * Math.PI * 2;
|
|
7407
|
+
gx[i] = Math.cos(angle);
|
|
7408
|
+
gy[i] = Math.sin(angle);
|
|
7409
|
+
}
|
|
7410
|
+
return { gx, gy, state: s };
|
|
7411
|
+
}
|
|
7412
|
+
function perlinNoise(px, py, gridSize, gx, gy) {
|
|
7413
|
+
const gx0 = Math.floor(px) % gridSize;
|
|
7414
|
+
const gy0 = Math.floor(py) % gridSize;
|
|
7415
|
+
const gx1 = (gx0 + 1) % gridSize;
|
|
7416
|
+
const gy1 = (gy0 + 1) % gridSize;
|
|
7417
|
+
const fx = px - Math.floor(px);
|
|
7418
|
+
const fy = py - Math.floor(py);
|
|
7419
|
+
const sx = smoothstep(fx);
|
|
7420
|
+
const sy = smoothstep(fy);
|
|
7421
|
+
const dot = (ix, iy, dx, dy) => {
|
|
7422
|
+
const idx = iy * gridSize + ix;
|
|
7423
|
+
return gx[idx] * dx + gy[idx] * dy;
|
|
7424
|
+
};
|
|
7425
|
+
const n00 = dot(gx0, gy0, fx, fy);
|
|
7426
|
+
const n10 = dot(gx1, gy0, fx - 1, fy);
|
|
7427
|
+
const n01 = dot(gx0, gy1, fx, fy - 1);
|
|
7428
|
+
const n11 = dot(gx1, gy1, fx - 1, fy - 1);
|
|
7429
|
+
return lerp(lerp(n00, n10, sx), lerp(n01, n11, sx), sy);
|
|
7430
|
+
}
|
|
7431
|
+
function layeredNoise(x, y, octaves, gridSize, gx, gy) {
|
|
7432
|
+
let value = 0;
|
|
7433
|
+
let amplitude = 1;
|
|
7434
|
+
let frequency = 1;
|
|
7435
|
+
let maxAmplitude = 0;
|
|
7436
|
+
for (let o = 0; o < octaves; o++) {
|
|
7437
|
+
value += perlinNoise(x * frequency, y * frequency, gridSize * frequency, gx, gy) * amplitude;
|
|
7438
|
+
maxAmplitude += amplitude;
|
|
7439
|
+
amplitude *= 0.5;
|
|
7440
|
+
frequency *= 2;
|
|
7441
|
+
}
|
|
7442
|
+
return (value / maxAmplitude + 1) * 0.5;
|
|
7443
|
+
}
|
|
7444
|
+
function generateProceduralTile(style) {
|
|
7445
|
+
const gridSize = 8;
|
|
7446
|
+
const maxFreq = gridSize * (1 << style.detail - 1);
|
|
7447
|
+
const { gx, gy } = generateGradients(maxFreq, seedState(style.seed));
|
|
7448
|
+
const data = new Uint8ClampedArray(TILE_PX * TILE_PX * 4);
|
|
7449
|
+
for (let py = 0; py < TILE_PX; py++) {
|
|
7450
|
+
for (let px = 0; px < TILE_PX; px++) {
|
|
7451
|
+
const nx = px / TILE_PX * gridSize;
|
|
7452
|
+
const ny = py / TILE_PX * gridSize;
|
|
7453
|
+
const n2 = layeredNoise(nx, ny, style.detail, gridSize, gx, gy);
|
|
7454
|
+
const alpha = Math.round(n2 * style.opacity * 255);
|
|
7455
|
+
const idx = (py * TILE_PX + px) * 4;
|
|
7456
|
+
data[idx] = 255;
|
|
7457
|
+
data[idx + 1] = 255;
|
|
7458
|
+
data[idx + 2] = 255;
|
|
7459
|
+
data[idx + 3] = alpha;
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
return { data, width: TILE_PX, height: TILE_PX };
|
|
7463
|
+
}
|
|
7464
|
+
var tileCache = /* @__PURE__ */ new Map();
|
|
7465
|
+
var MAX_CACHED_TILES = 16;
|
|
7466
|
+
function getCachedProceduralTile(style) {
|
|
7467
|
+
const key = `${style.opacity}\0${style.seed}\0${style.detail}`;
|
|
7468
|
+
const cached = tileCache.get(key);
|
|
7469
|
+
if (cached) return cached;
|
|
7470
|
+
const tile = generateProceduralTile(style);
|
|
7471
|
+
if (tileCache.size >= MAX_CACHED_TILES) {
|
|
7472
|
+
const oldest = tileCache.keys().next().value;
|
|
7473
|
+
if (oldest !== void 0) tileCache.delete(oldest);
|
|
7474
|
+
}
|
|
7475
|
+
tileCache.set(key, tile);
|
|
7476
|
+
return tile;
|
|
7477
|
+
}
|
|
7478
|
+
function clearProceduralTileCache() {
|
|
7479
|
+
tileCache.clear();
|
|
7480
|
+
}
|
|
7481
|
+
|
|
7482
|
+
// src/fog/fog-renderer.ts
|
|
7483
|
+
var DEFAULT_EDITOR_COLOR = "rgba(30, 40, 60, 0.45)";
|
|
7484
|
+
var DEFAULT_PLAYER_COLOR = "#0b1020";
|
|
7485
|
+
var FogRenderer = class {
|
|
7486
|
+
tileCache = /* @__PURE__ */ new Map();
|
|
7487
|
+
patternCache = /* @__PURE__ */ new Map();
|
|
7488
|
+
state = null;
|
|
7489
|
+
viewMode = "off";
|
|
7490
|
+
dirty = true;
|
|
7491
|
+
editorStyle;
|
|
7492
|
+
playerStyle;
|
|
7493
|
+
constructor(options = {}) {
|
|
7494
|
+
this.editorStyle = resolveFogStyle(
|
|
7495
|
+
options.editorStyle,
|
|
7496
|
+
options.editorColor,
|
|
7497
|
+
DEFAULT_EDITOR_COLOR
|
|
7498
|
+
);
|
|
7499
|
+
this.playerStyle = resolveFogStyle(
|
|
7500
|
+
options.playerStyle,
|
|
7501
|
+
options.playerColor,
|
|
7502
|
+
DEFAULT_PLAYER_COLOR
|
|
7503
|
+
);
|
|
7504
|
+
}
|
|
7505
|
+
setState(state) {
|
|
7506
|
+
this.state = state;
|
|
7507
|
+
this.dirty = true;
|
|
7508
|
+
}
|
|
7509
|
+
setViewMode(mode) {
|
|
7510
|
+
if (mode === this.viewMode) return;
|
|
7511
|
+
this.viewMode = mode;
|
|
7512
|
+
this.dirty = true;
|
|
7513
|
+
}
|
|
7514
|
+
getState() {
|
|
7515
|
+
return this.state;
|
|
7516
|
+
}
|
|
7517
|
+
getViewMode() {
|
|
7518
|
+
return this.viewMode;
|
|
7519
|
+
}
|
|
7520
|
+
markDirty() {
|
|
7521
|
+
this.dirty = true;
|
|
7522
|
+
}
|
|
7523
|
+
isDirty() {
|
|
7524
|
+
return this.dirty;
|
|
7525
|
+
}
|
|
7526
|
+
isVisible() {
|
|
7527
|
+
return this.viewMode !== "off" && this.state !== null;
|
|
7528
|
+
}
|
|
7529
|
+
getResolvedStyle(mode) {
|
|
7530
|
+
return mode === "editor" ? this.editorStyle : this.playerStyle;
|
|
7531
|
+
}
|
|
7532
|
+
render(ctx, camera, viewportWidth, viewportHeight, _dpr) {
|
|
7533
|
+
if (!this.state || this.viewMode === "off") return;
|
|
7534
|
+
const def = this.state.definition;
|
|
7535
|
+
const cellSize = def.cellSize;
|
|
7536
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
7537
|
+
const mode = this.viewMode === "editor" ? "editor" : "player";
|
|
7538
|
+
const style = this.getResolvedStyle(mode);
|
|
7539
|
+
const proceduralStyle = style.kind === "procedural" ? style : null;
|
|
7540
|
+
const color = style.kind === "procedural" ? normalizeCanvasColor(
|
|
7541
|
+
ctx,
|
|
7542
|
+
style.backdrop,
|
|
7543
|
+
mode === "player" ? DEFAULT_PLAYER_COLOR : DEFAULT_EDITOR_COLOR
|
|
7544
|
+
) : style.color;
|
|
7545
|
+
const safetyColor = proceduralStyle && mode === "player" ? DEFAULT_PLAYER_COLOR : null;
|
|
7546
|
+
const worldBounds = getVisibleWorld(camera, viewportWidth, viewportHeight);
|
|
7547
|
+
const minTX = Math.floor(Math.max(def.bounds.x, worldBounds.x) / tileWorldSize);
|
|
7548
|
+
const minTY = Math.floor(Math.max(def.bounds.y, worldBounds.y) / tileWorldSize);
|
|
7549
|
+
const maxTX = Math.floor(
|
|
7550
|
+
Math.min(def.bounds.x + def.bounds.w - 1, worldBounds.x + worldBounds.w) / tileWorldSize
|
|
7551
|
+
);
|
|
7552
|
+
const maxTY = Math.floor(
|
|
7553
|
+
Math.min(def.bounds.y + def.bounds.h - 1, worldBounds.y + worldBounds.h) / tileWorldSize
|
|
7554
|
+
);
|
|
7555
|
+
ctx.save();
|
|
7556
|
+
ctx.translate(camera.position.x, camera.position.y);
|
|
7557
|
+
ctx.scale(camera.zoom, camera.zoom);
|
|
7558
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
7559
|
+
for (const tile of this.state.tiles) {
|
|
7560
|
+
tileMap.set(`${tile.x},${tile.y}`, tile.data);
|
|
7561
|
+
}
|
|
7562
|
+
const baseCovered = def.base === "covered";
|
|
7563
|
+
for (let ty = minTY; ty <= maxTY; ty++) {
|
|
7564
|
+
for (let tx = minTX; tx <= maxTX; tx++) {
|
|
7565
|
+
const key = `${tx},${ty}`;
|
|
7566
|
+
const data = tileMap.get(key);
|
|
7567
|
+
const tileWorldX = tx * tileWorldSize;
|
|
7568
|
+
const tileWorldY = ty * tileWorldSize;
|
|
7569
|
+
if (!data && baseCovered) {
|
|
7570
|
+
ctx.fillStyle = color;
|
|
7571
|
+
const clipX = Math.max(tileWorldX, def.bounds.x);
|
|
7572
|
+
const clipY = Math.max(tileWorldY, def.bounds.y);
|
|
7573
|
+
const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
|
|
7574
|
+
const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
|
|
7575
|
+
if (clipR > clipX && clipB > clipY) {
|
|
7576
|
+
if (safetyColor) {
|
|
7577
|
+
ctx.fillStyle = safetyColor;
|
|
7578
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7579
|
+
}
|
|
7580
|
+
ctx.fillStyle = color;
|
|
7581
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7582
|
+
if (proceduralStyle) {
|
|
7583
|
+
this.paintProceduralOverlay(
|
|
7584
|
+
ctx,
|
|
7585
|
+
proceduralStyle,
|
|
7586
|
+
clipX,
|
|
7587
|
+
clipY,
|
|
7588
|
+
clipR - clipX,
|
|
7589
|
+
clipB - clipY
|
|
7590
|
+
);
|
|
7591
|
+
}
|
|
7592
|
+
}
|
|
7593
|
+
continue;
|
|
7594
|
+
}
|
|
7595
|
+
if (!data && !baseCovered) {
|
|
7596
|
+
continue;
|
|
7597
|
+
}
|
|
7598
|
+
if (data) {
|
|
7599
|
+
if (safetyColor) this.renderTile(ctx, data, tx, ty, def, safetyColor);
|
|
7600
|
+
this.renderTile(ctx, data, tx, ty, def, color);
|
|
7601
|
+
if (proceduralStyle) {
|
|
7602
|
+
this.renderTileProceduralOverlay(ctx, data, tx, ty, def, proceduralStyle);
|
|
7603
|
+
}
|
|
7604
|
+
}
|
|
7605
|
+
}
|
|
7606
|
+
}
|
|
7607
|
+
ctx.restore();
|
|
7608
|
+
this.dirty = false;
|
|
7609
|
+
}
|
|
7610
|
+
renderForExport(ctx, state, mode, color, style) {
|
|
7611
|
+
const def = state.definition;
|
|
7612
|
+
const cellSize = def.cellSize;
|
|
7613
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
7614
|
+
const resolved = style ? resolveFogStyle(
|
|
7615
|
+
style,
|
|
7616
|
+
void 0,
|
|
7617
|
+
mode === "editor" ? DEFAULT_EDITOR_COLOR : DEFAULT_PLAYER_COLOR
|
|
7618
|
+
) : this.getResolvedStyle(mode);
|
|
7619
|
+
const proceduralStyle = !color && resolved.kind === "procedural" ? resolved : null;
|
|
7620
|
+
const fogColor = color ?? (proceduralStyle ? normalizeCanvasColor(
|
|
7621
|
+
ctx,
|
|
7622
|
+
proceduralStyle.backdrop,
|
|
7623
|
+
mode === "player" ? DEFAULT_PLAYER_COLOR : DEFAULT_EDITOR_COLOR
|
|
7624
|
+
) : resolved.kind === "solid" ? resolved.color : resolved.backdrop);
|
|
7625
|
+
const safetyColor = proceduralStyle && mode === "player" ? DEFAULT_PLAYER_COLOR : null;
|
|
7626
|
+
const baseCovered = def.base === "covered";
|
|
7627
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
7628
|
+
for (const tile of state.tiles) {
|
|
7629
|
+
tileMap.set(`${tile.x},${tile.y}`, tile.data);
|
|
7630
|
+
}
|
|
7631
|
+
const minTX = Math.floor(def.bounds.x / tileWorldSize);
|
|
7632
|
+
const minTY = Math.floor(def.bounds.y / tileWorldSize);
|
|
7633
|
+
const maxTX = Math.floor((def.bounds.x + def.bounds.w - 1) / tileWorldSize);
|
|
7634
|
+
const maxTY = Math.floor((def.bounds.y + def.bounds.h - 1) / tileWorldSize);
|
|
7635
|
+
for (let ty = minTY; ty <= maxTY; ty++) {
|
|
7636
|
+
for (let tx = minTX; tx <= maxTX; tx++) {
|
|
7637
|
+
const key = `${tx},${ty}`;
|
|
7638
|
+
const data = tileMap.get(key);
|
|
7639
|
+
const tileWorldX = tx * tileWorldSize;
|
|
7640
|
+
const tileWorldY = ty * tileWorldSize;
|
|
7641
|
+
if (!data && baseCovered) {
|
|
7642
|
+
ctx.fillStyle = fogColor;
|
|
7643
|
+
const clipX = Math.max(tileWorldX, def.bounds.x);
|
|
7644
|
+
const clipY = Math.max(tileWorldY, def.bounds.y);
|
|
7645
|
+
const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
|
|
7646
|
+
const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
|
|
7647
|
+
if (clipR > clipX && clipB > clipY) {
|
|
7648
|
+
if (safetyColor) {
|
|
7649
|
+
ctx.fillStyle = safetyColor;
|
|
7650
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7651
|
+
}
|
|
7652
|
+
ctx.fillStyle = fogColor;
|
|
7653
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7654
|
+
if (proceduralStyle) {
|
|
7655
|
+
this.paintProceduralOverlay(
|
|
7656
|
+
ctx,
|
|
7657
|
+
proceduralStyle,
|
|
7658
|
+
clipX,
|
|
7659
|
+
clipY,
|
|
7660
|
+
clipR - clipX,
|
|
7661
|
+
clipB - clipY
|
|
7662
|
+
);
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
continue;
|
|
7666
|
+
}
|
|
7667
|
+
if (data) {
|
|
7668
|
+
if (safetyColor) this.renderTileForExport(ctx, data, tx, ty, def, safetyColor);
|
|
7669
|
+
this.renderTileForExport(ctx, data, tx, ty, def, fogColor);
|
|
7670
|
+
if (proceduralStyle) {
|
|
7671
|
+
this.renderTileProceduralOverlay(ctx, data, tx, ty, def, proceduralStyle);
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
}
|
|
7676
|
+
}
|
|
7677
|
+
dispose() {
|
|
7678
|
+
this.tileCache.clear();
|
|
7679
|
+
this.patternCache.clear();
|
|
7680
|
+
clearProceduralTileCache();
|
|
7681
|
+
this.state = null;
|
|
7682
|
+
}
|
|
7683
|
+
getOrCreatePattern(ctx, style, worldScale) {
|
|
7684
|
+
const key = `${style.backdrop}\0${style.tint}\0${style.opacity}\0${style.scale}\0${style.seed}\0${style.detail}\0${worldScale}`;
|
|
7685
|
+
const cached = this.patternCache.get(key);
|
|
7686
|
+
if (cached !== void 0) return cached;
|
|
7687
|
+
let pattern = null;
|
|
7688
|
+
try {
|
|
7689
|
+
const tileData = getCachedProceduralTile(style);
|
|
7690
|
+
pattern = this.createPatternFromTileData(ctx, tileData, style, worldScale);
|
|
7691
|
+
} catch {
|
|
7692
|
+
}
|
|
7693
|
+
if (this.patternCache.size >= 32) {
|
|
7694
|
+
const oldest = this.patternCache.keys().next().value;
|
|
7695
|
+
if (oldest !== void 0) this.patternCache.delete(oldest);
|
|
7696
|
+
}
|
|
7697
|
+
this.patternCache.set(key, pattern);
|
|
7698
|
+
return pattern;
|
|
7699
|
+
}
|
|
7700
|
+
createPatternFromTileData(ctx, tileData, style, worldScale) {
|
|
7701
|
+
if (typeof document === "undefined") return null;
|
|
7702
|
+
const sourceCanvas = document.createElement("canvas");
|
|
7703
|
+
sourceCanvas.width = tileData.width;
|
|
7704
|
+
sourceCanvas.height = tileData.height;
|
|
7705
|
+
const sourceCtx = sourceCanvas.getContext("2d");
|
|
7706
|
+
if (!sourceCtx) return null;
|
|
7707
|
+
const imageData = new ImageData(
|
|
7708
|
+
new Uint8ClampedArray(tileData.data),
|
|
7709
|
+
tileData.width,
|
|
7710
|
+
tileData.height
|
|
7711
|
+
);
|
|
7712
|
+
sourceCtx.putImageData(imageData, 0, 0);
|
|
7713
|
+
sourceCtx.globalCompositeOperation = "source-in";
|
|
7714
|
+
sourceCtx.fillStyle = normalizeCanvasColor(sourceCtx, style.tint, "#ffffff");
|
|
7715
|
+
sourceCtx.fillRect(0, 0, tileData.width, tileData.height);
|
|
7716
|
+
sourceCtx.globalCompositeOperation = "source-over";
|
|
7717
|
+
const patternScale = style.scale * worldScale / tileData.width;
|
|
7718
|
+
const pattern = ctx.createPattern(sourceCanvas, "repeat");
|
|
7719
|
+
if (pattern && typeof pattern.setTransform === "function" && typeof DOMMatrix !== "undefined") {
|
|
7720
|
+
try {
|
|
7721
|
+
pattern.setTransform(new DOMMatrix([patternScale, 0, 0, patternScale, 0, 0]));
|
|
7722
|
+
return pattern;
|
|
7723
|
+
} catch {
|
|
7724
|
+
}
|
|
7725
|
+
}
|
|
7726
|
+
const patternPx = Math.round(style.scale * worldScale);
|
|
7727
|
+
if (patternPx < 1) return null;
|
|
7728
|
+
const patternCanvas = document.createElement("canvas");
|
|
7729
|
+
patternCanvas.width = patternPx;
|
|
7730
|
+
patternCanvas.height = patternPx;
|
|
7731
|
+
const patternCtx = patternCanvas.getContext("2d");
|
|
7732
|
+
if (!patternCtx) return null;
|
|
7733
|
+
patternCtx.drawImage(sourceCanvas, 0, 0, patternPx, patternPx);
|
|
7734
|
+
return ctx.createPattern(patternCanvas, "repeat");
|
|
7735
|
+
}
|
|
7736
|
+
paintProceduralOverlay(ctx, style, x, y, w, h) {
|
|
7737
|
+
const pattern = this.getOrCreatePattern(ctx, style, 1);
|
|
7738
|
+
if (!pattern) return;
|
|
7739
|
+
ctx.save();
|
|
7740
|
+
ctx.fillStyle = pattern;
|
|
7741
|
+
ctx.fillRect(x, y, w, h);
|
|
7742
|
+
ctx.restore();
|
|
7743
|
+
}
|
|
7744
|
+
renderTileProceduralOverlay(ctx, data, tx, ty, def, style) {
|
|
7745
|
+
const cellSize = def.cellSize;
|
|
7746
|
+
const tileWorldX = tx * FOG_TILE_CELLS * cellSize;
|
|
7747
|
+
const tileWorldY = ty * FOG_TILE_CELLS * cellSize;
|
|
7748
|
+
const pattern = this.getOrCreatePattern(ctx, style, 1);
|
|
7749
|
+
if (!pattern) return;
|
|
7750
|
+
const bytes = decodeBase64(data);
|
|
7751
|
+
ctx.save();
|
|
7752
|
+
ctx.fillStyle = pattern;
|
|
7753
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7754
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7755
|
+
const cellWorldX = tileWorldX + col * cellSize;
|
|
7756
|
+
const cellWorldY = tileWorldY + row * cellSize;
|
|
7757
|
+
if (cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= def.bounds.x + def.bounds.w || cellWorldY >= def.bounds.y + def.bounds.h) {
|
|
7758
|
+
continue;
|
|
7759
|
+
}
|
|
7760
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7761
|
+
const byteIndex = index >> 3;
|
|
7762
|
+
const bitIndex = 7 - (index & 7);
|
|
7763
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7764
|
+
if (!revealed) {
|
|
7765
|
+
ctx.fillRect(cellWorldX, cellWorldY, cellSize, cellSize);
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
}
|
|
7769
|
+
ctx.restore();
|
|
7770
|
+
}
|
|
7771
|
+
tileRaster(data, color) {
|
|
7772
|
+
const key = `${color}\0${data}`;
|
|
7773
|
+
const cached = this.tileCache.get(key);
|
|
7774
|
+
if (cached) return cached;
|
|
7775
|
+
if (typeof document === "undefined") return null;
|
|
7776
|
+
const canvas = document.createElement("canvas");
|
|
7777
|
+
canvas.width = FOG_TILE_CELLS;
|
|
7778
|
+
canvas.height = FOG_TILE_CELLS;
|
|
7779
|
+
const ctx = canvas.getContext("2d");
|
|
7780
|
+
if (!ctx) return null;
|
|
7781
|
+
const bytes = decodeBase64(data);
|
|
7782
|
+
ctx.fillStyle = color;
|
|
7783
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7784
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7785
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7786
|
+
const byteIndex = index >> 3;
|
|
7787
|
+
const bitIndex = 7 - (index & 7);
|
|
7788
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7789
|
+
if (!revealed) ctx.fillRect(col, row, 1, 1);
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
if (this.tileCache.size >= 256) {
|
|
7793
|
+
const oldest = this.tileCache.keys().next().value;
|
|
7794
|
+
if (oldest !== void 0) this.tileCache.delete(oldest);
|
|
7795
|
+
}
|
|
7796
|
+
this.tileCache.set(key, canvas);
|
|
7797
|
+
return canvas;
|
|
7798
|
+
}
|
|
7799
|
+
renderTile(ctx, data, tx, ty, def, color) {
|
|
7800
|
+
const cellSize = def.cellSize;
|
|
7801
|
+
const tileWorldX = tx * FOG_TILE_CELLS * cellSize;
|
|
7802
|
+
const tileWorldY = ty * FOG_TILE_CELLS * cellSize;
|
|
7803
|
+
const raster = this.tileRaster(data, color);
|
|
7804
|
+
if (raster) {
|
|
7805
|
+
ctx.save();
|
|
7806
|
+
ctx.beginPath();
|
|
7807
|
+
ctx.rect(def.bounds.x, def.bounds.y, def.bounds.w, def.bounds.h);
|
|
7808
|
+
ctx.clip();
|
|
7809
|
+
ctx.imageSmoothingEnabled = false;
|
|
7810
|
+
ctx.drawImage(
|
|
7811
|
+
raster,
|
|
7812
|
+
tileWorldX,
|
|
7813
|
+
tileWorldY,
|
|
7814
|
+
FOG_TILE_CELLS * cellSize,
|
|
7815
|
+
FOG_TILE_CELLS * cellSize
|
|
7816
|
+
);
|
|
7817
|
+
ctx.restore();
|
|
7818
|
+
return;
|
|
7819
|
+
}
|
|
7820
|
+
const bytes = decodeBase64(data);
|
|
7821
|
+
ctx.fillStyle = color;
|
|
7822
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7823
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7824
|
+
const cellWorldX = tileWorldX + col * cellSize;
|
|
7825
|
+
const cellWorldY = tileWorldY + row * cellSize;
|
|
7826
|
+
if (cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= def.bounds.x + def.bounds.w || cellWorldY >= def.bounds.y + def.bounds.h) {
|
|
7827
|
+
continue;
|
|
7828
|
+
}
|
|
7829
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7830
|
+
const byteIndex = index >> 3;
|
|
7831
|
+
const bitIndex = 7 - (index & 7);
|
|
7832
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7833
|
+
const covered = !revealed;
|
|
7834
|
+
if (covered) {
|
|
7835
|
+
ctx.fillRect(cellWorldX, cellWorldY, cellSize, cellSize);
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
}
|
|
7839
|
+
}
|
|
7840
|
+
renderTileForExport(ctx, data, tx, ty, def, color) {
|
|
7841
|
+
this.renderTile(ctx, data, tx, ty, def, color);
|
|
7842
|
+
}
|
|
7843
|
+
};
|
|
7844
|
+
function getVisibleWorld(camera, viewportWidth, viewportHeight) {
|
|
7845
|
+
const topLeft = camera.screenToWorld({ x: 0, y: 0 });
|
|
7846
|
+
const bottomRight = camera.screenToWorld({ x: viewportWidth, y: viewportHeight });
|
|
7847
|
+
return {
|
|
7848
|
+
x: topLeft.x,
|
|
7849
|
+
y: topLeft.y,
|
|
7850
|
+
w: bottomRight.x - topLeft.x,
|
|
7851
|
+
h: bottomRight.y - topLeft.y
|
|
7852
|
+
};
|
|
7853
|
+
}
|
|
7854
|
+
function normalizeCanvasColor(ctx, value, fallback) {
|
|
7855
|
+
const previous = ctx.fillStyle;
|
|
7856
|
+
try {
|
|
7857
|
+
ctx.fillStyle = "#010203";
|
|
7858
|
+
ctx.fillStyle = value;
|
|
7859
|
+
const first = ctx.fillStyle;
|
|
7860
|
+
ctx.fillStyle = "#040506";
|
|
7861
|
+
ctx.fillStyle = value;
|
|
7862
|
+
const second = ctx.fillStyle;
|
|
7863
|
+
return typeof first === "string" && first === second ? first : fallback;
|
|
7864
|
+
} catch {
|
|
7865
|
+
return fallback;
|
|
7866
|
+
} finally {
|
|
7867
|
+
ctx.fillStyle = previous;
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7870
|
+
|
|
6808
7871
|
// src/canvas/export-image.ts
|
|
6809
7872
|
var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
|
|
6810
7873
|
var DEFAULT_MAX_DIMENSION = 16384;
|
|
@@ -7217,6 +8280,16 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
7217
8280
|
renderGridForBounds(ctx, grid, bounds);
|
|
7218
8281
|
ctx.restore();
|
|
7219
8282
|
}
|
|
8283
|
+
if (options.fog) {
|
|
8284
|
+
const fogRenderer = new FogRenderer();
|
|
8285
|
+
fogRenderer.renderForExport(
|
|
8286
|
+
ctx,
|
|
8287
|
+
options.fog.state,
|
|
8288
|
+
options.fog.mode,
|
|
8289
|
+
options.fog.color,
|
|
8290
|
+
options.fog.style
|
|
8291
|
+
);
|
|
8292
|
+
}
|
|
7220
8293
|
const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
|
|
7221
8294
|
return new Promise((resolve) => {
|
|
7222
8295
|
canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
|
|
@@ -7598,6 +8671,33 @@ async function exportSvg(store, options = {}, layerManager) {
|
|
|
7598
8671
|
const opacity = layerManager?.getLayer?.(grid.layerId)?.opacity ?? 1;
|
|
7599
8672
|
body += opacity === 1 ? emitted : `<g opacity="${n(opacity)}">${emitted}</g>`;
|
|
7600
8673
|
}
|
|
8674
|
+
if (options.fog && typeof document !== "undefined") {
|
|
8675
|
+
const fogState = options.fog.state;
|
|
8676
|
+
const fogW = Math.max(1, Math.ceil(bounds.w));
|
|
8677
|
+
const fogH = Math.max(1, Math.ceil(bounds.h));
|
|
8678
|
+
const fogCanvas = document.createElement("canvas");
|
|
8679
|
+
fogCanvas.width = fogW;
|
|
8680
|
+
fogCanvas.height = fogH;
|
|
8681
|
+
const fogCtx = fogCanvas.getContext("2d");
|
|
8682
|
+
if (fogCtx) {
|
|
8683
|
+
fogCtx.translate(-bounds.x, -bounds.y);
|
|
8684
|
+
const fogRenderer = new FogRenderer();
|
|
8685
|
+
fogRenderer.renderForExport(
|
|
8686
|
+
fogCtx,
|
|
8687
|
+
fogState,
|
|
8688
|
+
options.fog.mode,
|
|
8689
|
+
options.fog.color,
|
|
8690
|
+
options.fog.style
|
|
8691
|
+
);
|
|
8692
|
+
try {
|
|
8693
|
+
const fogDataUri = fogCanvas.toDataURL("image/png");
|
|
8694
|
+
if (fogDataUri.startsWith("data:")) {
|
|
8695
|
+
body += `<image href="${esc(fogDataUri)}" x="${n(bounds.x)}" y="${n(bounds.y)}" width="${n(bounds.w)}" height="${n(bounds.h)}" />`;
|
|
8696
|
+
}
|
|
8697
|
+
} catch {
|
|
8698
|
+
}
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
7601
8701
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(bounds.w)}" height="${n(bounds.h)}" viewBox="${n(bounds.x)} ${n(bounds.y)} ${n(bounds.w)} ${n(bounds.h)}">${body}</svg>`;
|
|
7602
8702
|
}
|
|
7603
8703
|
function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
|
|
@@ -8435,6 +9535,7 @@ var RenderLoop = class {
|
|
|
8435
9535
|
layerCache;
|
|
8436
9536
|
marginViewport;
|
|
8437
9537
|
hybridSurface;
|
|
9538
|
+
fogRenderer;
|
|
8438
9539
|
activeDrawingLayerId = null;
|
|
8439
9540
|
gridCacheDirty = true;
|
|
8440
9541
|
// set on recenter/viewport-change; consumed by the grid block
|
|
@@ -8458,6 +9559,7 @@ var RenderLoop = class {
|
|
|
8458
9559
|
this.layerCache = deps.layerCache;
|
|
8459
9560
|
this.marginViewport = deps.marginViewport;
|
|
8460
9561
|
this.hybridSurface = deps.hybridSurface;
|
|
9562
|
+
this.fogRenderer = deps.fogRenderer;
|
|
8461
9563
|
}
|
|
8462
9564
|
requestRender() {
|
|
8463
9565
|
this.needsRender = true;
|
|
@@ -8767,9 +9869,12 @@ var RenderLoop = class {
|
|
|
8767
9869
|
group.push(element);
|
|
8768
9870
|
}
|
|
8769
9871
|
const activeTool = this.toolManager.activeTool;
|
|
8770
|
-
const
|
|
9872
|
+
const fogVisible = this.fogRenderer?.isVisible() ?? false;
|
|
9873
|
+
const fogOrder = visibleElements.length + 1;
|
|
9874
|
+
const overlayOrder = fogVisible ? fogOrder + 1 : visibleElements.length + 1;
|
|
8771
9875
|
const hasOverlay = activeTool?.renderOverlay !== void 0 || this.overlays.size > 0;
|
|
8772
|
-
if (
|
|
9876
|
+
if (fogVisible) hybridOrders.add(fogOrder);
|
|
9877
|
+
if (hasOverlay && (hybridActive || fogVisible)) hybridOrders.add(overlayOrder);
|
|
8773
9878
|
this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
|
|
8774
9879
|
for (const [layerId, elements] of this.layerGroups) {
|
|
8775
9880
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
@@ -8883,8 +9988,18 @@ var RenderLoop = class {
|
|
|
8883
9988
|
}
|
|
8884
9989
|
hybridCtx.restore();
|
|
8885
9990
|
}
|
|
9991
|
+
if (fogVisible && this.fogRenderer) {
|
|
9992
|
+
const fogCtx = this.hybridSurface.getContext(fogOrder);
|
|
9993
|
+
if (fogCtx) {
|
|
9994
|
+
fogCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
9995
|
+
fogCtx.save();
|
|
9996
|
+
fogCtx.scale(dpr, dpr);
|
|
9997
|
+
this.fogRenderer.render(fogCtx, this.camera, cssWidth, cssHeight, dpr);
|
|
9998
|
+
fogCtx.restore();
|
|
9999
|
+
}
|
|
10000
|
+
}
|
|
8886
10001
|
const overlayT0 = performance.now();
|
|
8887
|
-
if (hybridActive && hasOverlay) {
|
|
10002
|
+
if ((hybridActive || fogVisible) && hasOverlay) {
|
|
8888
10003
|
const overlayCtx = this.hybridSurface.getContext(overlayOrder);
|
|
8889
10004
|
if (overlayCtx) {
|
|
8890
10005
|
overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
@@ -9895,6 +11010,248 @@ var ElementActivation = class {
|
|
|
9895
11010
|
}
|
|
9896
11011
|
};
|
|
9897
11012
|
|
|
11013
|
+
// src/fog/fog-command.ts
|
|
11014
|
+
var FogRegionCommand = class {
|
|
11015
|
+
constructor(manager, before, after) {
|
|
11016
|
+
this.manager = manager;
|
|
11017
|
+
this.before = before;
|
|
11018
|
+
this.after = after;
|
|
11019
|
+
}
|
|
11020
|
+
execute(_store) {
|
|
11021
|
+
this.manager.applyTilesDirect(this.after);
|
|
11022
|
+
}
|
|
11023
|
+
undo(_store) {
|
|
11024
|
+
this.manager.applyTilesDirect(this.before);
|
|
11025
|
+
}
|
|
11026
|
+
};
|
|
11027
|
+
var FogResetCommand = class {
|
|
11028
|
+
constructor(manager, before, after) {
|
|
11029
|
+
this.manager = manager;
|
|
11030
|
+
this.before = before;
|
|
11031
|
+
this.after = after;
|
|
11032
|
+
}
|
|
11033
|
+
execute(_store) {
|
|
11034
|
+
this.manager.restoreHistoryState(this.after);
|
|
11035
|
+
}
|
|
11036
|
+
undo(_store) {
|
|
11037
|
+
this.manager.restoreHistoryState(this.before);
|
|
11038
|
+
}
|
|
11039
|
+
};
|
|
11040
|
+
|
|
11041
|
+
// src/fog/fog-manager.ts
|
|
11042
|
+
function defaultIdFactory() {
|
|
11043
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
11044
|
+
return crypto.randomUUID();
|
|
11045
|
+
}
|
|
11046
|
+
return `fog-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
11047
|
+
}
|
|
11048
|
+
var FogManager = class {
|
|
11049
|
+
state = null;
|
|
11050
|
+
viewMode = "off";
|
|
11051
|
+
idFactory;
|
|
11052
|
+
onCommand;
|
|
11053
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
11054
|
+
viewListeners = /* @__PURE__ */ new Set();
|
|
11055
|
+
constructor(options = {}) {
|
|
11056
|
+
this.idFactory = options.idFactory ?? defaultIdFactory;
|
|
11057
|
+
this.onCommand = options.onCommand;
|
|
11058
|
+
}
|
|
11059
|
+
getState() {
|
|
11060
|
+
if (!this.state) return null;
|
|
11061
|
+
return {
|
|
11062
|
+
definition: { ...this.state.definition, bounds: { ...this.state.definition.bounds } },
|
|
11063
|
+
tiles: this.state.tiles.map((t) => ({ ...t }))
|
|
11064
|
+
};
|
|
11065
|
+
}
|
|
11066
|
+
getViewMode() {
|
|
11067
|
+
return this.viewMode;
|
|
11068
|
+
}
|
|
11069
|
+
initialize(options) {
|
|
11070
|
+
const base = options.base ?? "covered";
|
|
11071
|
+
const cellSize = options.cellSize ?? recommendedFogCellSize(options.bounds);
|
|
11072
|
+
const generation = this.idFactory();
|
|
11073
|
+
const newState = {
|
|
11074
|
+
definition: {
|
|
11075
|
+
version: 1,
|
|
11076
|
+
generation,
|
|
11077
|
+
bounds: { ...options.bounds },
|
|
11078
|
+
cellSize,
|
|
11079
|
+
tileCells: FOG_TILE_CELLS,
|
|
11080
|
+
base
|
|
11081
|
+
},
|
|
11082
|
+
tiles: []
|
|
11083
|
+
};
|
|
11084
|
+
validateFogState(newState);
|
|
11085
|
+
const before = this.state;
|
|
11086
|
+
this.state = newState;
|
|
11087
|
+
const command = new FogResetCommand(this, before, newState);
|
|
11088
|
+
this.onCommand?.(command);
|
|
11089
|
+
this.notifyChange({ kind: "definition" });
|
|
11090
|
+
return structuredClone(newState);
|
|
11091
|
+
}
|
|
11092
|
+
loadState(state, meta) {
|
|
11093
|
+
if (state !== null) {
|
|
11094
|
+
validateFogState(state);
|
|
11095
|
+
this.state = structuredClone(state);
|
|
11096
|
+
} else {
|
|
11097
|
+
this.state = null;
|
|
11098
|
+
}
|
|
11099
|
+
this.notifyChange({
|
|
11100
|
+
kind: state === null ? "disable" : "definition",
|
|
11101
|
+
origin: meta?.origin
|
|
11102
|
+
});
|
|
11103
|
+
}
|
|
11104
|
+
/** Restores a historical visual state without reusing its causal generation id. */
|
|
11105
|
+
restoreHistoryState(state) {
|
|
11106
|
+
if (state === null) {
|
|
11107
|
+
this.loadState(null);
|
|
11108
|
+
return;
|
|
11109
|
+
}
|
|
11110
|
+
this.loadState({
|
|
11111
|
+
definition: { ...state.definition, generation: this.idFactory() },
|
|
11112
|
+
tiles: state.tiles
|
|
11113
|
+
});
|
|
11114
|
+
}
|
|
11115
|
+
setBounds(bounds) {
|
|
11116
|
+
if (!this.state) return;
|
|
11117
|
+
const def = this.state.definition;
|
|
11118
|
+
validateFogState({
|
|
11119
|
+
definition: { ...def, bounds: { ...bounds } },
|
|
11120
|
+
tiles: []
|
|
11121
|
+
});
|
|
11122
|
+
const shrinks = bounds.x > def.bounds.x || bounds.y > def.bounds.y || bounds.x + bounds.w < def.bounds.x + def.bounds.w || bounds.y + bounds.h < def.bounds.y + def.bounds.h;
|
|
11123
|
+
const nextDefinition = {
|
|
11124
|
+
...def,
|
|
11125
|
+
bounds: { ...bounds },
|
|
11126
|
+
generation: shrinks ? this.idFactory() : def.generation
|
|
11127
|
+
};
|
|
11128
|
+
const tiles = this.state.tiles.flatMap((tile) => {
|
|
11129
|
+
const tileWorldX = tile.x * FOG_TILE_CELLS * def.cellSize;
|
|
11130
|
+
const tileWorldY = tile.y * FOG_TILE_CELLS * def.cellSize;
|
|
11131
|
+
const tileWorldW = FOG_TILE_CELLS * def.cellSize;
|
|
11132
|
+
const tileWorldH = FOG_TILE_CELLS * def.cellSize;
|
|
11133
|
+
const intersects2 = !(tileWorldX + tileWorldW <= bounds.x || tileWorldY + tileWorldH <= bounds.y || tileWorldX >= bounds.x + bounds.w || tileWorldY >= bounds.y + bounds.h);
|
|
11134
|
+
if (!intersects2) return [];
|
|
11135
|
+
const canonical = canonicalizeFogTile(tile, nextDefinition);
|
|
11136
|
+
return canonical ? [canonical] : [];
|
|
11137
|
+
});
|
|
11138
|
+
const before = this.state;
|
|
11139
|
+
this.state = {
|
|
11140
|
+
definition: nextDefinition,
|
|
11141
|
+
tiles
|
|
11142
|
+
};
|
|
11143
|
+
const command = new FogResetCommand(this, before, this.state);
|
|
11144
|
+
this.onCommand?.(command);
|
|
11145
|
+
this.notifyChange({ kind: "definition" });
|
|
11146
|
+
}
|
|
11147
|
+
reset(base) {
|
|
11148
|
+
if (!this.state) return;
|
|
11149
|
+
const before = this.state;
|
|
11150
|
+
const generation = this.idFactory();
|
|
11151
|
+
this.state = {
|
|
11152
|
+
definition: { ...this.state.definition, base, generation },
|
|
11153
|
+
tiles: []
|
|
11154
|
+
};
|
|
11155
|
+
const command = new FogResetCommand(this, before, this.state);
|
|
11156
|
+
this.onCommand?.(command);
|
|
11157
|
+
this.notifyChange({ kind: "reset" });
|
|
11158
|
+
}
|
|
11159
|
+
disable() {
|
|
11160
|
+
if (!this.state) return;
|
|
11161
|
+
const before = this.state;
|
|
11162
|
+
this.state = null;
|
|
11163
|
+
const command = new FogResetCommand(this, before, null);
|
|
11164
|
+
this.onCommand?.(command);
|
|
11165
|
+
this.notifyChange({ kind: "disable" });
|
|
11166
|
+
}
|
|
11167
|
+
setViewMode(mode) {
|
|
11168
|
+
if (mode === this.viewMode) return;
|
|
11169
|
+
this.viewMode = mode;
|
|
11170
|
+
this.notifyView({ mode });
|
|
11171
|
+
}
|
|
11172
|
+
applyRegion(region, operation) {
|
|
11173
|
+
if (!this.state) return;
|
|
11174
|
+
const result = rasterizeRegion(this.state, region, operation);
|
|
11175
|
+
if (result.noop) return;
|
|
11176
|
+
const before = this.collectTiles(result.changed);
|
|
11177
|
+
const newState = applyRasterResult(this.state, result);
|
|
11178
|
+
this.state = newState;
|
|
11179
|
+
const command = new FogRegionCommand(this, before, result.changed);
|
|
11180
|
+
this.onCommand?.(command);
|
|
11181
|
+
this.notifyChange({
|
|
11182
|
+
kind: "tiles",
|
|
11183
|
+
tiles: result.changed.map((t) => ({ x: t.x, y: t.y }))
|
|
11184
|
+
});
|
|
11185
|
+
}
|
|
11186
|
+
applyPatchDirect(patch, meta) {
|
|
11187
|
+
if (!this.state) return;
|
|
11188
|
+
const result = applyRasterResult(this.state, { changed: patch.tiles, noop: false });
|
|
11189
|
+
this.state = result;
|
|
11190
|
+
this.notifyChange({
|
|
11191
|
+
kind: "tiles",
|
|
11192
|
+
tiles: patch.tiles.map((t) => ({ x: t.x, y: t.y })),
|
|
11193
|
+
origin: meta?.origin
|
|
11194
|
+
});
|
|
11195
|
+
}
|
|
11196
|
+
applyTilesDirect(tiles) {
|
|
11197
|
+
if (!this.state) return;
|
|
11198
|
+
const result = applyRasterResult(this.state, { changed: tiles, noop: false });
|
|
11199
|
+
this.state = result;
|
|
11200
|
+
this.notifyChange({
|
|
11201
|
+
kind: "tiles",
|
|
11202
|
+
tiles: tiles.map((t) => ({ x: t.x, y: t.y }))
|
|
11203
|
+
});
|
|
11204
|
+
}
|
|
11205
|
+
on(event, listener) {
|
|
11206
|
+
if (event === "change") {
|
|
11207
|
+
const l2 = listener;
|
|
11208
|
+
this.changeListeners.add(l2);
|
|
11209
|
+
return () => this.changeListeners.delete(l2);
|
|
11210
|
+
}
|
|
11211
|
+
const l = listener;
|
|
11212
|
+
this.viewListeners.add(l);
|
|
11213
|
+
return () => this.viewListeners.delete(l);
|
|
11214
|
+
}
|
|
11215
|
+
dispose() {
|
|
11216
|
+
this.changeListeners.clear();
|
|
11217
|
+
this.viewListeners.clear();
|
|
11218
|
+
}
|
|
11219
|
+
collectTiles(changed) {
|
|
11220
|
+
if (!this.state) return [];
|
|
11221
|
+
const result = [];
|
|
11222
|
+
for (const c of changed) {
|
|
11223
|
+
const existing = this.state.tiles.find((t) => t.x === c.x && t.y === c.y);
|
|
11224
|
+
if (existing) {
|
|
11225
|
+
result.push(existing);
|
|
11226
|
+
} else {
|
|
11227
|
+
const baseVal = this.state.definition.base === "revealed";
|
|
11228
|
+
result.push({
|
|
11229
|
+
x: c.x,
|
|
11230
|
+
y: c.y,
|
|
11231
|
+
data: encodeBase64(createTileBytes(baseVal))
|
|
11232
|
+
});
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
return result;
|
|
11236
|
+
}
|
|
11237
|
+
notifyChange(event) {
|
|
11238
|
+
for (const listener of this.changeListeners) {
|
|
11239
|
+
try {
|
|
11240
|
+
listener(event);
|
|
11241
|
+
} catch {
|
|
11242
|
+
}
|
|
11243
|
+
}
|
|
11244
|
+
}
|
|
11245
|
+
notifyView(event) {
|
|
11246
|
+
for (const listener of this.viewListeners) {
|
|
11247
|
+
try {
|
|
11248
|
+
listener(event);
|
|
11249
|
+
} catch {
|
|
11250
|
+
}
|
|
11251
|
+
}
|
|
11252
|
+
}
|
|
11253
|
+
};
|
|
11254
|
+
|
|
9898
11255
|
// src/canvas/viewport.ts
|
|
9899
11256
|
var EMPTY_IDS = [];
|
|
9900
11257
|
function noop2() {
|
|
@@ -10021,8 +11378,13 @@ var Viewport = class _Viewport {
|
|
|
10021
11378
|
});
|
|
10022
11379
|
}
|
|
10023
11380
|
this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
|
|
11381
|
+
this.fogManager = new FogManager({
|
|
11382
|
+
onCommand: (cmd) => this.history.push(cmd)
|
|
11383
|
+
});
|
|
11384
|
+
this.fogRenderer = new FogRenderer(options.fog);
|
|
10024
11385
|
if (options.minimap) {
|
|
10025
11386
|
this.minimap = new Minimap(this.wrapper, this);
|
|
11387
|
+
this.minimap.setFogRenderer(this.fogRenderer);
|
|
10026
11388
|
}
|
|
10027
11389
|
this.domNodeManager = new DomNodeManager({
|
|
10028
11390
|
domLayer: this.paintStack,
|
|
@@ -10052,7 +11414,18 @@ var Viewport = class _Viewport {
|
|
|
10052
11414
|
domNodeManager: this.domNodeManager,
|
|
10053
11415
|
layerCache,
|
|
10054
11416
|
marginViewport: this.marginViewport,
|
|
10055
|
-
hybridSurface: new HybridRenderSurface(this.paintStack)
|
|
11417
|
+
hybridSurface: new HybridRenderSurface(this.paintStack),
|
|
11418
|
+
fogRenderer: this.fogRenderer
|
|
11419
|
+
});
|
|
11420
|
+
this.fogManager.on("change", () => {
|
|
11421
|
+
this.fogRenderer.setState(this.fogManager.getState());
|
|
11422
|
+
this.renderLoop.requestRender();
|
|
11423
|
+
this.minimap?.invalidateScene();
|
|
11424
|
+
});
|
|
11425
|
+
this.fogManager.on("view", () => {
|
|
11426
|
+
this.fogRenderer.setViewMode(this.fogManager.getViewMode());
|
|
11427
|
+
this.renderLoop.requestRender();
|
|
11428
|
+
this.minimap?.invalidateScene();
|
|
10056
11429
|
});
|
|
10057
11430
|
this.unsubHtmlPainters = this.htmlPainters.onChange(() => this.onHtmlRegistryChanged());
|
|
10058
11431
|
this.unsubCamera = this.camera.onChange(() => {
|
|
@@ -10164,6 +11537,8 @@ var Viewport = class _Viewport {
|
|
|
10164
11537
|
_smartGuides = false;
|
|
10165
11538
|
_gridSize;
|
|
10166
11539
|
renderLoop;
|
|
11540
|
+
fogManager;
|
|
11541
|
+
fogRenderer;
|
|
10167
11542
|
domNodeManager;
|
|
10168
11543
|
interactMode;
|
|
10169
11544
|
onHtmlElementMount;
|
|
@@ -10199,6 +11574,9 @@ var Viewport = class _Viewport {
|
|
|
10199
11574
|
get ctx() {
|
|
10200
11575
|
return this.canvasEl.getContext("2d");
|
|
10201
11576
|
}
|
|
11577
|
+
get fog() {
|
|
11578
|
+
return this.fogManager;
|
|
11579
|
+
}
|
|
10202
11580
|
get snapToGrid() {
|
|
10203
11581
|
return this._snapToGrid;
|
|
10204
11582
|
}
|
|
@@ -10283,7 +11661,8 @@ var Viewport = class _Viewport {
|
|
|
10283
11661
|
this.store.snapshot(),
|
|
10284
11662
|
this.camera,
|
|
10285
11663
|
this.layerManager.snapshot(),
|
|
10286
|
-
this.layerManager.activeLayerId
|
|
11664
|
+
this.layerManager.activeLayerId,
|
|
11665
|
+
this.fogManager.getState()
|
|
10287
11666
|
);
|
|
10288
11667
|
}
|
|
10289
11668
|
exportJSON() {
|
|
@@ -10304,13 +11683,41 @@ var Viewport = class _Viewport {
|
|
|
10304
11683
|
const expected = base.expectedCanvasTypes ? /* @__PURE__ */ new Set([...declared, ...base.expectedCanvasTypes]) : declared;
|
|
10305
11684
|
return { ...base, htmlPainters: registry, expectedCanvasTypes: expected };
|
|
10306
11685
|
}
|
|
11686
|
+
/**
|
|
11687
|
+
* Carry constructor-configured fog presentation into both implicit exports and
|
|
11688
|
+
* explicit state/mode exports. Explicit style and legacy color overrides win.
|
|
11689
|
+
*/
|
|
11690
|
+
withFogDefaults(options) {
|
|
11691
|
+
const fog = options.fog;
|
|
11692
|
+
if (fog === false) return options;
|
|
11693
|
+
if (fog !== void 0) {
|
|
11694
|
+
if (fog.style !== void 0 || fog.color !== void 0) return options;
|
|
11695
|
+
return {
|
|
11696
|
+
...options,
|
|
11697
|
+
fog: { ...fog, style: this.fogRenderer.getResolvedStyle(fog.mode) }
|
|
11698
|
+
};
|
|
11699
|
+
}
|
|
11700
|
+
if (!this.fogRenderer.isVisible()) return options;
|
|
11701
|
+
const state = this.fogManager.getState();
|
|
11702
|
+
if (!state) return options;
|
|
11703
|
+
const mode = this.fogRenderer.getViewMode();
|
|
11704
|
+
return {
|
|
11705
|
+
...options,
|
|
11706
|
+
fog: { state, mode, style: this.fogRenderer.getResolvedStyle(mode) }
|
|
11707
|
+
};
|
|
11708
|
+
}
|
|
10307
11709
|
async exportImage(options) {
|
|
10308
|
-
|
|
11710
|
+
const opts = this.withFogDefaults(this.withHtmlDefaults(options));
|
|
11711
|
+
return exportImage(this.store, opts, this.layerManager);
|
|
10309
11712
|
}
|
|
10310
11713
|
async exportSVG(options) {
|
|
10311
|
-
|
|
11714
|
+
const opts = this.withFogDefaults(this.withHtmlDefaults(options));
|
|
11715
|
+
return exportSvg(this.store, opts, this.layerManager);
|
|
10312
11716
|
}
|
|
10313
11717
|
loadState(state) {
|
|
11718
|
+
if (state.fog != null) {
|
|
11719
|
+
validateFogState(state.fog);
|
|
11720
|
+
}
|
|
10314
11721
|
this.inputHandler.flushPendingHistory();
|
|
10315
11722
|
this.historyRecorder.pause();
|
|
10316
11723
|
this.noteEditor.destroy(this.store);
|
|
@@ -10345,6 +11752,7 @@ var Viewport = class _Viewport {
|
|
|
10345
11752
|
}
|
|
10346
11753
|
}
|
|
10347
11754
|
}
|
|
11755
|
+
this.fogManager.loadState(state.fog ?? null);
|
|
10348
11756
|
this.history.clear();
|
|
10349
11757
|
this.historyRecorder.resume();
|
|
10350
11758
|
this.camera.moveTo(state.camera.position.x, state.camera.position.y);
|
|
@@ -10756,6 +12164,8 @@ var Viewport = class _Viewport {
|
|
|
10756
12164
|
this.unsubToolRegister();
|
|
10757
12165
|
this.unsubRecorderEnd();
|
|
10758
12166
|
this.unsubHtmlPainters();
|
|
12167
|
+
this.fogManager.dispose();
|
|
12168
|
+
this.fogRenderer.dispose();
|
|
10759
12169
|
this.activation?.dispose();
|
|
10760
12170
|
this.activation = null;
|
|
10761
12171
|
this.activationListeners.clear();
|
|
@@ -11935,15 +13345,15 @@ function applyCameraView(camera, view, canvasW, canvasH) {
|
|
|
11935
13345
|
var DEFAULT_DURATION_MS3 = 400;
|
|
11936
13346
|
var FRAMED_EPSILON = 1e-6;
|
|
11937
13347
|
var easeOutCubic2 = (t) => 1 - Math.pow(1 - t, 3);
|
|
11938
|
-
function
|
|
13348
|
+
function lerp2(a, b, k) {
|
|
11939
13349
|
return a + (b - a) * k;
|
|
11940
13350
|
}
|
|
11941
13351
|
function lerpView(from, to, k) {
|
|
11942
13352
|
return {
|
|
11943
|
-
x:
|
|
11944
|
-
y:
|
|
11945
|
-
w:
|
|
11946
|
-
h:
|
|
13353
|
+
x: lerp2(from.x, to.x, k),
|
|
13354
|
+
y: lerp2(from.y, to.y, k),
|
|
13355
|
+
w: lerp2(from.w, to.w, k),
|
|
13356
|
+
h: lerp2(from.h, to.h, k)
|
|
11947
13357
|
};
|
|
11948
13358
|
}
|
|
11949
13359
|
function viewsClose(a, b) {
|
|
@@ -13197,7 +14607,7 @@ var PencilTool = class {
|
|
|
13197
14607
|
};
|
|
13198
14608
|
|
|
13199
14609
|
// src/elements/stroke-erase.ts
|
|
13200
|
-
function
|
|
14610
|
+
function lerp3(a, b, t) {
|
|
13201
14611
|
return {
|
|
13202
14612
|
x: a.x + (b.x - a.x) * t,
|
|
13203
14613
|
y: a.y + (b.y - a.y) * t,
|
|
@@ -13256,13 +14666,13 @@ function erasePoints(points, eraser, radius) {
|
|
|
13256
14666
|
erased = true;
|
|
13257
14667
|
if (tLo > 0) {
|
|
13258
14668
|
if (current.length === 0) current.push(a);
|
|
13259
|
-
current.push(
|
|
14669
|
+
current.push(lerp3(a, b, tLo));
|
|
13260
14670
|
flush();
|
|
13261
14671
|
} else {
|
|
13262
14672
|
flush();
|
|
13263
14673
|
}
|
|
13264
14674
|
if (tHi < 1) {
|
|
13265
|
-
current = [
|
|
14675
|
+
current = [lerp3(a, b, tHi), b];
|
|
13266
14676
|
}
|
|
13267
14677
|
}
|
|
13268
14678
|
flush();
|
|
@@ -15597,8 +17007,188 @@ var PingTool = class {
|
|
|
15597
17007
|
}
|
|
15598
17008
|
};
|
|
15599
17009
|
|
|
17010
|
+
// src/tools/fog-tool.ts
|
|
17011
|
+
var DEFAULT_RADIUS5 = 40;
|
|
17012
|
+
var MIN_POINT_DISTANCE = 4;
|
|
17013
|
+
var FogTool = class {
|
|
17014
|
+
name = "fog";
|
|
17015
|
+
drawing = false;
|
|
17016
|
+
points = [];
|
|
17017
|
+
startPoint = null;
|
|
17018
|
+
operation;
|
|
17019
|
+
shape;
|
|
17020
|
+
radius;
|
|
17021
|
+
manager;
|
|
17022
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
17023
|
+
constructor(manager, options = {}) {
|
|
17024
|
+
this.manager = manager;
|
|
17025
|
+
this.operation = options.operation ?? "reveal";
|
|
17026
|
+
this.shape = options.shape ?? "brush";
|
|
17027
|
+
this.radius = options.radius ?? DEFAULT_RADIUS5;
|
|
17028
|
+
}
|
|
17029
|
+
onActivate(ctx) {
|
|
17030
|
+
ctx.setCursor?.("crosshair");
|
|
17031
|
+
}
|
|
17032
|
+
onDeactivate(ctx) {
|
|
17033
|
+
this.cancelGesture(ctx);
|
|
17034
|
+
ctx.setCursor?.("default");
|
|
17035
|
+
}
|
|
17036
|
+
getOptions() {
|
|
17037
|
+
return {
|
|
17038
|
+
operation: this.operation,
|
|
17039
|
+
shape: this.shape,
|
|
17040
|
+
radius: this.radius
|
|
17041
|
+
};
|
|
17042
|
+
}
|
|
17043
|
+
setOptions(options) {
|
|
17044
|
+
if (options.operation !== void 0) this.operation = options.operation;
|
|
17045
|
+
if (options.shape !== void 0) this.shape = options.shape;
|
|
17046
|
+
if (options.radius !== void 0 && Number.isFinite(options.radius) && options.radius > 0) {
|
|
17047
|
+
this.radius = options.radius;
|
|
17048
|
+
}
|
|
17049
|
+
for (const listener of this.optionListeners) listener();
|
|
17050
|
+
}
|
|
17051
|
+
onOptionsChange(listener) {
|
|
17052
|
+
this.optionListeners.add(listener);
|
|
17053
|
+
return () => this.optionListeners.delete(listener);
|
|
17054
|
+
}
|
|
17055
|
+
onPointerDown(state, ctx) {
|
|
17056
|
+
if (this.drawing) return;
|
|
17057
|
+
this.drawing = true;
|
|
17058
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
17059
|
+
this.startPoint = world;
|
|
17060
|
+
this.points = [world];
|
|
17061
|
+
ctx.requestRender();
|
|
17062
|
+
}
|
|
17063
|
+
onPointerMove(state, ctx) {
|
|
17064
|
+
if (!this.drawing) return;
|
|
17065
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
17066
|
+
if (this.shape === "rectangle") {
|
|
17067
|
+
if (this.startPoint) this.points = [this.startPoint, world];
|
|
17068
|
+
} else {
|
|
17069
|
+
const last = this.points[this.points.length - 1];
|
|
17070
|
+
if (last) {
|
|
17071
|
+
const dx = world.x - last.x;
|
|
17072
|
+
const dy = world.y - last.y;
|
|
17073
|
+
if (dx * dx + dy * dy < MIN_POINT_DISTANCE * MIN_POINT_DISTANCE) return;
|
|
17074
|
+
}
|
|
17075
|
+
this.points.push(world);
|
|
17076
|
+
}
|
|
17077
|
+
ctx.requestRender();
|
|
17078
|
+
}
|
|
17079
|
+
onPointerUp(_state, ctx) {
|
|
17080
|
+
if (!this.drawing) return;
|
|
17081
|
+
this.drawing = false;
|
|
17082
|
+
const region = this.buildRegion();
|
|
17083
|
+
if (region) {
|
|
17084
|
+
this.manager.applyRegion(region, this.operation);
|
|
17085
|
+
}
|
|
17086
|
+
this.points = [];
|
|
17087
|
+
this.startPoint = null;
|
|
17088
|
+
ctx.requestRender();
|
|
17089
|
+
}
|
|
17090
|
+
onPointerCancel(_state, ctx) {
|
|
17091
|
+
this.cancelGesture(ctx);
|
|
17092
|
+
}
|
|
17093
|
+
onKeyDown(event, ctx) {
|
|
17094
|
+
if (event.key === "Escape" && this.drawing) {
|
|
17095
|
+
this.cancelGesture(ctx);
|
|
17096
|
+
return true;
|
|
17097
|
+
}
|
|
17098
|
+
return false;
|
|
17099
|
+
}
|
|
17100
|
+
renderOverlay(ctx) {
|
|
17101
|
+
if (!this.drawing || this.points.length === 0) return;
|
|
17102
|
+
ctx.save();
|
|
17103
|
+
ctx.strokeStyle = this.operation === "reveal" ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.4)";
|
|
17104
|
+
ctx.fillStyle = this.operation === "reveal" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)";
|
|
17105
|
+
ctx.lineWidth = 2;
|
|
17106
|
+
ctx.setLineDash([6, 4]);
|
|
17107
|
+
switch (this.shape) {
|
|
17108
|
+
case "brush":
|
|
17109
|
+
this.renderBrushPreview(ctx);
|
|
17110
|
+
break;
|
|
17111
|
+
case "rectangle":
|
|
17112
|
+
this.renderRectanglePreview(ctx);
|
|
17113
|
+
break;
|
|
17114
|
+
case "polygon":
|
|
17115
|
+
this.renderPolygonPreview(ctx);
|
|
17116
|
+
break;
|
|
17117
|
+
}
|
|
17118
|
+
ctx.restore();
|
|
17119
|
+
}
|
|
17120
|
+
buildRegion() {
|
|
17121
|
+
switch (this.shape) {
|
|
17122
|
+
case "brush": {
|
|
17123
|
+
if (this.points.length === 0) return null;
|
|
17124
|
+
return { kind: "brush", points: this.points, radius: this.radius };
|
|
17125
|
+
}
|
|
17126
|
+
case "rectangle": {
|
|
17127
|
+
if (!this.startPoint || this.points.length < 2) return null;
|
|
17128
|
+
const end = this.points[this.points.length - 1];
|
|
17129
|
+
if (this.startPoint.x === end.x && this.startPoint.y === end.y) return null;
|
|
17130
|
+
return { kind: "rectangle", from: this.startPoint, to: end };
|
|
17131
|
+
}
|
|
17132
|
+
case "polygon": {
|
|
17133
|
+
if (this.points.length < 3) return null;
|
|
17134
|
+
return { kind: "polygon", points: this.points };
|
|
17135
|
+
}
|
|
17136
|
+
}
|
|
17137
|
+
}
|
|
17138
|
+
cancelGesture(ctx) {
|
|
17139
|
+
this.drawing = false;
|
|
17140
|
+
this.points = [];
|
|
17141
|
+
this.startPoint = null;
|
|
17142
|
+
ctx.requestRender();
|
|
17143
|
+
}
|
|
17144
|
+
renderBrushPreview(ctx) {
|
|
17145
|
+
if (this.points.length === 1) {
|
|
17146
|
+
const p = this.points[0];
|
|
17147
|
+
ctx.beginPath();
|
|
17148
|
+
ctx.arc(p.x, p.y, this.radius, 0, Math.PI * 2);
|
|
17149
|
+
ctx.fill();
|
|
17150
|
+
ctx.stroke();
|
|
17151
|
+
return;
|
|
17152
|
+
}
|
|
17153
|
+
ctx.beginPath();
|
|
17154
|
+
for (let i = 0; i < this.points.length; i++) {
|
|
17155
|
+
const p = this.points[i];
|
|
17156
|
+
if (i === 0) ctx.moveTo(p.x, p.y);
|
|
17157
|
+
else ctx.lineTo(p.x, p.y);
|
|
17158
|
+
}
|
|
17159
|
+
ctx.lineWidth = this.radius * 2;
|
|
17160
|
+
ctx.lineCap = "round";
|
|
17161
|
+
ctx.lineJoin = "round";
|
|
17162
|
+
ctx.stroke();
|
|
17163
|
+
}
|
|
17164
|
+
renderRectanglePreview(ctx) {
|
|
17165
|
+
if (this.points.length < 2) return;
|
|
17166
|
+
const from = this.points[0];
|
|
17167
|
+
const to = this.points[this.points.length - 1];
|
|
17168
|
+
const x = Math.min(from.x, to.x);
|
|
17169
|
+
const y = Math.min(from.y, to.y);
|
|
17170
|
+
const w = Math.abs(to.x - from.x);
|
|
17171
|
+
const h = Math.abs(to.y - from.y);
|
|
17172
|
+
ctx.fillRect(x, y, w, h);
|
|
17173
|
+
ctx.strokeRect(x, y, w, h);
|
|
17174
|
+
}
|
|
17175
|
+
renderPolygonPreview(ctx) {
|
|
17176
|
+
if (this.points.length < 2) return;
|
|
17177
|
+
const first = this.points[0];
|
|
17178
|
+
ctx.beginPath();
|
|
17179
|
+
ctx.moveTo(first.x, first.y);
|
|
17180
|
+
for (let i = 1; i < this.points.length; i++) {
|
|
17181
|
+
const p = this.points[i];
|
|
17182
|
+
ctx.lineTo(p.x, p.y);
|
|
17183
|
+
}
|
|
17184
|
+
ctx.closePath();
|
|
17185
|
+
ctx.fill();
|
|
17186
|
+
ctx.stroke();
|
|
17187
|
+
}
|
|
17188
|
+
};
|
|
17189
|
+
|
|
15600
17190
|
// src/index.ts
|
|
15601
|
-
var VERSION = "0.
|
|
17191
|
+
var VERSION = "0.67.0";
|
|
15602
17192
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15603
17193
|
0 && (module.exports = {
|
|
15604
17194
|
AWARENESS_MAX_SELECTION,
|
|
@@ -15612,6 +17202,12 @@ var VERSION = "0.65.0";
|
|
|
15612
17202
|
ElementStore,
|
|
15613
17203
|
EraserTool,
|
|
15614
17204
|
FOCUS_PRESENCE_KIND,
|
|
17205
|
+
FOG_MAX_TILES,
|
|
17206
|
+
FOG_STATE_VERSION,
|
|
17207
|
+
FOG_TILE_CELLS,
|
|
17208
|
+
FogManager,
|
|
17209
|
+
FogRenderer,
|
|
17210
|
+
FogTool,
|
|
15615
17211
|
HandTool,
|
|
15616
17212
|
HistoryStack,
|
|
15617
17213
|
HtmlPainterMissingError,
|
|
@@ -15655,6 +17251,7 @@ var VERSION = "0.65.0";
|
|
|
15655
17251
|
attachAwareness,
|
|
15656
17252
|
boundsIntersect,
|
|
15657
17253
|
cameraOriginForView,
|
|
17254
|
+
canonicalizeFogTile,
|
|
15658
17255
|
captureCameraView,
|
|
15659
17256
|
computeElementRects,
|
|
15660
17257
|
createArrow,
|
|
@@ -15672,6 +17269,8 @@ var VERSION = "0.65.0";
|
|
|
15672
17269
|
exportImage,
|
|
15673
17270
|
exportSvg,
|
|
15674
17271
|
fitZoomForView,
|
|
17272
|
+
fogDecodeBase64,
|
|
17273
|
+
fogEncodeBase64,
|
|
15675
17274
|
footprintFromSize,
|
|
15676
17275
|
getActiveFormats,
|
|
15677
17276
|
getArrowBounds,
|
|
@@ -15697,6 +17296,8 @@ var VERSION = "0.65.0";
|
|
|
15697
17296
|
isPathPresence,
|
|
15698
17297
|
isPingPresence,
|
|
15699
17298
|
pathDistanceCells,
|
|
17299
|
+
recommendedFogCellSize,
|
|
17300
|
+
resolveFogStyle,
|
|
15700
17301
|
resolveHtmlRouting,
|
|
15701
17302
|
setFontSize,
|
|
15702
17303
|
smartSnap,
|
|
@@ -15713,6 +17314,9 @@ var VERSION = "0.65.0";
|
|
|
15713
17314
|
toggleBold,
|
|
15714
17315
|
toggleItalic,
|
|
15715
17316
|
toggleStrikethrough,
|
|
15716
|
-
toggleUnderline
|
|
17317
|
+
toggleUnderline,
|
|
17318
|
+
validateFogDefinition,
|
|
17319
|
+
validateFogState,
|
|
17320
|
+
validateFogTile
|
|
15717
17321
|
});
|
|
15718
17322
|
//# sourceMappingURL=index.cjs.map
|