@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.js
CHANGED
|
@@ -454,8 +454,464 @@ function sanitizeAttributes(el, tag) {
|
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
// src/fog/types.ts
|
|
458
|
+
var FOG_STATE_VERSION = 1;
|
|
459
|
+
var FOG_TILE_CELLS = 128;
|
|
460
|
+
var FOG_MAX_TILES = 256;
|
|
461
|
+
|
|
462
|
+
// src/fog/tile-codec.ts
|
|
463
|
+
var TILE_BYTES = FOG_TILE_CELLS * FOG_TILE_CELLS / 8;
|
|
464
|
+
var CANONICAL_B64_LENGTH = Math.ceil(TILE_BYTES / 3) * 4;
|
|
465
|
+
var B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
466
|
+
var B64_LOOKUP = new Uint8Array(128);
|
|
467
|
+
for (let i = 0; i < B64_CHARS.length; i++) B64_LOOKUP[B64_CHARS.charCodeAt(i)] = i;
|
|
468
|
+
function encodeBase64(bytes) {
|
|
469
|
+
let result = "";
|
|
470
|
+
const len = bytes.length;
|
|
471
|
+
for (let i = 0; i < len; i += 3) {
|
|
472
|
+
const a = bytes[i];
|
|
473
|
+
const b = i + 1 < len ? bytes[i + 1] : 0;
|
|
474
|
+
const c = i + 2 < len ? bytes[i + 2] : 0;
|
|
475
|
+
result += B64_CHARS[a >> 2 & 63];
|
|
476
|
+
result += B64_CHARS[(a << 4 | b >> 4) & 63];
|
|
477
|
+
result += i + 1 < len ? B64_CHARS[(b << 2 | c >> 6) & 63] : "=";
|
|
478
|
+
result += i + 2 < len ? B64_CHARS[c & 63] : "=";
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
function decodeBase64(str) {
|
|
483
|
+
if (str.length % 4 !== 0) throw new Error("Invalid base64 length");
|
|
484
|
+
let padCount = 0;
|
|
485
|
+
if (str.length >= 2 && str[str.length - 1] === "=") {
|
|
486
|
+
padCount++;
|
|
487
|
+
if (str[str.length - 2] === "=") padCount++;
|
|
488
|
+
}
|
|
489
|
+
const byteLen = str.length / 4 * 3 - padCount;
|
|
490
|
+
const bytes = new Uint8Array(byteLen);
|
|
491
|
+
let j = 0;
|
|
492
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
493
|
+
const a = B64_LOOKUP[str.charCodeAt(i)];
|
|
494
|
+
const b = B64_LOOKUP[str.charCodeAt(i + 1)];
|
|
495
|
+
const c = str[i + 2] === "=" ? 0 : B64_LOOKUP[str.charCodeAt(i + 2)];
|
|
496
|
+
const d = str[i + 3] === "=" ? 0 : B64_LOOKUP[str.charCodeAt(i + 3)];
|
|
497
|
+
bytes[j++] = a << 2 | b >> 4;
|
|
498
|
+
if (j < byteLen) bytes[j++] = (b << 4 | c >> 2) & 255;
|
|
499
|
+
if (j < byteLen) bytes[j++] = (c << 6 | d) & 255;
|
|
500
|
+
}
|
|
501
|
+
return bytes;
|
|
502
|
+
}
|
|
503
|
+
function createTileBytes(fill) {
|
|
504
|
+
const bytes = new Uint8Array(TILE_BYTES);
|
|
505
|
+
if (fill) bytes.fill(255);
|
|
506
|
+
return bytes;
|
|
507
|
+
}
|
|
508
|
+
function setBit(bytes, col, row, value) {
|
|
509
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
510
|
+
const byteIndex = index >> 3;
|
|
511
|
+
const bitIndex = 7 - (index & 7);
|
|
512
|
+
if (value) {
|
|
513
|
+
bytes[byteIndex] = bytes[byteIndex] | 1 << bitIndex;
|
|
514
|
+
} else {
|
|
515
|
+
bytes[byteIndex] = bytes[byteIndex] & ~(1 << bitIndex);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function isTileAllValue(bytes, value) {
|
|
519
|
+
const expected = value ? 255 : 0;
|
|
520
|
+
for (let i = 0; i < TILE_BYTES; i++) {
|
|
521
|
+
if (bytes[i] !== expected) return false;
|
|
522
|
+
}
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
function isBaseValue(base) {
|
|
526
|
+
return base === "revealed";
|
|
527
|
+
}
|
|
528
|
+
function isTileBase(bytes, base) {
|
|
529
|
+
return isTileAllValue(bytes, isBaseValue(base));
|
|
530
|
+
}
|
|
531
|
+
function canonicalizeEdgePadding(bytes, def, tileX, tileY) {
|
|
532
|
+
const baseVal = isBaseValue(def.base);
|
|
533
|
+
const worldX = tileX * FOG_TILE_CELLS * def.cellSize;
|
|
534
|
+
const worldY = tileY * FOG_TILE_CELLS * def.cellSize;
|
|
535
|
+
const boundsRight = def.bounds.x + def.bounds.w;
|
|
536
|
+
const boundsBottom = def.bounds.y + def.bounds.h;
|
|
537
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
538
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
539
|
+
const cellWorldX = worldX + col * def.cellSize;
|
|
540
|
+
const cellWorldY = worldY + row * def.cellSize;
|
|
541
|
+
const outside = cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= boundsRight || cellWorldY >= boundsBottom;
|
|
542
|
+
if (outside) {
|
|
543
|
+
setBit(bytes, col, row, baseVal);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function canonicalizeFogTile(tile, def) {
|
|
549
|
+
const bytes = decodeBase64(tile.data);
|
|
550
|
+
canonicalizeEdgePadding(bytes, def, tile.x, tile.y);
|
|
551
|
+
if (isTileBase(bytes, def.base)) return null;
|
|
552
|
+
return { x: tile.x, y: tile.y, data: encodeBase64(bytes) };
|
|
553
|
+
}
|
|
554
|
+
function isCanonicalBase64(str) {
|
|
555
|
+
if (str.length !== CANONICAL_B64_LENGTH) return false;
|
|
556
|
+
if (str[str.length - 1] !== "=" || str[str.length - 2] === "=") return false;
|
|
557
|
+
for (let i = 0; i < str.length - 1; i++) {
|
|
558
|
+
if (!B64_CHARS.includes(str[i])) return false;
|
|
559
|
+
}
|
|
560
|
+
const finalSextet = B64_CHARS.indexOf(str[str.length - 2]);
|
|
561
|
+
return finalSextet >= 0 && (finalSextet & 3) === 0;
|
|
562
|
+
}
|
|
563
|
+
function isSafeInteger(n2) {
|
|
564
|
+
return typeof n2 === "number" && Number.isSafeInteger(n2);
|
|
565
|
+
}
|
|
566
|
+
function isFinitePositive(n2) {
|
|
567
|
+
return typeof n2 === "number" && Number.isFinite(n2) && n2 > 0;
|
|
568
|
+
}
|
|
569
|
+
function isFiniteBounds(b) {
|
|
570
|
+
if (typeof b !== "object" || b === null) return false;
|
|
571
|
+
const r = b;
|
|
572
|
+
return typeof r["x"] === "number" && Number.isFinite(r["x"]) && typeof r["y"] === "number" && Number.isFinite(r["y"]) && isFinitePositive(r["w"]) && isFinitePositive(r["h"]);
|
|
573
|
+
}
|
|
574
|
+
function tileIntersectsBounds(x, y, def) {
|
|
575
|
+
const tileWorldX = x * FOG_TILE_CELLS * def.cellSize;
|
|
576
|
+
const tileWorldY = y * FOG_TILE_CELLS * def.cellSize;
|
|
577
|
+
const tileWorldW = FOG_TILE_CELLS * def.cellSize;
|
|
578
|
+
const tileWorldH = FOG_TILE_CELLS * def.cellSize;
|
|
579
|
+
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);
|
|
580
|
+
}
|
|
581
|
+
function validateFogDefinition(def) {
|
|
582
|
+
if (typeof def !== "object" || def === null) {
|
|
583
|
+
throw new Error("Invalid fog definition: expected an object");
|
|
584
|
+
}
|
|
585
|
+
const d = def;
|
|
586
|
+
if (d["version"] !== 1) throw new Error("Invalid fog definition: unsupported version");
|
|
587
|
+
if (typeof d["generation"] !== "string" || d["generation"].length === 0 || d["generation"].length > 128 || !/^[\x20-\x7e]+$/.test(d["generation"])) {
|
|
588
|
+
throw new Error("Invalid fog definition: invalid generation");
|
|
589
|
+
}
|
|
590
|
+
if (!isFiniteBounds(d["bounds"])) {
|
|
591
|
+
throw new Error("Invalid fog definition: invalid bounds");
|
|
592
|
+
}
|
|
593
|
+
if (!isFinitePositive(d["cellSize"])) {
|
|
594
|
+
throw new Error("Invalid fog definition: invalid cellSize");
|
|
595
|
+
}
|
|
596
|
+
if (d["tileCells"] !== 128) {
|
|
597
|
+
throw new Error("Invalid fog definition: tileCells must be 128");
|
|
598
|
+
}
|
|
599
|
+
if (d["base"] !== "covered" && d["base"] !== "revealed") {
|
|
600
|
+
throw new Error("Invalid fog definition: invalid base");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function validateFogTile(tile, def) {
|
|
604
|
+
if (typeof tile !== "object" || tile === null) {
|
|
605
|
+
throw new Error("Invalid fog tile: expected an object");
|
|
606
|
+
}
|
|
607
|
+
const t = tile;
|
|
608
|
+
if (!isSafeInteger(t["x"]) || !isSafeInteger(t["y"])) {
|
|
609
|
+
throw new Error("Invalid fog tile: coordinates must be safe integers");
|
|
610
|
+
}
|
|
611
|
+
if (!tileIntersectsBounds(t["x"], t["y"], def)) {
|
|
612
|
+
throw new Error("Invalid fog tile: coordinates outside bounds");
|
|
613
|
+
}
|
|
614
|
+
if (typeof t["data"] !== "string" || !isCanonicalBase64(t["data"])) {
|
|
615
|
+
throw new Error("Invalid fog tile: invalid data");
|
|
616
|
+
}
|
|
617
|
+
const decoded = decodeBase64(t["data"]);
|
|
618
|
+
if (decoded.length !== TILE_BYTES) {
|
|
619
|
+
throw new Error("Invalid fog tile: decoded data wrong length");
|
|
620
|
+
}
|
|
621
|
+
if (isTileBase(decoded, def.base)) {
|
|
622
|
+
throw new Error("Invalid fog tile: base-value tiles must be omitted");
|
|
623
|
+
}
|
|
624
|
+
const canonical = new Uint8Array(decoded);
|
|
625
|
+
canonicalizeEdgePadding(canonical, def, t["x"], t["y"]);
|
|
626
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
627
|
+
if (decoded[i] !== canonical[i]) {
|
|
628
|
+
throw new Error("Invalid fog tile: non-canonical edge padding");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function validateFogState(state) {
|
|
633
|
+
if (typeof state !== "object" || state === null) {
|
|
634
|
+
throw new Error("Invalid fog state: expected an object");
|
|
635
|
+
}
|
|
636
|
+
const s = state;
|
|
637
|
+
validateFogDefinition(s["definition"]);
|
|
638
|
+
const def = s["definition"];
|
|
639
|
+
if (!Array.isArray(s["tiles"])) {
|
|
640
|
+
throw new Error("Invalid fog state: tiles must be an array");
|
|
641
|
+
}
|
|
642
|
+
const tiles = s["tiles"];
|
|
643
|
+
if (tiles.length > FOG_MAX_TILES) {
|
|
644
|
+
throw new Error(`Invalid fog state: too many tiles (${tiles.length} > ${FOG_MAX_TILES})`);
|
|
645
|
+
}
|
|
646
|
+
const seen = /* @__PURE__ */ new Set();
|
|
647
|
+
for (const tile of tiles) {
|
|
648
|
+
validateFogTile(tile, def);
|
|
649
|
+
const t = tile;
|
|
650
|
+
const key = `${t.x},${t.y}`;
|
|
651
|
+
if (seen.has(key)) {
|
|
652
|
+
throw new Error(`Invalid fog state: duplicate tile at (${t.x}, ${t.y})`);
|
|
653
|
+
}
|
|
654
|
+
seen.add(key);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function recommendedFogCellSize(bounds) {
|
|
658
|
+
let cellSize = 1;
|
|
659
|
+
while (true) {
|
|
660
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
661
|
+
const minTX = Math.floor(bounds.x / tileWorldSize);
|
|
662
|
+
const minTY = Math.floor(bounds.y / tileWorldSize);
|
|
663
|
+
const maxTX = Math.ceil((bounds.x + bounds.w) / tileWorldSize) - 1;
|
|
664
|
+
const maxTY = Math.ceil((bounds.y + bounds.h) / tileWorldSize) - 1;
|
|
665
|
+
const tw = maxTX - minTX + 1;
|
|
666
|
+
const th = maxTY - minTY + 1;
|
|
667
|
+
if (tw * th <= FOG_MAX_TILES) return cellSize;
|
|
668
|
+
cellSize++;
|
|
669
|
+
if (cellSize > 1e4) return cellSize;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function tileKey(x, y) {
|
|
673
|
+
return `${x},${y}`;
|
|
674
|
+
}
|
|
675
|
+
function worldToCell(worldX, worldY, cellSize) {
|
|
676
|
+
const cellX = Math.floor(worldX / cellSize);
|
|
677
|
+
const cellY = Math.floor(worldY / cellSize);
|
|
678
|
+
const tx = Math.floor(cellX / FOG_TILE_CELLS);
|
|
679
|
+
const ty = Math.floor(cellY / FOG_TILE_CELLS);
|
|
680
|
+
let col = cellX - tx * FOG_TILE_CELLS;
|
|
681
|
+
let row = cellY - ty * FOG_TILE_CELLS;
|
|
682
|
+
if (col < 0) col += FOG_TILE_CELLS;
|
|
683
|
+
if (row < 0) row += FOG_TILE_CELLS;
|
|
684
|
+
return { tx, ty, col, row };
|
|
685
|
+
}
|
|
686
|
+
function rasterizeRegion(state, region, operation) {
|
|
687
|
+
const { definition } = state;
|
|
688
|
+
const bitValue = operation === "reveal";
|
|
689
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
690
|
+
for (const tile of state.tiles) {
|
|
691
|
+
tileMap.set(tileKey(tile.x, tile.y), decodeBase64(tile.data));
|
|
692
|
+
}
|
|
693
|
+
const baseFill = isBaseValue(definition.base);
|
|
694
|
+
const affectedTiles = /* @__PURE__ */ new Set();
|
|
695
|
+
const setCellIfInBounds = (worldX, worldY) => {
|
|
696
|
+
if (worldX < definition.bounds.x || worldY < definition.bounds.y || worldX >= definition.bounds.x + definition.bounds.w || worldY >= definition.bounds.y + definition.bounds.h) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const { tx, ty, col, row } = worldToCell(worldX, worldY, definition.cellSize);
|
|
700
|
+
if (!tileIntersectsBounds(tx, ty, definition)) return;
|
|
701
|
+
const key = tileKey(tx, ty);
|
|
702
|
+
let bytes = tileMap.get(key);
|
|
703
|
+
if (!bytes) {
|
|
704
|
+
bytes = createTileBytes(baseFill);
|
|
705
|
+
tileMap.set(key, bytes);
|
|
706
|
+
} else if (!affectedTiles.has(key)) {
|
|
707
|
+
const clone = new Uint8Array(bytes);
|
|
708
|
+
tileMap.set(key, clone);
|
|
709
|
+
bytes = clone;
|
|
710
|
+
}
|
|
711
|
+
affectedTiles.add(key);
|
|
712
|
+
setBit(bytes, col, row, bitValue);
|
|
713
|
+
};
|
|
714
|
+
switch (region.kind) {
|
|
715
|
+
case "brush":
|
|
716
|
+
rasterizeBrush(region.points, region.radius, definition, setCellIfInBounds);
|
|
717
|
+
break;
|
|
718
|
+
case "rectangle":
|
|
719
|
+
rasterizeRectangle(region.from, region.to, definition, setCellIfInBounds);
|
|
720
|
+
break;
|
|
721
|
+
case "polygon":
|
|
722
|
+
rasterizePolygon(region.points, definition, setCellIfInBounds);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
if (affectedTiles.size === 0) return { changed: [], noop: true };
|
|
726
|
+
const changedTiles = [];
|
|
727
|
+
let hasChange = false;
|
|
728
|
+
for (const key of affectedTiles) {
|
|
729
|
+
const bytes = tileMap.get(key);
|
|
730
|
+
const [txStr, tyStr] = key.split(",");
|
|
731
|
+
const tx = Number(txStr);
|
|
732
|
+
const ty = Number(tyStr);
|
|
733
|
+
canonicalizeEdgePadding(bytes, definition, tx, ty);
|
|
734
|
+
const newData = encodeBase64(bytes);
|
|
735
|
+
const originalTile = state.tiles.find((t) => t.x === tx && t.y === ty);
|
|
736
|
+
if (originalTile) {
|
|
737
|
+
if (originalTile.data !== newData) {
|
|
738
|
+
hasChange = true;
|
|
739
|
+
changedTiles.push({ x: tx, y: ty, data: newData });
|
|
740
|
+
}
|
|
741
|
+
} else if (!isTileBase(bytes, definition.base)) {
|
|
742
|
+
hasChange = true;
|
|
743
|
+
changedTiles.push({ x: tx, y: ty, data: newData });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!hasChange) return { changed: [], noop: true };
|
|
747
|
+
return { changed: changedTiles, noop: false };
|
|
748
|
+
}
|
|
749
|
+
function applyRasterResult(state, result) {
|
|
750
|
+
if (result.noop) return state;
|
|
751
|
+
const changedMap = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const t of result.changed) changedMap.set(tileKey(t.x, t.y), t);
|
|
753
|
+
const tiles = [];
|
|
754
|
+
const seen = /* @__PURE__ */ new Set();
|
|
755
|
+
for (const tile of state.tiles) {
|
|
756
|
+
const key = tileKey(tile.x, tile.y);
|
|
757
|
+
seen.add(key);
|
|
758
|
+
const changed = changedMap.get(key);
|
|
759
|
+
if (changed) {
|
|
760
|
+
const bytes = decodeBase64(changed.data);
|
|
761
|
+
if (!isTileBase(bytes, state.definition.base)) {
|
|
762
|
+
tiles.push(changed);
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
tiles.push(tile);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
for (const t of result.changed) {
|
|
769
|
+
const key = tileKey(t.x, t.y);
|
|
770
|
+
if (seen.has(key)) continue;
|
|
771
|
+
const bytes = decodeBase64(t.data);
|
|
772
|
+
if (!isTileBase(bytes, state.definition.base)) {
|
|
773
|
+
tiles.push(t);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (tiles.length > FOG_MAX_TILES) {
|
|
777
|
+
throw new Error(`Fog tile cap exceeded: ${tiles.length} > ${FOG_MAX_TILES}`);
|
|
778
|
+
}
|
|
779
|
+
return { definition: state.definition, tiles };
|
|
780
|
+
}
|
|
781
|
+
function sampleAndSimplify(points, tolerance) {
|
|
782
|
+
if (points.length <= 2) return [...points];
|
|
783
|
+
const sampled = [points[0]];
|
|
784
|
+
let lastSampled = points[0];
|
|
785
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
786
|
+
const p = points[i];
|
|
787
|
+
const dx = p.x - lastSampled.x;
|
|
788
|
+
const dy = p.y - lastSampled.y;
|
|
789
|
+
if (dx * dx + dy * dy >= tolerance * tolerance) {
|
|
790
|
+
sampled.push(p);
|
|
791
|
+
lastSampled = p;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
sampled.push(points[points.length - 1]);
|
|
795
|
+
return sampled;
|
|
796
|
+
}
|
|
797
|
+
function rasterizeBrush(points, radius, def, setCell) {
|
|
798
|
+
if (points.length === 0) return;
|
|
799
|
+
const simplified = sampleAndSimplify(points, def.cellSize * 0.5);
|
|
800
|
+
for (let i = 0; i < simplified.length; i++) {
|
|
801
|
+
const p = simplified[i];
|
|
802
|
+
rasterizeDisc(p.x, p.y, radius, def, setCell);
|
|
803
|
+
if (i > 0) {
|
|
804
|
+
const prev = simplified[i - 1];
|
|
805
|
+
rasterizeCapsuleSegment(prev.x, prev.y, p.x, p.y, radius, def, setCell);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function rasterizeDisc(cx, cy, radius, def, setCell) {
|
|
810
|
+
const minX = Math.max(def.bounds.x, cx - radius);
|
|
811
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, cx + radius);
|
|
812
|
+
const minY = Math.max(def.bounds.y, cy - radius);
|
|
813
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, cy + radius);
|
|
814
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
815
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
816
|
+
const r2 = radius * radius;
|
|
817
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
818
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
819
|
+
const cellCenterX = wx + def.cellSize * 0.5;
|
|
820
|
+
const cellCenterY = wy + def.cellSize * 0.5;
|
|
821
|
+
const dx = cellCenterX - cx;
|
|
822
|
+
const dy = cellCenterY - cy;
|
|
823
|
+
if (dx * dx + dy * dy <= r2) {
|
|
824
|
+
setCell(wx, wy);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function rasterizeCapsuleSegment(x1, y1, x2, y2, radius, def, setCell) {
|
|
830
|
+
const segDx = x2 - x1;
|
|
831
|
+
const segDy = y2 - y1;
|
|
832
|
+
const segLen2 = segDx * segDx + segDy * segDy;
|
|
833
|
+
if (segLen2 === 0) return;
|
|
834
|
+
const minX = Math.max(def.bounds.x, Math.min(x1, x2) - radius);
|
|
835
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, Math.max(x1, x2) + radius);
|
|
836
|
+
const minY = Math.max(def.bounds.y, Math.min(y1, y2) - radius);
|
|
837
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, Math.max(y1, y2) + radius);
|
|
838
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
839
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
840
|
+
const r2 = radius * radius;
|
|
841
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
842
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
843
|
+
const cellCenterX = wx + def.cellSize * 0.5;
|
|
844
|
+
const cellCenterY = wy + def.cellSize * 0.5;
|
|
845
|
+
const t = Math.max(
|
|
846
|
+
0,
|
|
847
|
+
Math.min(1, ((cellCenterX - x1) * segDx + (cellCenterY - y1) * segDy) / segLen2)
|
|
848
|
+
);
|
|
849
|
+
const projX = x1 + t * segDx;
|
|
850
|
+
const projY = y1 + t * segDy;
|
|
851
|
+
const dx = cellCenterX - projX;
|
|
852
|
+
const dy = cellCenterY - projY;
|
|
853
|
+
if (dx * dx + dy * dy <= r2) {
|
|
854
|
+
setCell(wx, wy);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function rasterizeRectangle(from, to, def, setCell) {
|
|
860
|
+
const minX = Math.max(def.bounds.x, Math.min(from.x, to.x));
|
|
861
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, Math.max(from.x, to.x));
|
|
862
|
+
const minY = Math.max(def.bounds.y, Math.min(from.y, to.y));
|
|
863
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, Math.max(from.y, to.y));
|
|
864
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
865
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
866
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
867
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
868
|
+
setCell(wx, wy);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function rasterizePolygon(points, def, setCell) {
|
|
873
|
+
if (points.length < 3) return;
|
|
874
|
+
let minX = Infinity;
|
|
875
|
+
let maxX = -Infinity;
|
|
876
|
+
let minY = Infinity;
|
|
877
|
+
let maxY = -Infinity;
|
|
878
|
+
for (const p of points) {
|
|
879
|
+
if (p.x < minX) minX = p.x;
|
|
880
|
+
if (p.x > maxX) maxX = p.x;
|
|
881
|
+
if (p.y < minY) minY = p.y;
|
|
882
|
+
if (p.y > maxY) maxY = p.y;
|
|
883
|
+
}
|
|
884
|
+
minX = Math.max(def.bounds.x, minX);
|
|
885
|
+
maxX = Math.min(def.bounds.x + def.bounds.w - 1, maxX);
|
|
886
|
+
minY = Math.max(def.bounds.y, minY);
|
|
887
|
+
maxY = Math.min(def.bounds.y + def.bounds.h - 1, maxY);
|
|
888
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
889
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
890
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
891
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
892
|
+
const cx = wx + def.cellSize * 0.5;
|
|
893
|
+
const cy = wy + def.cellSize * 0.5;
|
|
894
|
+
if (pointInPolygon(cx, cy, points)) {
|
|
895
|
+
setCell(wx, wy);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function pointInPolygon(x, y, polygon) {
|
|
901
|
+
let inside = false;
|
|
902
|
+
const n2 = polygon.length;
|
|
903
|
+
for (let i = 0, j = n2 - 1; i < n2; j = i++) {
|
|
904
|
+
const pi = polygon[i];
|
|
905
|
+
const pj = polygon[j];
|
|
906
|
+
if (pi.y > y !== pj.y > y && x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x) {
|
|
907
|
+
inside = !inside;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return inside;
|
|
911
|
+
}
|
|
912
|
+
|
|
457
913
|
// src/core/state-serializer.ts
|
|
458
|
-
var CURRENT_VERSION =
|
|
914
|
+
var CURRENT_VERSION = 3;
|
|
459
915
|
var ELEMENT_TYPES = [
|
|
460
916
|
"stroke",
|
|
461
917
|
"note",
|
|
@@ -467,7 +923,7 @@ var ELEMENT_TYPES = [
|
|
|
467
923
|
"grid",
|
|
468
924
|
"template"
|
|
469
925
|
];
|
|
470
|
-
function exportState(elements, camera, layers = [], activeLayerId) {
|
|
926
|
+
function exportState(elements, camera, layers = [], activeLayerId, fog) {
|
|
471
927
|
const state = {
|
|
472
928
|
version: CURRENT_VERSION,
|
|
473
929
|
camera: {
|
|
@@ -484,6 +940,7 @@ function exportState(elements, camera, layers = [], activeLayerId) {
|
|
|
484
940
|
layers: layers.map((l) => ({ ...l }))
|
|
485
941
|
};
|
|
486
942
|
if (activeLayerId) state.activeLayerId = activeLayerId;
|
|
943
|
+
if (fog) state.fog = structuredClone(fog);
|
|
487
944
|
return state;
|
|
488
945
|
}
|
|
489
946
|
function parseState(json) {
|
|
@@ -561,6 +1018,9 @@ function validateState(data) {
|
|
|
561
1018
|
}
|
|
562
1019
|
}
|
|
563
1020
|
cleanBindings(elements);
|
|
1021
|
+
if (obj["fog"] !== void 0 && obj["fog"] !== null) {
|
|
1022
|
+
validateFogState(obj["fog"]);
|
|
1023
|
+
}
|
|
564
1024
|
}
|
|
565
1025
|
function validateElement(el) {
|
|
566
1026
|
if (!isRecord(el)) {
|
|
@@ -706,12 +1166,14 @@ var AutoSave = class {
|
|
|
706
1166
|
this.key = options.key ?? DEFAULT_KEY;
|
|
707
1167
|
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
708
1168
|
this.layerManager = options.layerManager;
|
|
1169
|
+
this.fogManager = options.fogManager;
|
|
709
1170
|
this.adapter = options.adapter ?? new LocalStorageAdapter();
|
|
710
1171
|
this.onError = options.onError;
|
|
711
1172
|
}
|
|
712
1173
|
key;
|
|
713
1174
|
debounceMs;
|
|
714
1175
|
layerManager;
|
|
1176
|
+
fogManager;
|
|
715
1177
|
adapter;
|
|
716
1178
|
timerId = null;
|
|
717
1179
|
unsubscribers = [];
|
|
@@ -729,6 +1191,9 @@ var AutoSave = class {
|
|
|
729
1191
|
if (this.layerManager) {
|
|
730
1192
|
this.unsubscribers.push(this.layerManager.on("change", schedule));
|
|
731
1193
|
}
|
|
1194
|
+
if (this.fogManager) {
|
|
1195
|
+
this.unsubscribers.push(this.fogManager.on("change", schedule));
|
|
1196
|
+
}
|
|
732
1197
|
}
|
|
733
1198
|
stop() {
|
|
734
1199
|
this.cancelPending();
|
|
@@ -765,7 +1230,8 @@ var AutoSave = class {
|
|
|
765
1230
|
this.saving = true;
|
|
766
1231
|
try {
|
|
767
1232
|
const layers = this.layerManager?.snapshot() ?? [];
|
|
768
|
-
const
|
|
1233
|
+
const fog = this.fogManager?.getState() ?? void 0;
|
|
1234
|
+
const state = exportState(this.store.snapshot(), this.camera, layers, void 0, fog);
|
|
769
1235
|
await this.adapter.save(this.key, JSON.stringify(state));
|
|
770
1236
|
} catch (e) {
|
|
771
1237
|
this.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -5097,6 +5563,8 @@ var MinimapController = class {
|
|
|
5097
5563
|
// read the `viewport` parameter. Assigned in the constructor body instead.
|
|
5098
5564
|
htmlPainters;
|
|
5099
5565
|
scene = null;
|
|
5566
|
+
fogRenderer = null;
|
|
5567
|
+
fogUnsub = null;
|
|
5100
5568
|
frameId = null;
|
|
5101
5569
|
debounceTimer = null;
|
|
5102
5570
|
dragging = false;
|
|
@@ -5111,6 +5579,15 @@ var MinimapController = class {
|
|
|
5111
5579
|
this.renderScene();
|
|
5112
5580
|
this.requestDraw();
|
|
5113
5581
|
}
|
|
5582
|
+
setFogRenderer(renderer) {
|
|
5583
|
+
if (this.disposed) return;
|
|
5584
|
+
if (this.fogUnsub) {
|
|
5585
|
+
this.fogUnsub();
|
|
5586
|
+
this.fogUnsub = null;
|
|
5587
|
+
}
|
|
5588
|
+
this.fogRenderer = renderer;
|
|
5589
|
+
this.invalidateScene();
|
|
5590
|
+
}
|
|
5114
5591
|
requestDraw() {
|
|
5115
5592
|
if (this.disposed || this.frameId !== null) return;
|
|
5116
5593
|
this.frameId = this.requestFrame(this.draw);
|
|
@@ -5172,8 +5649,15 @@ var MinimapController = class {
|
|
|
5172
5649
|
}
|
|
5173
5650
|
currentMapping() {
|
|
5174
5651
|
const viewportRect = this.viewport.getVisibleRect();
|
|
5175
|
-
|
|
5176
|
-
|
|
5652
|
+
let mapping = getElementsBoundingBox(this.sceneElements());
|
|
5653
|
+
mapping = mapping ? unionBounds(mapping, viewportRect) : viewportRect;
|
|
5654
|
+
if (this.fogRenderer?.isVisible()) {
|
|
5655
|
+
const fogState = this.fogRenderer.getState();
|
|
5656
|
+
if (fogState) {
|
|
5657
|
+
mapping = unionBounds(mapping, fogState.definition.bounds);
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
return mapping;
|
|
5177
5661
|
}
|
|
5178
5662
|
onViewChanged() {
|
|
5179
5663
|
if (this.disposed) return;
|
|
@@ -5231,6 +5715,23 @@ var MinimapController = class {
|
|
|
5231
5715
|
ctx.drawImage(layerCanvas, 0, 0);
|
|
5232
5716
|
ctx.restore();
|
|
5233
5717
|
}
|
|
5718
|
+
if (this.fogRenderer?.isVisible()) {
|
|
5719
|
+
const fogState = this.fogRenderer.getState();
|
|
5720
|
+
const fogMode = this.fogRenderer.getViewMode();
|
|
5721
|
+
if (fogState && (fogMode === "editor" || fogMode === "player")) {
|
|
5722
|
+
ctx.save();
|
|
5723
|
+
ctx.setTransform(
|
|
5724
|
+
dpr * transform.scale,
|
|
5725
|
+
0,
|
|
5726
|
+
0,
|
|
5727
|
+
dpr * transform.scale,
|
|
5728
|
+
dpr * transform.offsetX,
|
|
5729
|
+
dpr * transform.offsetY
|
|
5730
|
+
);
|
|
5731
|
+
this.fogRenderer.renderForExport(ctx, fogState, fogMode);
|
|
5732
|
+
ctx.restore();
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5234
5735
|
this.scene = { canvas: sceneCanvas, transform, mapping };
|
|
5235
5736
|
}
|
|
5236
5737
|
renderLayerElements(ctx, elements, t, dpr) {
|
|
@@ -5343,9 +5844,15 @@ var Minimap = class {
|
|
|
5343
5844
|
this.canvas = canvas;
|
|
5344
5845
|
this.controller = new MinimapController(viewport, canvas, { width: WIDTH, height: HEIGHT });
|
|
5345
5846
|
}
|
|
5847
|
+
setFogRenderer(renderer) {
|
|
5848
|
+
this.controller.setFogRenderer(renderer);
|
|
5849
|
+
}
|
|
5346
5850
|
scheduleDraw() {
|
|
5347
5851
|
this.controller.requestDraw();
|
|
5348
5852
|
}
|
|
5853
|
+
invalidateScene() {
|
|
5854
|
+
this.controller.invalidateScene();
|
|
5855
|
+
}
|
|
5349
5856
|
destroy() {
|
|
5350
5857
|
this.controller.dispose();
|
|
5351
5858
|
this.canvas.remove();
|
|
@@ -6667,6 +7174,548 @@ async function renderHtmlElements(elements, options) {
|
|
|
6667
7174
|
return sources;
|
|
6668
7175
|
}
|
|
6669
7176
|
|
|
7177
|
+
// src/fog/fog-style.ts
|
|
7178
|
+
var DEFAULT_PROCEDURAL_OPACITY = 0.6;
|
|
7179
|
+
var DEFAULT_PROCEDURAL_SCALE = 256;
|
|
7180
|
+
var DEFAULT_PROCEDURAL_SEED = 0;
|
|
7181
|
+
var DEFAULT_PROCEDURAL_DETAIL = 2;
|
|
7182
|
+
var DEFAULT_PROCEDURAL_TINT = "#ffffff";
|
|
7183
|
+
var MIN_SCALE = 64;
|
|
7184
|
+
var MAX_SCALE = 1024;
|
|
7185
|
+
var MIN_DETAIL = 1;
|
|
7186
|
+
var MAX_DETAIL = 4;
|
|
7187
|
+
var MAX_SEED = 65535;
|
|
7188
|
+
function clamp(value, min, max) {
|
|
7189
|
+
return Math.max(min, Math.min(max, value));
|
|
7190
|
+
}
|
|
7191
|
+
function finiteOrDefault(value, defaultValue) {
|
|
7192
|
+
return value !== void 0 && Number.isFinite(value) ? value : defaultValue;
|
|
7193
|
+
}
|
|
7194
|
+
function resolveFogStyle(style, legacyColor, defaultColor) {
|
|
7195
|
+
if (style && style.kind === "procedural") {
|
|
7196
|
+
const opacity = clamp(finiteOrDefault(style.opacity, DEFAULT_PROCEDURAL_OPACITY), 0, 1);
|
|
7197
|
+
const scale = clamp(
|
|
7198
|
+
finiteOrDefault(style.scale, DEFAULT_PROCEDURAL_SCALE),
|
|
7199
|
+
MIN_SCALE,
|
|
7200
|
+
MAX_SCALE
|
|
7201
|
+
);
|
|
7202
|
+
const seed = clamp(
|
|
7203
|
+
Math.floor(finiteOrDefault(style.seed, DEFAULT_PROCEDURAL_SEED)),
|
|
7204
|
+
0,
|
|
7205
|
+
MAX_SEED
|
|
7206
|
+
);
|
|
7207
|
+
const detail = clamp(
|
|
7208
|
+
Math.floor(finiteOrDefault(style.detail, DEFAULT_PROCEDURAL_DETAIL)),
|
|
7209
|
+
MIN_DETAIL,
|
|
7210
|
+
MAX_DETAIL
|
|
7211
|
+
);
|
|
7212
|
+
return {
|
|
7213
|
+
kind: "procedural",
|
|
7214
|
+
backdrop: style.backdrop,
|
|
7215
|
+
// `tint` is required for typed callers. Keep a visible runtime fallback for
|
|
7216
|
+
// untyped/older JavaScript hosts instead of producing a flat same-color overlay.
|
|
7217
|
+
tint: style.tint || DEFAULT_PROCEDURAL_TINT,
|
|
7218
|
+
opacity,
|
|
7219
|
+
scale,
|
|
7220
|
+
seed,
|
|
7221
|
+
detail
|
|
7222
|
+
};
|
|
7223
|
+
}
|
|
7224
|
+
if (style && (!style.kind || style.kind === "solid")) {
|
|
7225
|
+
return { kind: "solid", color: style.color };
|
|
7226
|
+
}
|
|
7227
|
+
return { kind: "solid", color: legacyColor ?? defaultColor };
|
|
7228
|
+
}
|
|
7229
|
+
|
|
7230
|
+
// src/fog/fog-procedural-tile.ts
|
|
7231
|
+
var TILE_PX = 128;
|
|
7232
|
+
function xorshift32(state) {
|
|
7233
|
+
state ^= state << 13;
|
|
7234
|
+
state ^= state >>> 17;
|
|
7235
|
+
state ^= state << 5;
|
|
7236
|
+
return state >>> 0;
|
|
7237
|
+
}
|
|
7238
|
+
function seedState(seed) {
|
|
7239
|
+
return seed * 2654435761 + 1 >>> 0 || 1;
|
|
7240
|
+
}
|
|
7241
|
+
function smoothstep(t) {
|
|
7242
|
+
return t * t * (3 - 2 * t);
|
|
7243
|
+
}
|
|
7244
|
+
function lerp(a, b, t) {
|
|
7245
|
+
return a + (b - a) * t;
|
|
7246
|
+
}
|
|
7247
|
+
function generateGradients(size, prngState) {
|
|
7248
|
+
const count = size * size;
|
|
7249
|
+
const gx = new Float32Array(count);
|
|
7250
|
+
const gy = new Float32Array(count);
|
|
7251
|
+
let s = prngState;
|
|
7252
|
+
for (let i = 0; i < count; i++) {
|
|
7253
|
+
s = xorshift32(s);
|
|
7254
|
+
const angle = (s >>> 0) / 4294967296 * Math.PI * 2;
|
|
7255
|
+
gx[i] = Math.cos(angle);
|
|
7256
|
+
gy[i] = Math.sin(angle);
|
|
7257
|
+
}
|
|
7258
|
+
return { gx, gy, state: s };
|
|
7259
|
+
}
|
|
7260
|
+
function perlinNoise(px, py, gridSize, gx, gy) {
|
|
7261
|
+
const gx0 = Math.floor(px) % gridSize;
|
|
7262
|
+
const gy0 = Math.floor(py) % gridSize;
|
|
7263
|
+
const gx1 = (gx0 + 1) % gridSize;
|
|
7264
|
+
const gy1 = (gy0 + 1) % gridSize;
|
|
7265
|
+
const fx = px - Math.floor(px);
|
|
7266
|
+
const fy = py - Math.floor(py);
|
|
7267
|
+
const sx = smoothstep(fx);
|
|
7268
|
+
const sy = smoothstep(fy);
|
|
7269
|
+
const dot = (ix, iy, dx, dy) => {
|
|
7270
|
+
const idx = iy * gridSize + ix;
|
|
7271
|
+
return gx[idx] * dx + gy[idx] * dy;
|
|
7272
|
+
};
|
|
7273
|
+
const n00 = dot(gx0, gy0, fx, fy);
|
|
7274
|
+
const n10 = dot(gx1, gy0, fx - 1, fy);
|
|
7275
|
+
const n01 = dot(gx0, gy1, fx, fy - 1);
|
|
7276
|
+
const n11 = dot(gx1, gy1, fx - 1, fy - 1);
|
|
7277
|
+
return lerp(lerp(n00, n10, sx), lerp(n01, n11, sx), sy);
|
|
7278
|
+
}
|
|
7279
|
+
function layeredNoise(x, y, octaves, gridSize, gx, gy) {
|
|
7280
|
+
let value = 0;
|
|
7281
|
+
let amplitude = 1;
|
|
7282
|
+
let frequency = 1;
|
|
7283
|
+
let maxAmplitude = 0;
|
|
7284
|
+
for (let o = 0; o < octaves; o++) {
|
|
7285
|
+
value += perlinNoise(x * frequency, y * frequency, gridSize * frequency, gx, gy) * amplitude;
|
|
7286
|
+
maxAmplitude += amplitude;
|
|
7287
|
+
amplitude *= 0.5;
|
|
7288
|
+
frequency *= 2;
|
|
7289
|
+
}
|
|
7290
|
+
return (value / maxAmplitude + 1) * 0.5;
|
|
7291
|
+
}
|
|
7292
|
+
function generateProceduralTile(style) {
|
|
7293
|
+
const gridSize = 8;
|
|
7294
|
+
const maxFreq = gridSize * (1 << style.detail - 1);
|
|
7295
|
+
const { gx, gy } = generateGradients(maxFreq, seedState(style.seed));
|
|
7296
|
+
const data = new Uint8ClampedArray(TILE_PX * TILE_PX * 4);
|
|
7297
|
+
for (let py = 0; py < TILE_PX; py++) {
|
|
7298
|
+
for (let px = 0; px < TILE_PX; px++) {
|
|
7299
|
+
const nx = px / TILE_PX * gridSize;
|
|
7300
|
+
const ny = py / TILE_PX * gridSize;
|
|
7301
|
+
const n2 = layeredNoise(nx, ny, style.detail, gridSize, gx, gy);
|
|
7302
|
+
const alpha = Math.round(n2 * style.opacity * 255);
|
|
7303
|
+
const idx = (py * TILE_PX + px) * 4;
|
|
7304
|
+
data[idx] = 255;
|
|
7305
|
+
data[idx + 1] = 255;
|
|
7306
|
+
data[idx + 2] = 255;
|
|
7307
|
+
data[idx + 3] = alpha;
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7310
|
+
return { data, width: TILE_PX, height: TILE_PX };
|
|
7311
|
+
}
|
|
7312
|
+
var tileCache = /* @__PURE__ */ new Map();
|
|
7313
|
+
var MAX_CACHED_TILES = 16;
|
|
7314
|
+
function getCachedProceduralTile(style) {
|
|
7315
|
+
const key = `${style.opacity}\0${style.seed}\0${style.detail}`;
|
|
7316
|
+
const cached = tileCache.get(key);
|
|
7317
|
+
if (cached) return cached;
|
|
7318
|
+
const tile = generateProceduralTile(style);
|
|
7319
|
+
if (tileCache.size >= MAX_CACHED_TILES) {
|
|
7320
|
+
const oldest = tileCache.keys().next().value;
|
|
7321
|
+
if (oldest !== void 0) tileCache.delete(oldest);
|
|
7322
|
+
}
|
|
7323
|
+
tileCache.set(key, tile);
|
|
7324
|
+
return tile;
|
|
7325
|
+
}
|
|
7326
|
+
function clearProceduralTileCache() {
|
|
7327
|
+
tileCache.clear();
|
|
7328
|
+
}
|
|
7329
|
+
|
|
7330
|
+
// src/fog/fog-renderer.ts
|
|
7331
|
+
var DEFAULT_EDITOR_COLOR = "rgba(30, 40, 60, 0.45)";
|
|
7332
|
+
var DEFAULT_PLAYER_COLOR = "#0b1020";
|
|
7333
|
+
var FogRenderer = class {
|
|
7334
|
+
tileCache = /* @__PURE__ */ new Map();
|
|
7335
|
+
patternCache = /* @__PURE__ */ new Map();
|
|
7336
|
+
state = null;
|
|
7337
|
+
viewMode = "off";
|
|
7338
|
+
dirty = true;
|
|
7339
|
+
editorStyle;
|
|
7340
|
+
playerStyle;
|
|
7341
|
+
constructor(options = {}) {
|
|
7342
|
+
this.editorStyle = resolveFogStyle(
|
|
7343
|
+
options.editorStyle,
|
|
7344
|
+
options.editorColor,
|
|
7345
|
+
DEFAULT_EDITOR_COLOR
|
|
7346
|
+
);
|
|
7347
|
+
this.playerStyle = resolveFogStyle(
|
|
7348
|
+
options.playerStyle,
|
|
7349
|
+
options.playerColor,
|
|
7350
|
+
DEFAULT_PLAYER_COLOR
|
|
7351
|
+
);
|
|
7352
|
+
}
|
|
7353
|
+
setState(state) {
|
|
7354
|
+
this.state = state;
|
|
7355
|
+
this.dirty = true;
|
|
7356
|
+
}
|
|
7357
|
+
setViewMode(mode) {
|
|
7358
|
+
if (mode === this.viewMode) return;
|
|
7359
|
+
this.viewMode = mode;
|
|
7360
|
+
this.dirty = true;
|
|
7361
|
+
}
|
|
7362
|
+
getState() {
|
|
7363
|
+
return this.state;
|
|
7364
|
+
}
|
|
7365
|
+
getViewMode() {
|
|
7366
|
+
return this.viewMode;
|
|
7367
|
+
}
|
|
7368
|
+
markDirty() {
|
|
7369
|
+
this.dirty = true;
|
|
7370
|
+
}
|
|
7371
|
+
isDirty() {
|
|
7372
|
+
return this.dirty;
|
|
7373
|
+
}
|
|
7374
|
+
isVisible() {
|
|
7375
|
+
return this.viewMode !== "off" && this.state !== null;
|
|
7376
|
+
}
|
|
7377
|
+
getResolvedStyle(mode) {
|
|
7378
|
+
return mode === "editor" ? this.editorStyle : this.playerStyle;
|
|
7379
|
+
}
|
|
7380
|
+
render(ctx, camera, viewportWidth, viewportHeight, _dpr) {
|
|
7381
|
+
if (!this.state || this.viewMode === "off") return;
|
|
7382
|
+
const def = this.state.definition;
|
|
7383
|
+
const cellSize = def.cellSize;
|
|
7384
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
7385
|
+
const mode = this.viewMode === "editor" ? "editor" : "player";
|
|
7386
|
+
const style = this.getResolvedStyle(mode);
|
|
7387
|
+
const proceduralStyle = style.kind === "procedural" ? style : null;
|
|
7388
|
+
const color = style.kind === "procedural" ? normalizeCanvasColor(
|
|
7389
|
+
ctx,
|
|
7390
|
+
style.backdrop,
|
|
7391
|
+
mode === "player" ? DEFAULT_PLAYER_COLOR : DEFAULT_EDITOR_COLOR
|
|
7392
|
+
) : style.color;
|
|
7393
|
+
const safetyColor = proceduralStyle && mode === "player" ? DEFAULT_PLAYER_COLOR : null;
|
|
7394
|
+
const worldBounds = getVisibleWorld(camera, viewportWidth, viewportHeight);
|
|
7395
|
+
const minTX = Math.floor(Math.max(def.bounds.x, worldBounds.x) / tileWorldSize);
|
|
7396
|
+
const minTY = Math.floor(Math.max(def.bounds.y, worldBounds.y) / tileWorldSize);
|
|
7397
|
+
const maxTX = Math.floor(
|
|
7398
|
+
Math.min(def.bounds.x + def.bounds.w - 1, worldBounds.x + worldBounds.w) / tileWorldSize
|
|
7399
|
+
);
|
|
7400
|
+
const maxTY = Math.floor(
|
|
7401
|
+
Math.min(def.bounds.y + def.bounds.h - 1, worldBounds.y + worldBounds.h) / tileWorldSize
|
|
7402
|
+
);
|
|
7403
|
+
ctx.save();
|
|
7404
|
+
ctx.translate(camera.position.x, camera.position.y);
|
|
7405
|
+
ctx.scale(camera.zoom, camera.zoom);
|
|
7406
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
7407
|
+
for (const tile of this.state.tiles) {
|
|
7408
|
+
tileMap.set(`${tile.x},${tile.y}`, tile.data);
|
|
7409
|
+
}
|
|
7410
|
+
const baseCovered = def.base === "covered";
|
|
7411
|
+
for (let ty = minTY; ty <= maxTY; ty++) {
|
|
7412
|
+
for (let tx = minTX; tx <= maxTX; tx++) {
|
|
7413
|
+
const key = `${tx},${ty}`;
|
|
7414
|
+
const data = tileMap.get(key);
|
|
7415
|
+
const tileWorldX = tx * tileWorldSize;
|
|
7416
|
+
const tileWorldY = ty * tileWorldSize;
|
|
7417
|
+
if (!data && baseCovered) {
|
|
7418
|
+
ctx.fillStyle = color;
|
|
7419
|
+
const clipX = Math.max(tileWorldX, def.bounds.x);
|
|
7420
|
+
const clipY = Math.max(tileWorldY, def.bounds.y);
|
|
7421
|
+
const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
|
|
7422
|
+
const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
|
|
7423
|
+
if (clipR > clipX && clipB > clipY) {
|
|
7424
|
+
if (safetyColor) {
|
|
7425
|
+
ctx.fillStyle = safetyColor;
|
|
7426
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7427
|
+
}
|
|
7428
|
+
ctx.fillStyle = color;
|
|
7429
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7430
|
+
if (proceduralStyle) {
|
|
7431
|
+
this.paintProceduralOverlay(
|
|
7432
|
+
ctx,
|
|
7433
|
+
proceduralStyle,
|
|
7434
|
+
clipX,
|
|
7435
|
+
clipY,
|
|
7436
|
+
clipR - clipX,
|
|
7437
|
+
clipB - clipY
|
|
7438
|
+
);
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
continue;
|
|
7442
|
+
}
|
|
7443
|
+
if (!data && !baseCovered) {
|
|
7444
|
+
continue;
|
|
7445
|
+
}
|
|
7446
|
+
if (data) {
|
|
7447
|
+
if (safetyColor) this.renderTile(ctx, data, tx, ty, def, safetyColor);
|
|
7448
|
+
this.renderTile(ctx, data, tx, ty, def, color);
|
|
7449
|
+
if (proceduralStyle) {
|
|
7450
|
+
this.renderTileProceduralOverlay(ctx, data, tx, ty, def, proceduralStyle);
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7455
|
+
ctx.restore();
|
|
7456
|
+
this.dirty = false;
|
|
7457
|
+
}
|
|
7458
|
+
renderForExport(ctx, state, mode, color, style) {
|
|
7459
|
+
const def = state.definition;
|
|
7460
|
+
const cellSize = def.cellSize;
|
|
7461
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
7462
|
+
const resolved = style ? resolveFogStyle(
|
|
7463
|
+
style,
|
|
7464
|
+
void 0,
|
|
7465
|
+
mode === "editor" ? DEFAULT_EDITOR_COLOR : DEFAULT_PLAYER_COLOR
|
|
7466
|
+
) : this.getResolvedStyle(mode);
|
|
7467
|
+
const proceduralStyle = !color && resolved.kind === "procedural" ? resolved : null;
|
|
7468
|
+
const fogColor = color ?? (proceduralStyle ? normalizeCanvasColor(
|
|
7469
|
+
ctx,
|
|
7470
|
+
proceduralStyle.backdrop,
|
|
7471
|
+
mode === "player" ? DEFAULT_PLAYER_COLOR : DEFAULT_EDITOR_COLOR
|
|
7472
|
+
) : resolved.kind === "solid" ? resolved.color : resolved.backdrop);
|
|
7473
|
+
const safetyColor = proceduralStyle && mode === "player" ? DEFAULT_PLAYER_COLOR : null;
|
|
7474
|
+
const baseCovered = def.base === "covered";
|
|
7475
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
7476
|
+
for (const tile of state.tiles) {
|
|
7477
|
+
tileMap.set(`${tile.x},${tile.y}`, tile.data);
|
|
7478
|
+
}
|
|
7479
|
+
const minTX = Math.floor(def.bounds.x / tileWorldSize);
|
|
7480
|
+
const minTY = Math.floor(def.bounds.y / tileWorldSize);
|
|
7481
|
+
const maxTX = Math.floor((def.bounds.x + def.bounds.w - 1) / tileWorldSize);
|
|
7482
|
+
const maxTY = Math.floor((def.bounds.y + def.bounds.h - 1) / tileWorldSize);
|
|
7483
|
+
for (let ty = minTY; ty <= maxTY; ty++) {
|
|
7484
|
+
for (let tx = minTX; tx <= maxTX; tx++) {
|
|
7485
|
+
const key = `${tx},${ty}`;
|
|
7486
|
+
const data = tileMap.get(key);
|
|
7487
|
+
const tileWorldX = tx * tileWorldSize;
|
|
7488
|
+
const tileWorldY = ty * tileWorldSize;
|
|
7489
|
+
if (!data && baseCovered) {
|
|
7490
|
+
ctx.fillStyle = fogColor;
|
|
7491
|
+
const clipX = Math.max(tileWorldX, def.bounds.x);
|
|
7492
|
+
const clipY = Math.max(tileWorldY, def.bounds.y);
|
|
7493
|
+
const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
|
|
7494
|
+
const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
|
|
7495
|
+
if (clipR > clipX && clipB > clipY) {
|
|
7496
|
+
if (safetyColor) {
|
|
7497
|
+
ctx.fillStyle = safetyColor;
|
|
7498
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7499
|
+
}
|
|
7500
|
+
ctx.fillStyle = fogColor;
|
|
7501
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7502
|
+
if (proceduralStyle) {
|
|
7503
|
+
this.paintProceduralOverlay(
|
|
7504
|
+
ctx,
|
|
7505
|
+
proceduralStyle,
|
|
7506
|
+
clipX,
|
|
7507
|
+
clipY,
|
|
7508
|
+
clipR - clipX,
|
|
7509
|
+
clipB - clipY
|
|
7510
|
+
);
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
continue;
|
|
7514
|
+
}
|
|
7515
|
+
if (data) {
|
|
7516
|
+
if (safetyColor) this.renderTileForExport(ctx, data, tx, ty, def, safetyColor);
|
|
7517
|
+
this.renderTileForExport(ctx, data, tx, ty, def, fogColor);
|
|
7518
|
+
if (proceduralStyle) {
|
|
7519
|
+
this.renderTileProceduralOverlay(ctx, data, tx, ty, def, proceduralStyle);
|
|
7520
|
+
}
|
|
7521
|
+
}
|
|
7522
|
+
}
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
dispose() {
|
|
7526
|
+
this.tileCache.clear();
|
|
7527
|
+
this.patternCache.clear();
|
|
7528
|
+
clearProceduralTileCache();
|
|
7529
|
+
this.state = null;
|
|
7530
|
+
}
|
|
7531
|
+
getOrCreatePattern(ctx, style, worldScale) {
|
|
7532
|
+
const key = `${style.backdrop}\0${style.tint}\0${style.opacity}\0${style.scale}\0${style.seed}\0${style.detail}\0${worldScale}`;
|
|
7533
|
+
const cached = this.patternCache.get(key);
|
|
7534
|
+
if (cached !== void 0) return cached;
|
|
7535
|
+
let pattern = null;
|
|
7536
|
+
try {
|
|
7537
|
+
const tileData = getCachedProceduralTile(style);
|
|
7538
|
+
pattern = this.createPatternFromTileData(ctx, tileData, style, worldScale);
|
|
7539
|
+
} catch {
|
|
7540
|
+
}
|
|
7541
|
+
if (this.patternCache.size >= 32) {
|
|
7542
|
+
const oldest = this.patternCache.keys().next().value;
|
|
7543
|
+
if (oldest !== void 0) this.patternCache.delete(oldest);
|
|
7544
|
+
}
|
|
7545
|
+
this.patternCache.set(key, pattern);
|
|
7546
|
+
return pattern;
|
|
7547
|
+
}
|
|
7548
|
+
createPatternFromTileData(ctx, tileData, style, worldScale) {
|
|
7549
|
+
if (typeof document === "undefined") return null;
|
|
7550
|
+
const sourceCanvas = document.createElement("canvas");
|
|
7551
|
+
sourceCanvas.width = tileData.width;
|
|
7552
|
+
sourceCanvas.height = tileData.height;
|
|
7553
|
+
const sourceCtx = sourceCanvas.getContext("2d");
|
|
7554
|
+
if (!sourceCtx) return null;
|
|
7555
|
+
const imageData = new ImageData(
|
|
7556
|
+
new Uint8ClampedArray(tileData.data),
|
|
7557
|
+
tileData.width,
|
|
7558
|
+
tileData.height
|
|
7559
|
+
);
|
|
7560
|
+
sourceCtx.putImageData(imageData, 0, 0);
|
|
7561
|
+
sourceCtx.globalCompositeOperation = "source-in";
|
|
7562
|
+
sourceCtx.fillStyle = normalizeCanvasColor(sourceCtx, style.tint, "#ffffff");
|
|
7563
|
+
sourceCtx.fillRect(0, 0, tileData.width, tileData.height);
|
|
7564
|
+
sourceCtx.globalCompositeOperation = "source-over";
|
|
7565
|
+
const patternScale = style.scale * worldScale / tileData.width;
|
|
7566
|
+
const pattern = ctx.createPattern(sourceCanvas, "repeat");
|
|
7567
|
+
if (pattern && typeof pattern.setTransform === "function" && typeof DOMMatrix !== "undefined") {
|
|
7568
|
+
try {
|
|
7569
|
+
pattern.setTransform(new DOMMatrix([patternScale, 0, 0, patternScale, 0, 0]));
|
|
7570
|
+
return pattern;
|
|
7571
|
+
} catch {
|
|
7572
|
+
}
|
|
7573
|
+
}
|
|
7574
|
+
const patternPx = Math.round(style.scale * worldScale);
|
|
7575
|
+
if (patternPx < 1) return null;
|
|
7576
|
+
const patternCanvas = document.createElement("canvas");
|
|
7577
|
+
patternCanvas.width = patternPx;
|
|
7578
|
+
patternCanvas.height = patternPx;
|
|
7579
|
+
const patternCtx = patternCanvas.getContext("2d");
|
|
7580
|
+
if (!patternCtx) return null;
|
|
7581
|
+
patternCtx.drawImage(sourceCanvas, 0, 0, patternPx, patternPx);
|
|
7582
|
+
return ctx.createPattern(patternCanvas, "repeat");
|
|
7583
|
+
}
|
|
7584
|
+
paintProceduralOverlay(ctx, style, x, y, w, h) {
|
|
7585
|
+
const pattern = this.getOrCreatePattern(ctx, style, 1);
|
|
7586
|
+
if (!pattern) return;
|
|
7587
|
+
ctx.save();
|
|
7588
|
+
ctx.fillStyle = pattern;
|
|
7589
|
+
ctx.fillRect(x, y, w, h);
|
|
7590
|
+
ctx.restore();
|
|
7591
|
+
}
|
|
7592
|
+
renderTileProceduralOverlay(ctx, data, tx, ty, def, style) {
|
|
7593
|
+
const cellSize = def.cellSize;
|
|
7594
|
+
const tileWorldX = tx * FOG_TILE_CELLS * cellSize;
|
|
7595
|
+
const tileWorldY = ty * FOG_TILE_CELLS * cellSize;
|
|
7596
|
+
const pattern = this.getOrCreatePattern(ctx, style, 1);
|
|
7597
|
+
if (!pattern) return;
|
|
7598
|
+
const bytes = decodeBase64(data);
|
|
7599
|
+
ctx.save();
|
|
7600
|
+
ctx.fillStyle = pattern;
|
|
7601
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7602
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7603
|
+
const cellWorldX = tileWorldX + col * cellSize;
|
|
7604
|
+
const cellWorldY = tileWorldY + row * cellSize;
|
|
7605
|
+
if (cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= def.bounds.x + def.bounds.w || cellWorldY >= def.bounds.y + def.bounds.h) {
|
|
7606
|
+
continue;
|
|
7607
|
+
}
|
|
7608
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7609
|
+
const byteIndex = index >> 3;
|
|
7610
|
+
const bitIndex = 7 - (index & 7);
|
|
7611
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7612
|
+
if (!revealed) {
|
|
7613
|
+
ctx.fillRect(cellWorldX, cellWorldY, cellSize, cellSize);
|
|
7614
|
+
}
|
|
7615
|
+
}
|
|
7616
|
+
}
|
|
7617
|
+
ctx.restore();
|
|
7618
|
+
}
|
|
7619
|
+
tileRaster(data, color) {
|
|
7620
|
+
const key = `${color}\0${data}`;
|
|
7621
|
+
const cached = this.tileCache.get(key);
|
|
7622
|
+
if (cached) return cached;
|
|
7623
|
+
if (typeof document === "undefined") return null;
|
|
7624
|
+
const canvas = document.createElement("canvas");
|
|
7625
|
+
canvas.width = FOG_TILE_CELLS;
|
|
7626
|
+
canvas.height = FOG_TILE_CELLS;
|
|
7627
|
+
const ctx = canvas.getContext("2d");
|
|
7628
|
+
if (!ctx) return null;
|
|
7629
|
+
const bytes = decodeBase64(data);
|
|
7630
|
+
ctx.fillStyle = color;
|
|
7631
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7632
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7633
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7634
|
+
const byteIndex = index >> 3;
|
|
7635
|
+
const bitIndex = 7 - (index & 7);
|
|
7636
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7637
|
+
if (!revealed) ctx.fillRect(col, row, 1, 1);
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
if (this.tileCache.size >= 256) {
|
|
7641
|
+
const oldest = this.tileCache.keys().next().value;
|
|
7642
|
+
if (oldest !== void 0) this.tileCache.delete(oldest);
|
|
7643
|
+
}
|
|
7644
|
+
this.tileCache.set(key, canvas);
|
|
7645
|
+
return canvas;
|
|
7646
|
+
}
|
|
7647
|
+
renderTile(ctx, data, tx, ty, def, color) {
|
|
7648
|
+
const cellSize = def.cellSize;
|
|
7649
|
+
const tileWorldX = tx * FOG_TILE_CELLS * cellSize;
|
|
7650
|
+
const tileWorldY = ty * FOG_TILE_CELLS * cellSize;
|
|
7651
|
+
const raster = this.tileRaster(data, color);
|
|
7652
|
+
if (raster) {
|
|
7653
|
+
ctx.save();
|
|
7654
|
+
ctx.beginPath();
|
|
7655
|
+
ctx.rect(def.bounds.x, def.bounds.y, def.bounds.w, def.bounds.h);
|
|
7656
|
+
ctx.clip();
|
|
7657
|
+
ctx.imageSmoothingEnabled = false;
|
|
7658
|
+
ctx.drawImage(
|
|
7659
|
+
raster,
|
|
7660
|
+
tileWorldX,
|
|
7661
|
+
tileWorldY,
|
|
7662
|
+
FOG_TILE_CELLS * cellSize,
|
|
7663
|
+
FOG_TILE_CELLS * cellSize
|
|
7664
|
+
);
|
|
7665
|
+
ctx.restore();
|
|
7666
|
+
return;
|
|
7667
|
+
}
|
|
7668
|
+
const bytes = decodeBase64(data);
|
|
7669
|
+
ctx.fillStyle = color;
|
|
7670
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7671
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7672
|
+
const cellWorldX = tileWorldX + col * cellSize;
|
|
7673
|
+
const cellWorldY = tileWorldY + row * cellSize;
|
|
7674
|
+
if (cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= def.bounds.x + def.bounds.w || cellWorldY >= def.bounds.y + def.bounds.h) {
|
|
7675
|
+
continue;
|
|
7676
|
+
}
|
|
7677
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7678
|
+
const byteIndex = index >> 3;
|
|
7679
|
+
const bitIndex = 7 - (index & 7);
|
|
7680
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7681
|
+
const covered = !revealed;
|
|
7682
|
+
if (covered) {
|
|
7683
|
+
ctx.fillRect(cellWorldX, cellWorldY, cellSize, cellSize);
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
}
|
|
7687
|
+
}
|
|
7688
|
+
renderTileForExport(ctx, data, tx, ty, def, color) {
|
|
7689
|
+
this.renderTile(ctx, data, tx, ty, def, color);
|
|
7690
|
+
}
|
|
7691
|
+
};
|
|
7692
|
+
function getVisibleWorld(camera, viewportWidth, viewportHeight) {
|
|
7693
|
+
const topLeft = camera.screenToWorld({ x: 0, y: 0 });
|
|
7694
|
+
const bottomRight = camera.screenToWorld({ x: viewportWidth, y: viewportHeight });
|
|
7695
|
+
return {
|
|
7696
|
+
x: topLeft.x,
|
|
7697
|
+
y: topLeft.y,
|
|
7698
|
+
w: bottomRight.x - topLeft.x,
|
|
7699
|
+
h: bottomRight.y - topLeft.y
|
|
7700
|
+
};
|
|
7701
|
+
}
|
|
7702
|
+
function normalizeCanvasColor(ctx, value, fallback) {
|
|
7703
|
+
const previous = ctx.fillStyle;
|
|
7704
|
+
try {
|
|
7705
|
+
ctx.fillStyle = "#010203";
|
|
7706
|
+
ctx.fillStyle = value;
|
|
7707
|
+
const first = ctx.fillStyle;
|
|
7708
|
+
ctx.fillStyle = "#040506";
|
|
7709
|
+
ctx.fillStyle = value;
|
|
7710
|
+
const second = ctx.fillStyle;
|
|
7711
|
+
return typeof first === "string" && first === second ? first : fallback;
|
|
7712
|
+
} catch {
|
|
7713
|
+
return fallback;
|
|
7714
|
+
} finally {
|
|
7715
|
+
ctx.fillStyle = previous;
|
|
7716
|
+
}
|
|
7717
|
+
}
|
|
7718
|
+
|
|
6670
7719
|
// src/canvas/export-image.ts
|
|
6671
7720
|
var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
|
|
6672
7721
|
var DEFAULT_MAX_DIMENSION = 16384;
|
|
@@ -7079,6 +8128,16 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
7079
8128
|
renderGridForBounds(ctx, grid, bounds);
|
|
7080
8129
|
ctx.restore();
|
|
7081
8130
|
}
|
|
8131
|
+
if (options.fog) {
|
|
8132
|
+
const fogRenderer = new FogRenderer();
|
|
8133
|
+
fogRenderer.renderForExport(
|
|
8134
|
+
ctx,
|
|
8135
|
+
options.fog.state,
|
|
8136
|
+
options.fog.mode,
|
|
8137
|
+
options.fog.color,
|
|
8138
|
+
options.fog.style
|
|
8139
|
+
);
|
|
8140
|
+
}
|
|
7082
8141
|
const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
|
|
7083
8142
|
return new Promise((resolve) => {
|
|
7084
8143
|
canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
|
|
@@ -7460,6 +8519,33 @@ async function exportSvg(store, options = {}, layerManager) {
|
|
|
7460
8519
|
const opacity = layerManager?.getLayer?.(grid.layerId)?.opacity ?? 1;
|
|
7461
8520
|
body += opacity === 1 ? emitted : `<g opacity="${n(opacity)}">${emitted}</g>`;
|
|
7462
8521
|
}
|
|
8522
|
+
if (options.fog && typeof document !== "undefined") {
|
|
8523
|
+
const fogState = options.fog.state;
|
|
8524
|
+
const fogW = Math.max(1, Math.ceil(bounds.w));
|
|
8525
|
+
const fogH = Math.max(1, Math.ceil(bounds.h));
|
|
8526
|
+
const fogCanvas = document.createElement("canvas");
|
|
8527
|
+
fogCanvas.width = fogW;
|
|
8528
|
+
fogCanvas.height = fogH;
|
|
8529
|
+
const fogCtx = fogCanvas.getContext("2d");
|
|
8530
|
+
if (fogCtx) {
|
|
8531
|
+
fogCtx.translate(-bounds.x, -bounds.y);
|
|
8532
|
+
const fogRenderer = new FogRenderer();
|
|
8533
|
+
fogRenderer.renderForExport(
|
|
8534
|
+
fogCtx,
|
|
8535
|
+
fogState,
|
|
8536
|
+
options.fog.mode,
|
|
8537
|
+
options.fog.color,
|
|
8538
|
+
options.fog.style
|
|
8539
|
+
);
|
|
8540
|
+
try {
|
|
8541
|
+
const fogDataUri = fogCanvas.toDataURL("image/png");
|
|
8542
|
+
if (fogDataUri.startsWith("data:")) {
|
|
8543
|
+
body += `<image href="${esc(fogDataUri)}" x="${n(bounds.x)}" y="${n(bounds.y)}" width="${n(bounds.w)}" height="${n(bounds.h)}" />`;
|
|
8544
|
+
}
|
|
8545
|
+
} catch {
|
|
8546
|
+
}
|
|
8547
|
+
}
|
|
8548
|
+
}
|
|
7463
8549
|
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>`;
|
|
7464
8550
|
}
|
|
7465
8551
|
function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
|
|
@@ -8297,6 +9383,7 @@ var RenderLoop = class {
|
|
|
8297
9383
|
layerCache;
|
|
8298
9384
|
marginViewport;
|
|
8299
9385
|
hybridSurface;
|
|
9386
|
+
fogRenderer;
|
|
8300
9387
|
activeDrawingLayerId = null;
|
|
8301
9388
|
gridCacheDirty = true;
|
|
8302
9389
|
// set on recenter/viewport-change; consumed by the grid block
|
|
@@ -8320,6 +9407,7 @@ var RenderLoop = class {
|
|
|
8320
9407
|
this.layerCache = deps.layerCache;
|
|
8321
9408
|
this.marginViewport = deps.marginViewport;
|
|
8322
9409
|
this.hybridSurface = deps.hybridSurface;
|
|
9410
|
+
this.fogRenderer = deps.fogRenderer;
|
|
8323
9411
|
}
|
|
8324
9412
|
requestRender() {
|
|
8325
9413
|
this.needsRender = true;
|
|
@@ -8629,9 +9717,12 @@ var RenderLoop = class {
|
|
|
8629
9717
|
group.push(element);
|
|
8630
9718
|
}
|
|
8631
9719
|
const activeTool = this.toolManager.activeTool;
|
|
8632
|
-
const
|
|
9720
|
+
const fogVisible = this.fogRenderer?.isVisible() ?? false;
|
|
9721
|
+
const fogOrder = visibleElements.length + 1;
|
|
9722
|
+
const overlayOrder = fogVisible ? fogOrder + 1 : visibleElements.length + 1;
|
|
8633
9723
|
const hasOverlay = activeTool?.renderOverlay !== void 0 || this.overlays.size > 0;
|
|
8634
|
-
if (
|
|
9724
|
+
if (fogVisible) hybridOrders.add(fogOrder);
|
|
9725
|
+
if (hasOverlay && (hybridActive || fogVisible)) hybridOrders.add(overlayOrder);
|
|
8635
9726
|
this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
|
|
8636
9727
|
for (const [layerId, elements] of this.layerGroups) {
|
|
8637
9728
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
@@ -8745,8 +9836,18 @@ var RenderLoop = class {
|
|
|
8745
9836
|
}
|
|
8746
9837
|
hybridCtx.restore();
|
|
8747
9838
|
}
|
|
9839
|
+
if (fogVisible && this.fogRenderer) {
|
|
9840
|
+
const fogCtx = this.hybridSurface.getContext(fogOrder);
|
|
9841
|
+
if (fogCtx) {
|
|
9842
|
+
fogCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
9843
|
+
fogCtx.save();
|
|
9844
|
+
fogCtx.scale(dpr, dpr);
|
|
9845
|
+
this.fogRenderer.render(fogCtx, this.camera, cssWidth, cssHeight, dpr);
|
|
9846
|
+
fogCtx.restore();
|
|
9847
|
+
}
|
|
9848
|
+
}
|
|
8748
9849
|
const overlayT0 = performance.now();
|
|
8749
|
-
if (hybridActive && hasOverlay) {
|
|
9850
|
+
if ((hybridActive || fogVisible) && hasOverlay) {
|
|
8750
9851
|
const overlayCtx = this.hybridSurface.getContext(overlayOrder);
|
|
8751
9852
|
if (overlayCtx) {
|
|
8752
9853
|
overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
@@ -9757,6 +10858,248 @@ var ElementActivation = class {
|
|
|
9757
10858
|
}
|
|
9758
10859
|
};
|
|
9759
10860
|
|
|
10861
|
+
// src/fog/fog-command.ts
|
|
10862
|
+
var FogRegionCommand = class {
|
|
10863
|
+
constructor(manager, before, after) {
|
|
10864
|
+
this.manager = manager;
|
|
10865
|
+
this.before = before;
|
|
10866
|
+
this.after = after;
|
|
10867
|
+
}
|
|
10868
|
+
execute(_store) {
|
|
10869
|
+
this.manager.applyTilesDirect(this.after);
|
|
10870
|
+
}
|
|
10871
|
+
undo(_store) {
|
|
10872
|
+
this.manager.applyTilesDirect(this.before);
|
|
10873
|
+
}
|
|
10874
|
+
};
|
|
10875
|
+
var FogResetCommand = class {
|
|
10876
|
+
constructor(manager, before, after) {
|
|
10877
|
+
this.manager = manager;
|
|
10878
|
+
this.before = before;
|
|
10879
|
+
this.after = after;
|
|
10880
|
+
}
|
|
10881
|
+
execute(_store) {
|
|
10882
|
+
this.manager.restoreHistoryState(this.after);
|
|
10883
|
+
}
|
|
10884
|
+
undo(_store) {
|
|
10885
|
+
this.manager.restoreHistoryState(this.before);
|
|
10886
|
+
}
|
|
10887
|
+
};
|
|
10888
|
+
|
|
10889
|
+
// src/fog/fog-manager.ts
|
|
10890
|
+
function defaultIdFactory() {
|
|
10891
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
10892
|
+
return crypto.randomUUID();
|
|
10893
|
+
}
|
|
10894
|
+
return `fog-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
10895
|
+
}
|
|
10896
|
+
var FogManager = class {
|
|
10897
|
+
state = null;
|
|
10898
|
+
viewMode = "off";
|
|
10899
|
+
idFactory;
|
|
10900
|
+
onCommand;
|
|
10901
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
10902
|
+
viewListeners = /* @__PURE__ */ new Set();
|
|
10903
|
+
constructor(options = {}) {
|
|
10904
|
+
this.idFactory = options.idFactory ?? defaultIdFactory;
|
|
10905
|
+
this.onCommand = options.onCommand;
|
|
10906
|
+
}
|
|
10907
|
+
getState() {
|
|
10908
|
+
if (!this.state) return null;
|
|
10909
|
+
return {
|
|
10910
|
+
definition: { ...this.state.definition, bounds: { ...this.state.definition.bounds } },
|
|
10911
|
+
tiles: this.state.tiles.map((t) => ({ ...t }))
|
|
10912
|
+
};
|
|
10913
|
+
}
|
|
10914
|
+
getViewMode() {
|
|
10915
|
+
return this.viewMode;
|
|
10916
|
+
}
|
|
10917
|
+
initialize(options) {
|
|
10918
|
+
const base = options.base ?? "covered";
|
|
10919
|
+
const cellSize = options.cellSize ?? recommendedFogCellSize(options.bounds);
|
|
10920
|
+
const generation = this.idFactory();
|
|
10921
|
+
const newState = {
|
|
10922
|
+
definition: {
|
|
10923
|
+
version: 1,
|
|
10924
|
+
generation,
|
|
10925
|
+
bounds: { ...options.bounds },
|
|
10926
|
+
cellSize,
|
|
10927
|
+
tileCells: FOG_TILE_CELLS,
|
|
10928
|
+
base
|
|
10929
|
+
},
|
|
10930
|
+
tiles: []
|
|
10931
|
+
};
|
|
10932
|
+
validateFogState(newState);
|
|
10933
|
+
const before = this.state;
|
|
10934
|
+
this.state = newState;
|
|
10935
|
+
const command = new FogResetCommand(this, before, newState);
|
|
10936
|
+
this.onCommand?.(command);
|
|
10937
|
+
this.notifyChange({ kind: "definition" });
|
|
10938
|
+
return structuredClone(newState);
|
|
10939
|
+
}
|
|
10940
|
+
loadState(state, meta) {
|
|
10941
|
+
if (state !== null) {
|
|
10942
|
+
validateFogState(state);
|
|
10943
|
+
this.state = structuredClone(state);
|
|
10944
|
+
} else {
|
|
10945
|
+
this.state = null;
|
|
10946
|
+
}
|
|
10947
|
+
this.notifyChange({
|
|
10948
|
+
kind: state === null ? "disable" : "definition",
|
|
10949
|
+
origin: meta?.origin
|
|
10950
|
+
});
|
|
10951
|
+
}
|
|
10952
|
+
/** Restores a historical visual state without reusing its causal generation id. */
|
|
10953
|
+
restoreHistoryState(state) {
|
|
10954
|
+
if (state === null) {
|
|
10955
|
+
this.loadState(null);
|
|
10956
|
+
return;
|
|
10957
|
+
}
|
|
10958
|
+
this.loadState({
|
|
10959
|
+
definition: { ...state.definition, generation: this.idFactory() },
|
|
10960
|
+
tiles: state.tiles
|
|
10961
|
+
});
|
|
10962
|
+
}
|
|
10963
|
+
setBounds(bounds) {
|
|
10964
|
+
if (!this.state) return;
|
|
10965
|
+
const def = this.state.definition;
|
|
10966
|
+
validateFogState({
|
|
10967
|
+
definition: { ...def, bounds: { ...bounds } },
|
|
10968
|
+
tiles: []
|
|
10969
|
+
});
|
|
10970
|
+
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;
|
|
10971
|
+
const nextDefinition = {
|
|
10972
|
+
...def,
|
|
10973
|
+
bounds: { ...bounds },
|
|
10974
|
+
generation: shrinks ? this.idFactory() : def.generation
|
|
10975
|
+
};
|
|
10976
|
+
const tiles = this.state.tiles.flatMap((tile) => {
|
|
10977
|
+
const tileWorldX = tile.x * FOG_TILE_CELLS * def.cellSize;
|
|
10978
|
+
const tileWorldY = tile.y * FOG_TILE_CELLS * def.cellSize;
|
|
10979
|
+
const tileWorldW = FOG_TILE_CELLS * def.cellSize;
|
|
10980
|
+
const tileWorldH = FOG_TILE_CELLS * def.cellSize;
|
|
10981
|
+
const intersects2 = !(tileWorldX + tileWorldW <= bounds.x || tileWorldY + tileWorldH <= bounds.y || tileWorldX >= bounds.x + bounds.w || tileWorldY >= bounds.y + bounds.h);
|
|
10982
|
+
if (!intersects2) return [];
|
|
10983
|
+
const canonical = canonicalizeFogTile(tile, nextDefinition);
|
|
10984
|
+
return canonical ? [canonical] : [];
|
|
10985
|
+
});
|
|
10986
|
+
const before = this.state;
|
|
10987
|
+
this.state = {
|
|
10988
|
+
definition: nextDefinition,
|
|
10989
|
+
tiles
|
|
10990
|
+
};
|
|
10991
|
+
const command = new FogResetCommand(this, before, this.state);
|
|
10992
|
+
this.onCommand?.(command);
|
|
10993
|
+
this.notifyChange({ kind: "definition" });
|
|
10994
|
+
}
|
|
10995
|
+
reset(base) {
|
|
10996
|
+
if (!this.state) return;
|
|
10997
|
+
const before = this.state;
|
|
10998
|
+
const generation = this.idFactory();
|
|
10999
|
+
this.state = {
|
|
11000
|
+
definition: { ...this.state.definition, base, generation },
|
|
11001
|
+
tiles: []
|
|
11002
|
+
};
|
|
11003
|
+
const command = new FogResetCommand(this, before, this.state);
|
|
11004
|
+
this.onCommand?.(command);
|
|
11005
|
+
this.notifyChange({ kind: "reset" });
|
|
11006
|
+
}
|
|
11007
|
+
disable() {
|
|
11008
|
+
if (!this.state) return;
|
|
11009
|
+
const before = this.state;
|
|
11010
|
+
this.state = null;
|
|
11011
|
+
const command = new FogResetCommand(this, before, null);
|
|
11012
|
+
this.onCommand?.(command);
|
|
11013
|
+
this.notifyChange({ kind: "disable" });
|
|
11014
|
+
}
|
|
11015
|
+
setViewMode(mode) {
|
|
11016
|
+
if (mode === this.viewMode) return;
|
|
11017
|
+
this.viewMode = mode;
|
|
11018
|
+
this.notifyView({ mode });
|
|
11019
|
+
}
|
|
11020
|
+
applyRegion(region, operation) {
|
|
11021
|
+
if (!this.state) return;
|
|
11022
|
+
const result = rasterizeRegion(this.state, region, operation);
|
|
11023
|
+
if (result.noop) return;
|
|
11024
|
+
const before = this.collectTiles(result.changed);
|
|
11025
|
+
const newState = applyRasterResult(this.state, result);
|
|
11026
|
+
this.state = newState;
|
|
11027
|
+
const command = new FogRegionCommand(this, before, result.changed);
|
|
11028
|
+
this.onCommand?.(command);
|
|
11029
|
+
this.notifyChange({
|
|
11030
|
+
kind: "tiles",
|
|
11031
|
+
tiles: result.changed.map((t) => ({ x: t.x, y: t.y }))
|
|
11032
|
+
});
|
|
11033
|
+
}
|
|
11034
|
+
applyPatchDirect(patch, meta) {
|
|
11035
|
+
if (!this.state) return;
|
|
11036
|
+
const result = applyRasterResult(this.state, { changed: patch.tiles, noop: false });
|
|
11037
|
+
this.state = result;
|
|
11038
|
+
this.notifyChange({
|
|
11039
|
+
kind: "tiles",
|
|
11040
|
+
tiles: patch.tiles.map((t) => ({ x: t.x, y: t.y })),
|
|
11041
|
+
origin: meta?.origin
|
|
11042
|
+
});
|
|
11043
|
+
}
|
|
11044
|
+
applyTilesDirect(tiles) {
|
|
11045
|
+
if (!this.state) return;
|
|
11046
|
+
const result = applyRasterResult(this.state, { changed: tiles, noop: false });
|
|
11047
|
+
this.state = result;
|
|
11048
|
+
this.notifyChange({
|
|
11049
|
+
kind: "tiles",
|
|
11050
|
+
tiles: tiles.map((t) => ({ x: t.x, y: t.y }))
|
|
11051
|
+
});
|
|
11052
|
+
}
|
|
11053
|
+
on(event, listener) {
|
|
11054
|
+
if (event === "change") {
|
|
11055
|
+
const l2 = listener;
|
|
11056
|
+
this.changeListeners.add(l2);
|
|
11057
|
+
return () => this.changeListeners.delete(l2);
|
|
11058
|
+
}
|
|
11059
|
+
const l = listener;
|
|
11060
|
+
this.viewListeners.add(l);
|
|
11061
|
+
return () => this.viewListeners.delete(l);
|
|
11062
|
+
}
|
|
11063
|
+
dispose() {
|
|
11064
|
+
this.changeListeners.clear();
|
|
11065
|
+
this.viewListeners.clear();
|
|
11066
|
+
}
|
|
11067
|
+
collectTiles(changed) {
|
|
11068
|
+
if (!this.state) return [];
|
|
11069
|
+
const result = [];
|
|
11070
|
+
for (const c of changed) {
|
|
11071
|
+
const existing = this.state.tiles.find((t) => t.x === c.x && t.y === c.y);
|
|
11072
|
+
if (existing) {
|
|
11073
|
+
result.push(existing);
|
|
11074
|
+
} else {
|
|
11075
|
+
const baseVal = this.state.definition.base === "revealed";
|
|
11076
|
+
result.push({
|
|
11077
|
+
x: c.x,
|
|
11078
|
+
y: c.y,
|
|
11079
|
+
data: encodeBase64(createTileBytes(baseVal))
|
|
11080
|
+
});
|
|
11081
|
+
}
|
|
11082
|
+
}
|
|
11083
|
+
return result;
|
|
11084
|
+
}
|
|
11085
|
+
notifyChange(event) {
|
|
11086
|
+
for (const listener of this.changeListeners) {
|
|
11087
|
+
try {
|
|
11088
|
+
listener(event);
|
|
11089
|
+
} catch {
|
|
11090
|
+
}
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
notifyView(event) {
|
|
11094
|
+
for (const listener of this.viewListeners) {
|
|
11095
|
+
try {
|
|
11096
|
+
listener(event);
|
|
11097
|
+
} catch {
|
|
11098
|
+
}
|
|
11099
|
+
}
|
|
11100
|
+
}
|
|
11101
|
+
};
|
|
11102
|
+
|
|
9760
11103
|
// src/canvas/viewport.ts
|
|
9761
11104
|
var EMPTY_IDS = [];
|
|
9762
11105
|
function noop2() {
|
|
@@ -9883,8 +11226,13 @@ var Viewport = class _Viewport {
|
|
|
9883
11226
|
});
|
|
9884
11227
|
}
|
|
9885
11228
|
this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
|
|
11229
|
+
this.fogManager = new FogManager({
|
|
11230
|
+
onCommand: (cmd) => this.history.push(cmd)
|
|
11231
|
+
});
|
|
11232
|
+
this.fogRenderer = new FogRenderer(options.fog);
|
|
9886
11233
|
if (options.minimap) {
|
|
9887
11234
|
this.minimap = new Minimap(this.wrapper, this);
|
|
11235
|
+
this.minimap.setFogRenderer(this.fogRenderer);
|
|
9888
11236
|
}
|
|
9889
11237
|
this.domNodeManager = new DomNodeManager({
|
|
9890
11238
|
domLayer: this.paintStack,
|
|
@@ -9914,7 +11262,18 @@ var Viewport = class _Viewport {
|
|
|
9914
11262
|
domNodeManager: this.domNodeManager,
|
|
9915
11263
|
layerCache,
|
|
9916
11264
|
marginViewport: this.marginViewport,
|
|
9917
|
-
hybridSurface: new HybridRenderSurface(this.paintStack)
|
|
11265
|
+
hybridSurface: new HybridRenderSurface(this.paintStack),
|
|
11266
|
+
fogRenderer: this.fogRenderer
|
|
11267
|
+
});
|
|
11268
|
+
this.fogManager.on("change", () => {
|
|
11269
|
+
this.fogRenderer.setState(this.fogManager.getState());
|
|
11270
|
+
this.renderLoop.requestRender();
|
|
11271
|
+
this.minimap?.invalidateScene();
|
|
11272
|
+
});
|
|
11273
|
+
this.fogManager.on("view", () => {
|
|
11274
|
+
this.fogRenderer.setViewMode(this.fogManager.getViewMode());
|
|
11275
|
+
this.renderLoop.requestRender();
|
|
11276
|
+
this.minimap?.invalidateScene();
|
|
9918
11277
|
});
|
|
9919
11278
|
this.unsubHtmlPainters = this.htmlPainters.onChange(() => this.onHtmlRegistryChanged());
|
|
9920
11279
|
this.unsubCamera = this.camera.onChange(() => {
|
|
@@ -10026,6 +11385,8 @@ var Viewport = class _Viewport {
|
|
|
10026
11385
|
_smartGuides = false;
|
|
10027
11386
|
_gridSize;
|
|
10028
11387
|
renderLoop;
|
|
11388
|
+
fogManager;
|
|
11389
|
+
fogRenderer;
|
|
10029
11390
|
domNodeManager;
|
|
10030
11391
|
interactMode;
|
|
10031
11392
|
onHtmlElementMount;
|
|
@@ -10061,6 +11422,9 @@ var Viewport = class _Viewport {
|
|
|
10061
11422
|
get ctx() {
|
|
10062
11423
|
return this.canvasEl.getContext("2d");
|
|
10063
11424
|
}
|
|
11425
|
+
get fog() {
|
|
11426
|
+
return this.fogManager;
|
|
11427
|
+
}
|
|
10064
11428
|
get snapToGrid() {
|
|
10065
11429
|
return this._snapToGrid;
|
|
10066
11430
|
}
|
|
@@ -10145,7 +11509,8 @@ var Viewport = class _Viewport {
|
|
|
10145
11509
|
this.store.snapshot(),
|
|
10146
11510
|
this.camera,
|
|
10147
11511
|
this.layerManager.snapshot(),
|
|
10148
|
-
this.layerManager.activeLayerId
|
|
11512
|
+
this.layerManager.activeLayerId,
|
|
11513
|
+
this.fogManager.getState()
|
|
10149
11514
|
);
|
|
10150
11515
|
}
|
|
10151
11516
|
exportJSON() {
|
|
@@ -10166,13 +11531,41 @@ var Viewport = class _Viewport {
|
|
|
10166
11531
|
const expected = base.expectedCanvasTypes ? /* @__PURE__ */ new Set([...declared, ...base.expectedCanvasTypes]) : declared;
|
|
10167
11532
|
return { ...base, htmlPainters: registry, expectedCanvasTypes: expected };
|
|
10168
11533
|
}
|
|
11534
|
+
/**
|
|
11535
|
+
* Carry constructor-configured fog presentation into both implicit exports and
|
|
11536
|
+
* explicit state/mode exports. Explicit style and legacy color overrides win.
|
|
11537
|
+
*/
|
|
11538
|
+
withFogDefaults(options) {
|
|
11539
|
+
const fog = options.fog;
|
|
11540
|
+
if (fog === false) return options;
|
|
11541
|
+
if (fog !== void 0) {
|
|
11542
|
+
if (fog.style !== void 0 || fog.color !== void 0) return options;
|
|
11543
|
+
return {
|
|
11544
|
+
...options,
|
|
11545
|
+
fog: { ...fog, style: this.fogRenderer.getResolvedStyle(fog.mode) }
|
|
11546
|
+
};
|
|
11547
|
+
}
|
|
11548
|
+
if (!this.fogRenderer.isVisible()) return options;
|
|
11549
|
+
const state = this.fogManager.getState();
|
|
11550
|
+
if (!state) return options;
|
|
11551
|
+
const mode = this.fogRenderer.getViewMode();
|
|
11552
|
+
return {
|
|
11553
|
+
...options,
|
|
11554
|
+
fog: { state, mode, style: this.fogRenderer.getResolvedStyle(mode) }
|
|
11555
|
+
};
|
|
11556
|
+
}
|
|
10169
11557
|
async exportImage(options) {
|
|
10170
|
-
|
|
11558
|
+
const opts = this.withFogDefaults(this.withHtmlDefaults(options));
|
|
11559
|
+
return exportImage(this.store, opts, this.layerManager);
|
|
10171
11560
|
}
|
|
10172
11561
|
async exportSVG(options) {
|
|
10173
|
-
|
|
11562
|
+
const opts = this.withFogDefaults(this.withHtmlDefaults(options));
|
|
11563
|
+
return exportSvg(this.store, opts, this.layerManager);
|
|
10174
11564
|
}
|
|
10175
11565
|
loadState(state) {
|
|
11566
|
+
if (state.fog != null) {
|
|
11567
|
+
validateFogState(state.fog);
|
|
11568
|
+
}
|
|
10176
11569
|
this.inputHandler.flushPendingHistory();
|
|
10177
11570
|
this.historyRecorder.pause();
|
|
10178
11571
|
this.noteEditor.destroy(this.store);
|
|
@@ -10207,6 +11600,7 @@ var Viewport = class _Viewport {
|
|
|
10207
11600
|
}
|
|
10208
11601
|
}
|
|
10209
11602
|
}
|
|
11603
|
+
this.fogManager.loadState(state.fog ?? null);
|
|
10210
11604
|
this.history.clear();
|
|
10211
11605
|
this.historyRecorder.resume();
|
|
10212
11606
|
this.camera.moveTo(state.camera.position.x, state.camera.position.y);
|
|
@@ -10618,6 +12012,8 @@ var Viewport = class _Viewport {
|
|
|
10618
12012
|
this.unsubToolRegister();
|
|
10619
12013
|
this.unsubRecorderEnd();
|
|
10620
12014
|
this.unsubHtmlPainters();
|
|
12015
|
+
this.fogManager.dispose();
|
|
12016
|
+
this.fogRenderer.dispose();
|
|
10621
12017
|
this.activation?.dispose();
|
|
10622
12018
|
this.activation = null;
|
|
10623
12019
|
this.activationListeners.clear();
|
|
@@ -11797,15 +13193,15 @@ function applyCameraView(camera, view, canvasW, canvasH) {
|
|
|
11797
13193
|
var DEFAULT_DURATION_MS3 = 400;
|
|
11798
13194
|
var FRAMED_EPSILON = 1e-6;
|
|
11799
13195
|
var easeOutCubic2 = (t) => 1 - Math.pow(1 - t, 3);
|
|
11800
|
-
function
|
|
13196
|
+
function lerp2(a, b, k) {
|
|
11801
13197
|
return a + (b - a) * k;
|
|
11802
13198
|
}
|
|
11803
13199
|
function lerpView(from, to, k) {
|
|
11804
13200
|
return {
|
|
11805
|
-
x:
|
|
11806
|
-
y:
|
|
11807
|
-
w:
|
|
11808
|
-
h:
|
|
13201
|
+
x: lerp2(from.x, to.x, k),
|
|
13202
|
+
y: lerp2(from.y, to.y, k),
|
|
13203
|
+
w: lerp2(from.w, to.w, k),
|
|
13204
|
+
h: lerp2(from.h, to.h, k)
|
|
11809
13205
|
};
|
|
11810
13206
|
}
|
|
11811
13207
|
function viewsClose(a, b) {
|
|
@@ -13059,7 +14455,7 @@ var PencilTool = class {
|
|
|
13059
14455
|
};
|
|
13060
14456
|
|
|
13061
14457
|
// src/elements/stroke-erase.ts
|
|
13062
|
-
function
|
|
14458
|
+
function lerp3(a, b, t) {
|
|
13063
14459
|
return {
|
|
13064
14460
|
x: a.x + (b.x - a.x) * t,
|
|
13065
14461
|
y: a.y + (b.y - a.y) * t,
|
|
@@ -13118,13 +14514,13 @@ function erasePoints(points, eraser, radius) {
|
|
|
13118
14514
|
erased = true;
|
|
13119
14515
|
if (tLo > 0) {
|
|
13120
14516
|
if (current.length === 0) current.push(a);
|
|
13121
|
-
current.push(
|
|
14517
|
+
current.push(lerp3(a, b, tLo));
|
|
13122
14518
|
flush();
|
|
13123
14519
|
} else {
|
|
13124
14520
|
flush();
|
|
13125
14521
|
}
|
|
13126
14522
|
if (tHi < 1) {
|
|
13127
|
-
current = [
|
|
14523
|
+
current = [lerp3(a, b, tHi), b];
|
|
13128
14524
|
}
|
|
13129
14525
|
}
|
|
13130
14526
|
flush();
|
|
@@ -15459,8 +16855,188 @@ var PingTool = class {
|
|
|
15459
16855
|
}
|
|
15460
16856
|
};
|
|
15461
16857
|
|
|
16858
|
+
// src/tools/fog-tool.ts
|
|
16859
|
+
var DEFAULT_RADIUS5 = 40;
|
|
16860
|
+
var MIN_POINT_DISTANCE = 4;
|
|
16861
|
+
var FogTool = class {
|
|
16862
|
+
name = "fog";
|
|
16863
|
+
drawing = false;
|
|
16864
|
+
points = [];
|
|
16865
|
+
startPoint = null;
|
|
16866
|
+
operation;
|
|
16867
|
+
shape;
|
|
16868
|
+
radius;
|
|
16869
|
+
manager;
|
|
16870
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
16871
|
+
constructor(manager, options = {}) {
|
|
16872
|
+
this.manager = manager;
|
|
16873
|
+
this.operation = options.operation ?? "reveal";
|
|
16874
|
+
this.shape = options.shape ?? "brush";
|
|
16875
|
+
this.radius = options.radius ?? DEFAULT_RADIUS5;
|
|
16876
|
+
}
|
|
16877
|
+
onActivate(ctx) {
|
|
16878
|
+
ctx.setCursor?.("crosshair");
|
|
16879
|
+
}
|
|
16880
|
+
onDeactivate(ctx) {
|
|
16881
|
+
this.cancelGesture(ctx);
|
|
16882
|
+
ctx.setCursor?.("default");
|
|
16883
|
+
}
|
|
16884
|
+
getOptions() {
|
|
16885
|
+
return {
|
|
16886
|
+
operation: this.operation,
|
|
16887
|
+
shape: this.shape,
|
|
16888
|
+
radius: this.radius
|
|
16889
|
+
};
|
|
16890
|
+
}
|
|
16891
|
+
setOptions(options) {
|
|
16892
|
+
if (options.operation !== void 0) this.operation = options.operation;
|
|
16893
|
+
if (options.shape !== void 0) this.shape = options.shape;
|
|
16894
|
+
if (options.radius !== void 0 && Number.isFinite(options.radius) && options.radius > 0) {
|
|
16895
|
+
this.radius = options.radius;
|
|
16896
|
+
}
|
|
16897
|
+
for (const listener of this.optionListeners) listener();
|
|
16898
|
+
}
|
|
16899
|
+
onOptionsChange(listener) {
|
|
16900
|
+
this.optionListeners.add(listener);
|
|
16901
|
+
return () => this.optionListeners.delete(listener);
|
|
16902
|
+
}
|
|
16903
|
+
onPointerDown(state, ctx) {
|
|
16904
|
+
if (this.drawing) return;
|
|
16905
|
+
this.drawing = true;
|
|
16906
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
16907
|
+
this.startPoint = world;
|
|
16908
|
+
this.points = [world];
|
|
16909
|
+
ctx.requestRender();
|
|
16910
|
+
}
|
|
16911
|
+
onPointerMove(state, ctx) {
|
|
16912
|
+
if (!this.drawing) return;
|
|
16913
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
16914
|
+
if (this.shape === "rectangle") {
|
|
16915
|
+
if (this.startPoint) this.points = [this.startPoint, world];
|
|
16916
|
+
} else {
|
|
16917
|
+
const last = this.points[this.points.length - 1];
|
|
16918
|
+
if (last) {
|
|
16919
|
+
const dx = world.x - last.x;
|
|
16920
|
+
const dy = world.y - last.y;
|
|
16921
|
+
if (dx * dx + dy * dy < MIN_POINT_DISTANCE * MIN_POINT_DISTANCE) return;
|
|
16922
|
+
}
|
|
16923
|
+
this.points.push(world);
|
|
16924
|
+
}
|
|
16925
|
+
ctx.requestRender();
|
|
16926
|
+
}
|
|
16927
|
+
onPointerUp(_state, ctx) {
|
|
16928
|
+
if (!this.drawing) return;
|
|
16929
|
+
this.drawing = false;
|
|
16930
|
+
const region = this.buildRegion();
|
|
16931
|
+
if (region) {
|
|
16932
|
+
this.manager.applyRegion(region, this.operation);
|
|
16933
|
+
}
|
|
16934
|
+
this.points = [];
|
|
16935
|
+
this.startPoint = null;
|
|
16936
|
+
ctx.requestRender();
|
|
16937
|
+
}
|
|
16938
|
+
onPointerCancel(_state, ctx) {
|
|
16939
|
+
this.cancelGesture(ctx);
|
|
16940
|
+
}
|
|
16941
|
+
onKeyDown(event, ctx) {
|
|
16942
|
+
if (event.key === "Escape" && this.drawing) {
|
|
16943
|
+
this.cancelGesture(ctx);
|
|
16944
|
+
return true;
|
|
16945
|
+
}
|
|
16946
|
+
return false;
|
|
16947
|
+
}
|
|
16948
|
+
renderOverlay(ctx) {
|
|
16949
|
+
if (!this.drawing || this.points.length === 0) return;
|
|
16950
|
+
ctx.save();
|
|
16951
|
+
ctx.strokeStyle = this.operation === "reveal" ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.4)";
|
|
16952
|
+
ctx.fillStyle = this.operation === "reveal" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)";
|
|
16953
|
+
ctx.lineWidth = 2;
|
|
16954
|
+
ctx.setLineDash([6, 4]);
|
|
16955
|
+
switch (this.shape) {
|
|
16956
|
+
case "brush":
|
|
16957
|
+
this.renderBrushPreview(ctx);
|
|
16958
|
+
break;
|
|
16959
|
+
case "rectangle":
|
|
16960
|
+
this.renderRectanglePreview(ctx);
|
|
16961
|
+
break;
|
|
16962
|
+
case "polygon":
|
|
16963
|
+
this.renderPolygonPreview(ctx);
|
|
16964
|
+
break;
|
|
16965
|
+
}
|
|
16966
|
+
ctx.restore();
|
|
16967
|
+
}
|
|
16968
|
+
buildRegion() {
|
|
16969
|
+
switch (this.shape) {
|
|
16970
|
+
case "brush": {
|
|
16971
|
+
if (this.points.length === 0) return null;
|
|
16972
|
+
return { kind: "brush", points: this.points, radius: this.radius };
|
|
16973
|
+
}
|
|
16974
|
+
case "rectangle": {
|
|
16975
|
+
if (!this.startPoint || this.points.length < 2) return null;
|
|
16976
|
+
const end = this.points[this.points.length - 1];
|
|
16977
|
+
if (this.startPoint.x === end.x && this.startPoint.y === end.y) return null;
|
|
16978
|
+
return { kind: "rectangle", from: this.startPoint, to: end };
|
|
16979
|
+
}
|
|
16980
|
+
case "polygon": {
|
|
16981
|
+
if (this.points.length < 3) return null;
|
|
16982
|
+
return { kind: "polygon", points: this.points };
|
|
16983
|
+
}
|
|
16984
|
+
}
|
|
16985
|
+
}
|
|
16986
|
+
cancelGesture(ctx) {
|
|
16987
|
+
this.drawing = false;
|
|
16988
|
+
this.points = [];
|
|
16989
|
+
this.startPoint = null;
|
|
16990
|
+
ctx.requestRender();
|
|
16991
|
+
}
|
|
16992
|
+
renderBrushPreview(ctx) {
|
|
16993
|
+
if (this.points.length === 1) {
|
|
16994
|
+
const p = this.points[0];
|
|
16995
|
+
ctx.beginPath();
|
|
16996
|
+
ctx.arc(p.x, p.y, this.radius, 0, Math.PI * 2);
|
|
16997
|
+
ctx.fill();
|
|
16998
|
+
ctx.stroke();
|
|
16999
|
+
return;
|
|
17000
|
+
}
|
|
17001
|
+
ctx.beginPath();
|
|
17002
|
+
for (let i = 0; i < this.points.length; i++) {
|
|
17003
|
+
const p = this.points[i];
|
|
17004
|
+
if (i === 0) ctx.moveTo(p.x, p.y);
|
|
17005
|
+
else ctx.lineTo(p.x, p.y);
|
|
17006
|
+
}
|
|
17007
|
+
ctx.lineWidth = this.radius * 2;
|
|
17008
|
+
ctx.lineCap = "round";
|
|
17009
|
+
ctx.lineJoin = "round";
|
|
17010
|
+
ctx.stroke();
|
|
17011
|
+
}
|
|
17012
|
+
renderRectanglePreview(ctx) {
|
|
17013
|
+
if (this.points.length < 2) return;
|
|
17014
|
+
const from = this.points[0];
|
|
17015
|
+
const to = this.points[this.points.length - 1];
|
|
17016
|
+
const x = Math.min(from.x, to.x);
|
|
17017
|
+
const y = Math.min(from.y, to.y);
|
|
17018
|
+
const w = Math.abs(to.x - from.x);
|
|
17019
|
+
const h = Math.abs(to.y - from.y);
|
|
17020
|
+
ctx.fillRect(x, y, w, h);
|
|
17021
|
+
ctx.strokeRect(x, y, w, h);
|
|
17022
|
+
}
|
|
17023
|
+
renderPolygonPreview(ctx) {
|
|
17024
|
+
if (this.points.length < 2) return;
|
|
17025
|
+
const first = this.points[0];
|
|
17026
|
+
ctx.beginPath();
|
|
17027
|
+
ctx.moveTo(first.x, first.y);
|
|
17028
|
+
for (let i = 1; i < this.points.length; i++) {
|
|
17029
|
+
const p = this.points[i];
|
|
17030
|
+
ctx.lineTo(p.x, p.y);
|
|
17031
|
+
}
|
|
17032
|
+
ctx.closePath();
|
|
17033
|
+
ctx.fill();
|
|
17034
|
+
ctx.stroke();
|
|
17035
|
+
}
|
|
17036
|
+
};
|
|
17037
|
+
|
|
15462
17038
|
// src/index.ts
|
|
15463
|
-
var VERSION = "0.
|
|
17039
|
+
var VERSION = "0.67.0";
|
|
15464
17040
|
export {
|
|
15465
17041
|
AWARENESS_MAX_SELECTION,
|
|
15466
17042
|
AWARENESS_PRESENCE_KIND,
|
|
@@ -15473,6 +17049,12 @@ export {
|
|
|
15473
17049
|
ElementStore,
|
|
15474
17050
|
EraserTool,
|
|
15475
17051
|
FOCUS_PRESENCE_KIND,
|
|
17052
|
+
FOG_MAX_TILES,
|
|
17053
|
+
FOG_STATE_VERSION,
|
|
17054
|
+
FOG_TILE_CELLS,
|
|
17055
|
+
FogManager,
|
|
17056
|
+
FogRenderer,
|
|
17057
|
+
FogTool,
|
|
15476
17058
|
HandTool,
|
|
15477
17059
|
HistoryStack,
|
|
15478
17060
|
HtmlPainterMissingError,
|
|
@@ -15516,6 +17098,7 @@ export {
|
|
|
15516
17098
|
attachAwareness,
|
|
15517
17099
|
boundsIntersect,
|
|
15518
17100
|
cameraOriginForView,
|
|
17101
|
+
canonicalizeFogTile,
|
|
15519
17102
|
captureCameraView,
|
|
15520
17103
|
computeElementRects,
|
|
15521
17104
|
createArrow,
|
|
@@ -15533,6 +17116,8 @@ export {
|
|
|
15533
17116
|
exportImage,
|
|
15534
17117
|
exportSvg,
|
|
15535
17118
|
fitZoomForView,
|
|
17119
|
+
decodeBase64 as fogDecodeBase64,
|
|
17120
|
+
encodeBase64 as fogEncodeBase64,
|
|
15536
17121
|
footprintFromSize,
|
|
15537
17122
|
getActiveFormats,
|
|
15538
17123
|
getArrowBounds,
|
|
@@ -15558,6 +17143,8 @@ export {
|
|
|
15558
17143
|
isPathPresence,
|
|
15559
17144
|
isPingPresence,
|
|
15560
17145
|
pathDistanceCells,
|
|
17146
|
+
recommendedFogCellSize,
|
|
17147
|
+
resolveFogStyle,
|
|
15561
17148
|
resolveHtmlRouting,
|
|
15562
17149
|
setFontSize,
|
|
15563
17150
|
smartSnap,
|
|
@@ -15574,6 +17161,9 @@ export {
|
|
|
15574
17161
|
toggleBold,
|
|
15575
17162
|
toggleItalic,
|
|
15576
17163
|
toggleStrikethrough,
|
|
15577
|
-
toggleUnderline
|
|
17164
|
+
toggleUnderline,
|
|
17165
|
+
validateFogDefinition,
|
|
17166
|
+
validateFogState,
|
|
17167
|
+
validateFogTile
|
|
15578
17168
|
};
|
|
15579
17169
|
//# sourceMappingURL=index.js.map
|