@canvas-harness/core 0.1.4 → 0.1.5

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.js CHANGED
@@ -4884,7 +4884,7 @@ var drawWithNodeTransform = (ctx, node, fn) => {
4884
4884
  };
4885
4885
 
4886
4886
  // src/render/renderer.ts
4887
- var VIEWPORT_OVERSCAN_PX = 64;
4887
+ var SCENE_CACHE_MARGIN_PX = 256;
4888
4888
  var MIN_ON_SCREEN_SIZE_PX = 1.5;
4889
4889
  var MIN_READABLE_FONT_PX = 3;
4890
4890
  var createRenderer = (opts) => {
@@ -4901,6 +4901,30 @@ var createRenderer = (opts) => {
4901
4901
  let interactiveDirty = false;
4902
4902
  let overlaySet = /* @__PURE__ */ new Set();
4903
4903
  let lastDrawn = 0;
4904
+ let cacheSurface = null;
4905
+ let cacheCamX = 0;
4906
+ let cacheCamY = 0;
4907
+ let cacheCamZ = 1;
4908
+ let cacheStale = true;
4909
+ const ensureCacheSurface = () => {
4910
+ const dpr = staticSurface.dpr;
4911
+ const cssW = staticSurface.cssWidth + 2 * SCENE_CACHE_MARGIN_PX;
4912
+ const cssH = staticSurface.cssHeight + 2 * SCENE_CACHE_MARGIN_PX;
4913
+ if (!cacheSurface) {
4914
+ const canvas = document.createElement("canvas");
4915
+ const ctx = canvas.getContext("2d");
4916
+ if (!ctx) throw new Error("Canvas 2d context unavailable");
4917
+ cacheSurface = { canvas, ctx, cssWidth: 0, cssHeight: 0, dpr: 1 };
4918
+ }
4919
+ if (cacheSurface.cssWidth !== cssW || cacheSurface.cssHeight !== cssH || cacheSurface.dpr !== dpr) {
4920
+ cacheSurface.cssWidth = cssW;
4921
+ cacheSurface.cssHeight = cssH;
4922
+ cacheSurface.dpr = dpr;
4923
+ cacheSurface.canvas.width = Math.max(1, Math.round(cssW * dpr));
4924
+ cacheSurface.canvas.height = Math.max(1, Math.round(cssH * dpr));
4925
+ }
4926
+ return cacheSurface;
4927
+ };
4904
4928
  let sortedNodeIdsCache = null;
4905
4929
  let sortedEdgeIdsCache = null;
4906
4930
  const invalidateSortedCaches = () => {
@@ -4909,6 +4933,7 @@ var createRenderer = (opts) => {
4909
4933
  };
4910
4934
  const requestRepaint = () => {
4911
4935
  staticDirty = true;
4936
+ cacheStale = true;
4912
4937
  loop.requestFrame();
4913
4938
  };
4914
4939
  const assetCache = createAssetCache({ onReady: requestRepaint });
@@ -4923,16 +4948,12 @@ var createRenderer = (opts) => {
4923
4948
  interactiveDirty = false;
4924
4949
  }
4925
4950
  };
4926
- const paintStatic = () => {
4927
- const camera = store.getCamera();
4928
- clearSurface(staticSurface);
4929
- applyCameraTransform(staticSurface, camera);
4930
- const scale = camera.z * staticSurface.dpr;
4951
+ const paintSceneBody = (surface, camera, viewport, fullRender = true) => {
4952
+ const scale = camera.z * surface.dpr;
4931
4953
  const interaction = store.getInteractionState();
4932
4954
  const excludedNodes = interaction.mode === "dragging" || interaction.mode === "resizing" ? new Set(interaction.draggedIds) : null;
4933
4955
  const excludedEdges = excludedNodes ? incidentEdgeIds(excludedNodes) : null;
4934
- const viewport = inflateRect(worldViewport(staticSurface, camera), VIEWPORT_OVERSCAN_PX);
4935
- paintBackground(staticSurface.ctx, { viewport, zoom: camera.z, background });
4956
+ paintBackground(surface.ctx, { viewport, zoom: camera.z, background });
4936
4957
  const visible = visibleNodes(camera, viewport);
4937
4958
  const isMoving2 = interaction.mode === "panning" || interaction.mode === "zooming" || interaction.mode === "dragging" || interaction.mode === "resizing" || interaction.mode === "rotating";
4938
4959
  const minOnScreen = MIN_ON_SCREEN_SIZE_PX;
@@ -4954,8 +4975,8 @@ var createRenderer = (opts) => {
4954
4975
  for (const node of visible) {
4955
4976
  if (node.type !== "frame") continue;
4956
4977
  if (excludedNodes?.has(node.id)) continue;
4957
- drawWithNodeTransform(staticSurface.ctx, node, () => {
4958
- paintFrameNode(staticSurface.ctx, node, scale, theme);
4978
+ drawWithNodeTransform(surface.ctx, node, () => {
4979
+ paintFrameNode(surface.ctx, node, scale, theme);
4959
4980
  });
4960
4981
  drawn++;
4961
4982
  }
@@ -4968,52 +4989,53 @@ var createRenderer = (opts) => {
4968
4989
  const useRough = roughEnabled && (node.style?.roughness ?? 0) > 0;
4969
4990
  const roughReady = useRough ? getRoughCanvasCtor() !== null : false;
4970
4991
  const composite = isCompositePrimitive(node.type);
4971
- drawWithNodeTransform(staticSurface.ctx, node, () => {
4992
+ drawWithNodeTransform(surface.ctx, node, () => {
4972
4993
  if (useRough && roughReady) {
4973
4994
  if (composite) {
4974
- drawCompositeRough(staticSurface.ctx, node, camera.z, theme);
4995
+ drawCompositeRough(surface.ctx, node, camera.z, theme);
4975
4996
  } else {
4976
- staticSurface.ctx.translate(ROUGH_FILL_MISREGISTER_X, ROUGH_FILL_MISREGISTER_Y);
4977
- drawShape(staticSurface.ctx, node, scale, theme, { skipStroke: true });
4978
- staticSurface.ctx.translate(-ROUGH_FILL_MISREGISTER_X, -ROUGH_FILL_MISREGISTER_Y);
4979
- drawRoughShape(staticSurface.ctx, node, camera.z, theme);
4997
+ surface.ctx.translate(ROUGH_FILL_MISREGISTER_X, ROUGH_FILL_MISREGISTER_Y);
4998
+ drawShape(surface.ctx, node, scale, theme, { skipStroke: true });
4999
+ surface.ctx.translate(-ROUGH_FILL_MISREGISTER_X, -ROUGH_FILL_MISREGISTER_Y);
5000
+ drawRoughShape(surface.ctx, node, camera.z, theme);
4980
5001
  }
4981
5002
  } else {
4982
- drawShape(staticSurface.ctx, node, scale, theme);
5003
+ drawShape(surface.ctx, node, scale, theme);
4983
5004
  if (useRough && !roughReady) {
4984
5005
  onRoughReady(() => {
4985
5006
  staticDirty = true;
5007
+ cacheStale = true;
4986
5008
  loop.requestFrame();
4987
5009
  });
4988
5010
  }
4989
5011
  }
4990
- if (!isEditingThis) paintNodeContent(staticSurface.ctx, node, renderEnv);
5012
+ if (!isEditingThis) paintNodeContent(surface.ctx, node, renderEnv);
4991
5013
  });
4992
5014
  drawn++;
4993
5015
  continue;
4994
5016
  }
4995
5017
  if (node.type === "image") {
4996
- drawWithNodeTransform(staticSurface.ctx, node, () => {
4997
- paintImageNode(staticSurface.ctx, node, assetCache, theme);
5018
+ drawWithNodeTransform(surface.ctx, node, () => {
5019
+ paintImageNode(surface.ctx, node, assetCache, theme);
4998
5020
  });
4999
5021
  drawn++;
5000
5022
  continue;
5001
5023
  }
5002
5024
  if (node.type === "icon") {
5003
- drawWithNodeTransform(staticSurface.ctx, node, () => {
5004
- paintIconNode(staticSurface.ctx, node, assetCache, scale, theme);
5025
+ drawWithNodeTransform(surface.ctx, node, () => {
5026
+ paintIconNode(surface.ctx, node, assetCache, scale, theme);
5005
5027
  });
5006
5028
  drawn++;
5007
5029
  continue;
5008
5030
  }
5009
5031
  if (node.type === "text") {
5010
- drawWithNodeTransform(staticSurface.ctx, node, () => {
5032
+ drawWithNodeTransform(surface.ctx, node, () => {
5011
5033
  if (isEditingThis) return;
5012
5034
  const hasContent = node.content && node.content.trim().length > 0;
5013
5035
  if (hasContent) {
5014
- paintNodeContent(staticSurface.ctx, node, renderEnv);
5036
+ paintNodeContent(surface.ctx, node, renderEnv);
5015
5037
  } else {
5016
- paintEmptyTextPlaceholder(staticSurface.ctx, node, camera.z);
5038
+ paintEmptyTextPlaceholder(surface.ctx, node, camera.z);
5017
5039
  }
5018
5040
  });
5019
5041
  drawn++;
@@ -5025,7 +5047,7 @@ var createRenderer = (opts) => {
5025
5047
  if (camera.z < def.lod.minZoomForPlaceholder) continue;
5026
5048
  const preferCanvas = camera.z < def.lod.minZoomForReact || isMoving2;
5027
5049
  if (preferCanvas) {
5028
- if (paintCustomCanvasFallback(staticSurface.ctx, node, def, scale, renderEnv)) {
5050
+ if (paintCustomCanvasFallback(surface.ctx, node, def, scale, renderEnv)) {
5029
5051
  drawn++;
5030
5052
  }
5031
5053
  continue;
@@ -5035,10 +5057,10 @@ var createRenderer = (opts) => {
5035
5057
  continue;
5036
5058
  }
5037
5059
  if (def.renderCanvas) {
5038
- drawWithNodeTransform(staticSurface.ctx, node, () => {
5039
- staticSurface.ctx.save();
5040
- def.renderCanvas(staticSurface.ctx, node, renderEnv);
5041
- staticSurface.ctx.restore();
5060
+ drawWithNodeTransform(surface.ctx, node, () => {
5061
+ surface.ctx.save();
5062
+ def.renderCanvas(surface.ctx, node, renderEnv);
5063
+ surface.ctx.restore();
5042
5064
  });
5043
5065
  drawn++;
5044
5066
  }
@@ -5047,15 +5069,120 @@ var createRenderer = (opts) => {
5047
5069
  const edgeRoughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visEdges.length <= ROUGH_MAX_NODES;
5048
5070
  for (const edge of visEdges) {
5049
5071
  if (excludedEdges?.has(edge.id)) continue;
5050
- paintOneEdge(staticSurface.ctx, edge, scale, edgeRoughEnabled, camera.z, isMoving2);
5072
+ paintOneEdge(surface.ctx, edge, scale, edgeRoughEnabled, camera.z, isMoving2);
5051
5073
  drawn++;
5052
5074
  }
5075
+ if (!fullRender) return;
5053
5076
  lastDrawn = drawn;
5054
5077
  if (!setsEqual(nextOverlaySet, overlaySet)) {
5055
5078
  overlaySet = nextOverlaySet;
5056
5079
  onOverlayChange?.([...overlaySet]);
5057
5080
  }
5058
5081
  };
5082
+ const applyCacheTransform = (cache5, centerX, centerY, z) => {
5083
+ const s = z * cache5.dpr;
5084
+ const m = SCENE_CACHE_MARGIN_PX * cache5.dpr;
5085
+ cache5.ctx.setTransform(s, 0, 0, s, -centerX * s + m, -centerY * s + m);
5086
+ };
5087
+ const renderFullCache = (camera) => {
5088
+ const cache5 = ensureCacheSurface();
5089
+ clearSurface(cache5);
5090
+ applyCacheTransform(cache5, camera.x, camera.y, camera.z);
5091
+ const marginWorld = SCENE_CACHE_MARGIN_PX / camera.z;
5092
+ const viewport = inflateRect(worldViewport(staticSurface, camera), marginWorld);
5093
+ paintSceneBody(cache5, camera, viewport);
5094
+ cacheCamX = camera.x;
5095
+ cacheCamY = camera.y;
5096
+ cacheCamZ = camera.z;
5097
+ cacheStale = false;
5098
+ };
5099
+ const canExtend = (camera) => {
5100
+ if (!cacheSurface) return false;
5101
+ const s = camera.z * staticSurface.dpr;
5102
+ const dx = Math.abs((cacheCamX - camera.x) * s);
5103
+ const dy = Math.abs((cacheCamY - camera.y) * s);
5104
+ return dx < cacheSurface.canvas.width && dy < cacheSurface.canvas.height;
5105
+ };
5106
+ const renderCacheStrip = (cache5, centerX, centerY, z, px, py, pw, ph) => {
5107
+ const ctx = cache5.ctx;
5108
+ const s = z * cache5.dpr;
5109
+ const m = SCENE_CACHE_MARGIN_PX * cache5.dpr;
5110
+ ctx.save();
5111
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
5112
+ ctx.beginPath();
5113
+ ctx.rect(px, py, pw, ph);
5114
+ ctx.clip();
5115
+ ctx.clearRect(px, py, pw, ph);
5116
+ applyCacheTransform(cache5, centerX, centerY, z);
5117
+ const viewport = {
5118
+ x: (px - m) / s + centerX,
5119
+ y: (py - m) / s + centerY,
5120
+ w: pw / s,
5121
+ h: ph / s
5122
+ };
5123
+ paintSceneBody(cache5, { z }, viewport, false);
5124
+ ctx.restore();
5125
+ };
5126
+ const extendCache = (camera) => {
5127
+ const cache5 = ensureCacheSurface();
5128
+ const s = camera.z * cache5.dpr;
5129
+ const cacheW = cache5.canvas.width;
5130
+ const cacheH = cache5.canvas.height;
5131
+ const dx = Math.round((cacheCamX - camera.x) * s);
5132
+ const dy = Math.round((cacheCamY - camera.y) * s);
5133
+ const newCamX = cacheCamX - dx / s;
5134
+ const newCamY = cacheCamY - dy / s;
5135
+ cache5.ctx.setTransform(1, 0, 0, 1, 0, 0);
5136
+ cache5.ctx.drawImage(cache5.canvas, 0, 0, cacheW, cacheH, dx, dy, cacheW, cacheH);
5137
+ cacheCamX = newCamX;
5138
+ cacheCamY = newCamY;
5139
+ cacheCamZ = camera.z;
5140
+ const hw = Math.abs(dx);
5141
+ const vh = Math.abs(dy);
5142
+ const hx = dx > 0 ? 0 : cacheW - hw;
5143
+ const vy = dy > 0 ? 0 : cacheH - vh;
5144
+ const vx = dx > 0 ? hw : 0;
5145
+ const vw = cacheW - hw;
5146
+ if (hw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, hx, 0, hw, cacheH);
5147
+ if (vh > 0 && vw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, vx, vy, vw, vh);
5148
+ };
5149
+ const cacheSourceOffset = (camera) => {
5150
+ const dpr = staticSurface.dpr;
5151
+ return {
5152
+ x: Math.round(((camera.x - cacheCamX) * cacheCamZ + SCENE_CACHE_MARGIN_PX) * dpr),
5153
+ y: Math.round(((camera.y - cacheCamY) * cacheCamZ + SCENE_CACHE_MARGIN_PX) * dpr)
5154
+ };
5155
+ };
5156
+ const viewportFitsInCache = (camera) => {
5157
+ if (!cacheSurface) return false;
5158
+ const { x, y } = cacheSourceOffset(camera);
5159
+ return x >= 0 && y >= 0 && x + staticSurface.canvas.width <= cacheSurface.canvas.width && y + staticSurface.canvas.height <= cacheSurface.canvas.height;
5160
+ };
5161
+ const presentStatic = (camera) => {
5162
+ const cache5 = ensureCacheSurface();
5163
+ const w = staticSurface.canvas.width;
5164
+ const h = staticSurface.canvas.height;
5165
+ const { x: srcX, y: srcY } = cacheSourceOffset(camera);
5166
+ staticSurface.ctx.setTransform(1, 0, 0, 1, 0, 0);
5167
+ staticSurface.ctx.clearRect(0, 0, w, h);
5168
+ staticSurface.ctx.drawImage(cache5.canvas, srcX, srcY, w, h, 0, 0, w, h);
5169
+ };
5170
+ const paintStatic = () => {
5171
+ const camera = store.getCamera();
5172
+ if (!cacheStale && camera.z === cacheCamZ) {
5173
+ if (viewportFitsInCache(camera)) {
5174
+ presentStatic(camera);
5175
+ return;
5176
+ }
5177
+ if (canExtend(camera)) {
5178
+ extendCache(camera);
5179
+ presentStatic(camera);
5180
+ return;
5181
+ }
5182
+ }
5183
+ renderFullCache(camera);
5184
+ presentStatic(camera);
5185
+ };
5059
5186
  const paintCustomCanvasFallback = (ctx, node, def, drawScale, env) => {
5060
5187
  if (def.getSnapshot) {
5061
5188
  const snap = def.getSnapshot(node, {
@@ -5340,6 +5467,7 @@ var createRenderer = (opts) => {
5340
5467
  const onStoreChange = () => {
5341
5468
  invalidateSortedCaches();
5342
5469
  staticDirty = true;
5470
+ cacheStale = true;
5343
5471
  interactiveDirty = true;
5344
5472
  loop.requestFrame();
5345
5473
  };
@@ -5356,6 +5484,7 @@ var createRenderer = (opts) => {
5356
5484
  interactiveDirty = true;
5357
5485
  if (state.mode === "dragging" || state.mode === "resizing" || state.mode === "rotating" || state.mode === "panning" || state.mode === "zooming" || state.mode === "idle") {
5358
5486
  staticDirty = true;
5487
+ cacheStale = true;
5359
5488
  }
5360
5489
  loop.requestFrame();
5361
5490
  };
@@ -5365,16 +5494,19 @@ var createRenderer = (opts) => {
5365
5494
  const unsubInteraction = store.subscribe("interaction", onInteractionChange);
5366
5495
  const unsubFontEpoch = subscribeFontEpoch(() => {
5367
5496
  staticDirty = true;
5497
+ cacheStale = true;
5368
5498
  loop.requestFrame();
5369
5499
  });
5370
5500
  const unsubMathEpoch = subscribeMathEpoch(() => {
5371
5501
  staticDirty = true;
5502
+ cacheStale = true;
5372
5503
  loop.requestFrame();
5373
5504
  });
5374
5505
  return {
5375
5506
  start() {
5376
5507
  loop.start();
5377
5508
  staticDirty = true;
5509
+ cacheStale = true;
5378
5510
  interactiveDirty = isInteractive(store.getInteractionState());
5379
5511
  loop.requestFrame();
5380
5512
  },
@@ -5383,6 +5515,7 @@ var createRenderer = (opts) => {
5383
5515
  },
5384
5516
  invalidate() {
5385
5517
  staticDirty = true;
5518
+ cacheStale = true;
5386
5519
  interactiveDirty = true;
5387
5520
  loop.requestFrame();
5388
5521
  },
@@ -5391,6 +5524,7 @@ var createRenderer = (opts) => {
5391
5524
  const b = sizeSurface(interactiveSurface, cssW, cssH, maxDpr);
5392
5525
  if (a || b) {
5393
5526
  staticDirty = true;
5527
+ cacheStale = true;
5394
5528
  interactiveDirty = true;
5395
5529
  loop.requestFrame();
5396
5530
  }
@@ -5398,6 +5532,7 @@ var createRenderer = (opts) => {
5398
5532
  setBackground(bg) {
5399
5533
  background = bg;
5400
5534
  staticDirty = true;
5535
+ cacheStale = true;
5401
5536
  loop.requestFrame();
5402
5537
  },
5403
5538
  setSelectionColor(color) {
@@ -5408,6 +5543,7 @@ var createRenderer = (opts) => {
5408
5543
  setHideFrames(hidden) {
5409
5544
  hideFrames = hidden;
5410
5545
  staticDirty = true;
5546
+ cacheStale = true;
5411
5547
  loop.requestFrame();
5412
5548
  },
5413
5549
  stats: () => loop.stats(),
@@ -5422,6 +5558,11 @@ var createRenderer = (opts) => {
5422
5558
  unsubFontEpoch();
5423
5559
  unsubMathEpoch();
5424
5560
  assetCache.dispose();
5561
+ if (cacheSurface) {
5562
+ cacheSurface.canvas.width = 0;
5563
+ cacheSurface.canvas.height = 0;
5564
+ cacheSurface = null;
5565
+ }
5425
5566
  }
5426
5567
  };
5427
5568
  };