@fieldnotes/core 0.66.0 → 0.68.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/dist/index.cjs CHANGED
@@ -126,6 +126,7 @@ __export(index_exports, {
126
126
  isPingPresence: () => isPingPresence,
127
127
  pathDistanceCells: () => pathDistanceCells,
128
128
  recommendedFogCellSize: () => recommendedFogCellSize,
129
+ resolveFogStyle: () => resolveFogStyle,
129
130
  resolveHtmlRouting: () => resolveHtmlRouting,
130
131
  setFontSize: () => setFontSize,
131
132
  smartSnap: () => smartSnap,
@@ -7325,19 +7326,196 @@ async function renderHtmlElements(elements, options) {
7325
7326
  return sources;
7326
7327
  }
7327
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
+
7328
7482
  // src/fog/fog-renderer.ts
7329
7483
  var DEFAULT_EDITOR_COLOR = "rgba(30, 40, 60, 0.45)";
7330
7484
  var DEFAULT_PLAYER_COLOR = "#0b1020";
7331
7485
  var FogRenderer = class {
7332
7486
  tileCache = /* @__PURE__ */ new Map();
7487
+ patternCache = /* @__PURE__ */ new Map();
7333
7488
  state = null;
7334
7489
  viewMode = "off";
7335
7490
  dirty = true;
7336
- editorColor;
7337
- playerColor;
7491
+ editorStyle;
7492
+ playerStyle;
7338
7493
  constructor(options = {}) {
7339
- this.editorColor = options.editorColor ?? DEFAULT_EDITOR_COLOR;
7340
- this.playerColor = options.playerColor ?? DEFAULT_PLAYER_COLOR;
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
+ setOptions(options) {
7506
+ this.editorStyle = resolveFogStyle(
7507
+ options.editorStyle,
7508
+ options.editorColor,
7509
+ DEFAULT_EDITOR_COLOR
7510
+ );
7511
+ this.playerStyle = resolveFogStyle(
7512
+ options.playerStyle,
7513
+ options.playerColor,
7514
+ DEFAULT_PLAYER_COLOR
7515
+ );
7516
+ this.tileCache.clear();
7517
+ this.patternCache.clear();
7518
+ this.dirty = true;
7341
7519
  }
7342
7520
  setState(state) {
7343
7521
  this.state = state;
@@ -7363,12 +7541,23 @@ var FogRenderer = class {
7363
7541
  isVisible() {
7364
7542
  return this.viewMode !== "off" && this.state !== null;
7365
7543
  }
7544
+ getResolvedStyle(mode) {
7545
+ return mode === "editor" ? this.editorStyle : this.playerStyle;
7546
+ }
7366
7547
  render(ctx, camera, viewportWidth, viewportHeight, _dpr) {
7367
7548
  if (!this.state || this.viewMode === "off") return;
7368
7549
  const def = this.state.definition;
7369
7550
  const cellSize = def.cellSize;
7370
7551
  const tileWorldSize = FOG_TILE_CELLS * cellSize;
7371
- const color = this.viewMode === "editor" ? this.editorColor : this.playerColor;
7552
+ const mode = this.viewMode === "editor" ? "editor" : "player";
7553
+ const style = this.getResolvedStyle(mode);
7554
+ const proceduralStyle = style.kind === "procedural" ? style : null;
7555
+ const color = style.kind === "procedural" ? normalizeCanvasColor(
7556
+ ctx,
7557
+ style.backdrop,
7558
+ mode === "player" ? DEFAULT_PLAYER_COLOR : DEFAULT_EDITOR_COLOR
7559
+ ) : style.color;
7560
+ const safetyColor = proceduralStyle && mode === "player" ? DEFAULT_PLAYER_COLOR : null;
7372
7561
  const worldBounds = getVisibleWorld(camera, viewportWidth, viewportHeight);
7373
7562
  const minTX = Math.floor(Math.max(def.bounds.x, worldBounds.x) / tileWorldSize);
7374
7563
  const minTY = Math.floor(Math.max(def.bounds.y, worldBounds.y) / tileWorldSize);
@@ -7399,7 +7588,22 @@ var FogRenderer = class {
7399
7588
  const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
7400
7589
  const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
7401
7590
  if (clipR > clipX && clipB > clipY) {
7591
+ if (safetyColor) {
7592
+ ctx.fillStyle = safetyColor;
7593
+ ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
7594
+ }
7595
+ ctx.fillStyle = color;
7402
7596
  ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
7597
+ if (proceduralStyle) {
7598
+ this.paintProceduralOverlay(
7599
+ ctx,
7600
+ proceduralStyle,
7601
+ clipX,
7602
+ clipY,
7603
+ clipR - clipX,
7604
+ clipB - clipY
7605
+ );
7606
+ }
7403
7607
  }
7404
7608
  continue;
7405
7609
  }
@@ -7407,18 +7611,33 @@ var FogRenderer = class {
7407
7611
  continue;
7408
7612
  }
7409
7613
  if (data) {
7614
+ if (safetyColor) this.renderTile(ctx, data, tx, ty, def, safetyColor);
7410
7615
  this.renderTile(ctx, data, tx, ty, def, color);
7616
+ if (proceduralStyle) {
7617
+ this.renderTileProceduralOverlay(ctx, data, tx, ty, def, proceduralStyle);
7618
+ }
7411
7619
  }
7412
7620
  }
7413
7621
  }
7414
7622
  ctx.restore();
7415
7623
  this.dirty = false;
7416
7624
  }
7417
- renderForExport(ctx, state, mode, color) {
7625
+ renderForExport(ctx, state, mode, color, style) {
7418
7626
  const def = state.definition;
7419
7627
  const cellSize = def.cellSize;
7420
7628
  const tileWorldSize = FOG_TILE_CELLS * cellSize;
7421
- const fogColor = color ?? (mode === "editor" ? this.editorColor : this.playerColor);
7629
+ const resolved = style ? resolveFogStyle(
7630
+ style,
7631
+ void 0,
7632
+ mode === "editor" ? DEFAULT_EDITOR_COLOR : DEFAULT_PLAYER_COLOR
7633
+ ) : this.getResolvedStyle(mode);
7634
+ const proceduralStyle = !color && resolved.kind === "procedural" ? resolved : null;
7635
+ const fogColor = color ?? (proceduralStyle ? normalizeCanvasColor(
7636
+ ctx,
7637
+ proceduralStyle.backdrop,
7638
+ mode === "player" ? DEFAULT_PLAYER_COLOR : DEFAULT_EDITOR_COLOR
7639
+ ) : resolved.kind === "solid" ? resolved.color : resolved.backdrop);
7640
+ const safetyColor = proceduralStyle && mode === "player" ? DEFAULT_PLAYER_COLOR : null;
7422
7641
  const baseCovered = def.base === "covered";
7423
7642
  const tileMap = /* @__PURE__ */ new Map();
7424
7643
  for (const tile of state.tiles) {
@@ -7441,20 +7660,129 @@ var FogRenderer = class {
7441
7660
  const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
7442
7661
  const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
7443
7662
  if (clipR > clipX && clipB > clipY) {
7663
+ if (safetyColor) {
7664
+ ctx.fillStyle = safetyColor;
7665
+ ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
7666
+ }
7667
+ ctx.fillStyle = fogColor;
7444
7668
  ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
7669
+ if (proceduralStyle) {
7670
+ this.paintProceduralOverlay(
7671
+ ctx,
7672
+ proceduralStyle,
7673
+ clipX,
7674
+ clipY,
7675
+ clipR - clipX,
7676
+ clipB - clipY
7677
+ );
7678
+ }
7445
7679
  }
7446
7680
  continue;
7447
7681
  }
7448
7682
  if (data) {
7683
+ if (safetyColor) this.renderTileForExport(ctx, data, tx, ty, def, safetyColor);
7449
7684
  this.renderTileForExport(ctx, data, tx, ty, def, fogColor);
7685
+ if (proceduralStyle) {
7686
+ this.renderTileProceduralOverlay(ctx, data, tx, ty, def, proceduralStyle);
7687
+ }
7450
7688
  }
7451
7689
  }
7452
7690
  }
7453
7691
  }
7454
7692
  dispose() {
7455
7693
  this.tileCache.clear();
7694
+ this.patternCache.clear();
7695
+ clearProceduralTileCache();
7456
7696
  this.state = null;
7457
7697
  }
7698
+ getOrCreatePattern(ctx, style, worldScale) {
7699
+ const key = `${style.backdrop}\0${style.tint}\0${style.opacity}\0${style.scale}\0${style.seed}\0${style.detail}\0${worldScale}`;
7700
+ const cached = this.patternCache.get(key);
7701
+ if (cached !== void 0) return cached;
7702
+ let pattern = null;
7703
+ try {
7704
+ const tileData = getCachedProceduralTile(style);
7705
+ pattern = this.createPatternFromTileData(ctx, tileData, style, worldScale);
7706
+ } catch {
7707
+ }
7708
+ if (this.patternCache.size >= 32) {
7709
+ const oldest = this.patternCache.keys().next().value;
7710
+ if (oldest !== void 0) this.patternCache.delete(oldest);
7711
+ }
7712
+ this.patternCache.set(key, pattern);
7713
+ return pattern;
7714
+ }
7715
+ createPatternFromTileData(ctx, tileData, style, worldScale) {
7716
+ if (typeof document === "undefined") return null;
7717
+ const sourceCanvas = document.createElement("canvas");
7718
+ sourceCanvas.width = tileData.width;
7719
+ sourceCanvas.height = tileData.height;
7720
+ const sourceCtx = sourceCanvas.getContext("2d");
7721
+ if (!sourceCtx) return null;
7722
+ const imageData = new ImageData(
7723
+ new Uint8ClampedArray(tileData.data),
7724
+ tileData.width,
7725
+ tileData.height
7726
+ );
7727
+ sourceCtx.putImageData(imageData, 0, 0);
7728
+ sourceCtx.globalCompositeOperation = "source-in";
7729
+ sourceCtx.fillStyle = normalizeCanvasColor(sourceCtx, style.tint, "#ffffff");
7730
+ sourceCtx.fillRect(0, 0, tileData.width, tileData.height);
7731
+ sourceCtx.globalCompositeOperation = "source-over";
7732
+ const patternScale = style.scale * worldScale / tileData.width;
7733
+ const pattern = ctx.createPattern(sourceCanvas, "repeat");
7734
+ if (pattern && typeof pattern.setTransform === "function" && typeof DOMMatrix !== "undefined") {
7735
+ try {
7736
+ pattern.setTransform(new DOMMatrix([patternScale, 0, 0, patternScale, 0, 0]));
7737
+ return pattern;
7738
+ } catch {
7739
+ }
7740
+ }
7741
+ const patternPx = Math.round(style.scale * worldScale);
7742
+ if (patternPx < 1) return null;
7743
+ const patternCanvas = document.createElement("canvas");
7744
+ patternCanvas.width = patternPx;
7745
+ patternCanvas.height = patternPx;
7746
+ const patternCtx = patternCanvas.getContext("2d");
7747
+ if (!patternCtx) return null;
7748
+ patternCtx.drawImage(sourceCanvas, 0, 0, patternPx, patternPx);
7749
+ return ctx.createPattern(patternCanvas, "repeat");
7750
+ }
7751
+ paintProceduralOverlay(ctx, style, x, y, w, h) {
7752
+ const pattern = this.getOrCreatePattern(ctx, style, 1);
7753
+ if (!pattern) return;
7754
+ ctx.save();
7755
+ ctx.fillStyle = pattern;
7756
+ ctx.fillRect(x, y, w, h);
7757
+ ctx.restore();
7758
+ }
7759
+ renderTileProceduralOverlay(ctx, data, tx, ty, def, style) {
7760
+ const cellSize = def.cellSize;
7761
+ const tileWorldX = tx * FOG_TILE_CELLS * cellSize;
7762
+ const tileWorldY = ty * FOG_TILE_CELLS * cellSize;
7763
+ const pattern = this.getOrCreatePattern(ctx, style, 1);
7764
+ if (!pattern) return;
7765
+ const bytes = decodeBase64(data);
7766
+ ctx.save();
7767
+ ctx.fillStyle = pattern;
7768
+ for (let row = 0; row < FOG_TILE_CELLS; row++) {
7769
+ for (let col = 0; col < FOG_TILE_CELLS; col++) {
7770
+ const cellWorldX = tileWorldX + col * cellSize;
7771
+ const cellWorldY = tileWorldY + row * cellSize;
7772
+ if (cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= def.bounds.x + def.bounds.w || cellWorldY >= def.bounds.y + def.bounds.h) {
7773
+ continue;
7774
+ }
7775
+ const index = row * FOG_TILE_CELLS + col;
7776
+ const byteIndex = index >> 3;
7777
+ const bitIndex = 7 - (index & 7);
7778
+ const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
7779
+ if (!revealed) {
7780
+ ctx.fillRect(cellWorldX, cellWorldY, cellSize, cellSize);
7781
+ }
7782
+ }
7783
+ }
7784
+ ctx.restore();
7785
+ }
7458
7786
  tileRaster(data, color) {
7459
7787
  const key = `${color}\0${data}`;
7460
7788
  const cached = this.tileCache.get(key);
@@ -7538,6 +7866,22 @@ function getVisibleWorld(camera, viewportWidth, viewportHeight) {
7538
7866
  h: bottomRight.y - topLeft.y
7539
7867
  };
7540
7868
  }
7869
+ function normalizeCanvasColor(ctx, value, fallback) {
7870
+ const previous = ctx.fillStyle;
7871
+ try {
7872
+ ctx.fillStyle = "#010203";
7873
+ ctx.fillStyle = value;
7874
+ const first = ctx.fillStyle;
7875
+ ctx.fillStyle = "#040506";
7876
+ ctx.fillStyle = value;
7877
+ const second = ctx.fillStyle;
7878
+ return typeof first === "string" && first === second ? first : fallback;
7879
+ } catch {
7880
+ return fallback;
7881
+ } finally {
7882
+ ctx.fillStyle = previous;
7883
+ }
7884
+ }
7541
7885
 
7542
7886
  // src/canvas/export-image.ts
7543
7887
  var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
@@ -7953,7 +8297,13 @@ async function exportImage(store, options = {}, layerManager) {
7953
8297
  }
7954
8298
  if (options.fog) {
7955
8299
  const fogRenderer = new FogRenderer();
7956
- fogRenderer.renderForExport(ctx, options.fog.state, options.fog.mode, options.fog.color);
8300
+ fogRenderer.renderForExport(
8301
+ ctx,
8302
+ options.fog.state,
8303
+ options.fog.mode,
8304
+ options.fog.color,
8305
+ options.fog.style
8306
+ );
7957
8307
  }
7958
8308
  const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
7959
8309
  return new Promise((resolve) => {
@@ -8347,7 +8697,13 @@ async function exportSvg(store, options = {}, layerManager) {
8347
8697
  if (fogCtx) {
8348
8698
  fogCtx.translate(-bounds.x, -bounds.y);
8349
8699
  const fogRenderer = new FogRenderer();
8350
- fogRenderer.renderForExport(fogCtx, fogState, options.fog.mode, options.fog.color);
8700
+ fogRenderer.renderForExport(
8701
+ fogCtx,
8702
+ fogState,
8703
+ options.fog.mode,
8704
+ options.fog.color,
8705
+ options.fog.style
8706
+ );
8351
8707
  try {
8352
8708
  const fogDataUri = fogCanvas.toDataURL("image/png");
8353
8709
  if (fogDataUri.startsWith("data:")) {
@@ -11236,6 +11592,11 @@ var Viewport = class _Viewport {
11236
11592
  get fog() {
11237
11593
  return this.fogManager;
11238
11594
  }
11595
+ setFogStyle(options) {
11596
+ this.fogRenderer.setOptions(options);
11597
+ this.renderLoop.requestRender();
11598
+ this.minimap?.invalidateScene();
11599
+ }
11239
11600
  get snapToGrid() {
11240
11601
  return this._snapToGrid;
11241
11602
  }
@@ -11342,26 +11703,35 @@ var Viewport = class _Viewport {
11342
11703
  const expected = base.expectedCanvasTypes ? /* @__PURE__ */ new Set([...declared, ...base.expectedCanvasTypes]) : declared;
11343
11704
  return { ...base, htmlPainters: registry, expectedCanvasTypes: expected };
11344
11705
  }
11345
- async exportImage(options) {
11346
- const opts = this.withHtmlDefaults(options);
11347
- if (opts.fog === void 0 && this.fogRenderer.isVisible()) {
11348
- const state = this.fogManager.getState();
11349
- if (state) {
11350
- const mode = this.fogRenderer.getViewMode();
11351
- opts.fog = { state, mode };
11352
- }
11706
+ /**
11707
+ * Carry constructor-configured fog presentation into both implicit exports and
11708
+ * explicit state/mode exports. Explicit style and legacy color overrides win.
11709
+ */
11710
+ withFogDefaults(options) {
11711
+ const fog = options.fog;
11712
+ if (fog === false) return options;
11713
+ if (fog !== void 0) {
11714
+ if (fog.style !== void 0 || fog.color !== void 0) return options;
11715
+ return {
11716
+ ...options,
11717
+ fog: { ...fog, style: this.fogRenderer.getResolvedStyle(fog.mode) }
11718
+ };
11353
11719
  }
11720
+ if (!this.fogRenderer.isVisible()) return options;
11721
+ const state = this.fogManager.getState();
11722
+ if (!state) return options;
11723
+ const mode = this.fogRenderer.getViewMode();
11724
+ return {
11725
+ ...options,
11726
+ fog: { state, mode, style: this.fogRenderer.getResolvedStyle(mode) }
11727
+ };
11728
+ }
11729
+ async exportImage(options) {
11730
+ const opts = this.withFogDefaults(this.withHtmlDefaults(options));
11354
11731
  return exportImage(this.store, opts, this.layerManager);
11355
11732
  }
11356
11733
  async exportSVG(options) {
11357
- const opts = this.withHtmlDefaults(options);
11358
- if (opts.fog === void 0 && this.fogRenderer.isVisible()) {
11359
- const state = this.fogManager.getState();
11360
- if (state) {
11361
- const mode = this.fogRenderer.getViewMode();
11362
- opts.fog = { state, mode };
11363
- }
11364
- }
11734
+ const opts = this.withFogDefaults(this.withHtmlDefaults(options));
11365
11735
  return exportSvg(this.store, opts, this.layerManager);
11366
11736
  }
11367
11737
  loadState(state) {
@@ -12995,15 +13365,15 @@ function applyCameraView(camera, view, canvasW, canvasH) {
12995
13365
  var DEFAULT_DURATION_MS3 = 400;
12996
13366
  var FRAMED_EPSILON = 1e-6;
12997
13367
  var easeOutCubic2 = (t) => 1 - Math.pow(1 - t, 3);
12998
- function lerp(a, b, k) {
13368
+ function lerp2(a, b, k) {
12999
13369
  return a + (b - a) * k;
13000
13370
  }
13001
13371
  function lerpView(from, to, k) {
13002
13372
  return {
13003
- x: lerp(from.x, to.x, k),
13004
- y: lerp(from.y, to.y, k),
13005
- w: lerp(from.w, to.w, k),
13006
- h: lerp(from.h, to.h, k)
13373
+ x: lerp2(from.x, to.x, k),
13374
+ y: lerp2(from.y, to.y, k),
13375
+ w: lerp2(from.w, to.w, k),
13376
+ h: lerp2(from.h, to.h, k)
13007
13377
  };
13008
13378
  }
13009
13379
  function viewsClose(a, b) {
@@ -14257,7 +14627,7 @@ var PencilTool = class {
14257
14627
  };
14258
14628
 
14259
14629
  // src/elements/stroke-erase.ts
14260
- function lerp2(a, b, t) {
14630
+ function lerp3(a, b, t) {
14261
14631
  return {
14262
14632
  x: a.x + (b.x - a.x) * t,
14263
14633
  y: a.y + (b.y - a.y) * t,
@@ -14316,13 +14686,13 @@ function erasePoints(points, eraser, radius) {
14316
14686
  erased = true;
14317
14687
  if (tLo > 0) {
14318
14688
  if (current.length === 0) current.push(a);
14319
- current.push(lerp2(a, b, tLo));
14689
+ current.push(lerp3(a, b, tLo));
14320
14690
  flush();
14321
14691
  } else {
14322
14692
  flush();
14323
14693
  }
14324
14694
  if (tHi < 1) {
14325
- current = [lerp2(a, b, tHi), b];
14695
+ current = [lerp3(a, b, tHi), b];
14326
14696
  }
14327
14697
  }
14328
14698
  flush();
@@ -16838,7 +17208,7 @@ var FogTool = class {
16838
17208
  };
16839
17209
 
16840
17210
  // src/index.ts
16841
- var VERSION = "0.66.0";
17211
+ var VERSION = "0.68.0";
16842
17212
  // Annotate the CommonJS export names for ESM import in node:
16843
17213
  0 && (module.exports = {
16844
17214
  AWARENESS_MAX_SELECTION,
@@ -16947,6 +17317,7 @@ var VERSION = "0.66.0";
16947
17317
  isPingPresence,
16948
17318
  pathDistanceCells,
16949
17319
  recommendedFogCellSize,
17320
+ resolveFogStyle,
16950
17321
  resolveHtmlRouting,
16951
17322
  setFontSize,
16952
17323
  smartSnap,