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