@canvas-harness/core 0.1.2 → 0.1.4

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.d.cts CHANGED
@@ -2097,7 +2097,13 @@ type CanvasSurface = {
2097
2097
  /** Device pixels — backing-store size. */
2098
2098
  dpr: number;
2099
2099
  };
2100
- declare const getDpr: () => number;
2100
+ /**
2101
+ * Resolved DPR for the canvas backing store. Clamped by `maxDpr`
2102
+ * (consumer-supplied) and the absolute `HARD_MAX_DPR` ceiling. When
2103
+ * `maxDpr` is omitted, the tier-based default (see
2104
+ * `defaultMaxDprForSize`) is used — pass `cssW`/`cssH` to enable it.
2105
+ */
2106
+ declare const getDpr: (maxDpr?: number, cssW?: number, cssH?: number) => number;
2101
2107
  /**
2102
2108
  * Builds a managed canvas surface. Caller pins the canvas element; we size
2103
2109
  * it and reset the 2d context's transform to logical-pixel space.
@@ -2105,12 +2111,12 @@ declare const getDpr: () => number;
2105
2111
  * Subsequent calls to `setSize` re-allocate the backing store if the new
2106
2112
  * `cssW × cssH × DPR` differs from the current.
2107
2113
  */
2108
- declare const setupSurface: (canvas: HTMLCanvasElement) => CanvasSurface;
2114
+ declare const setupSurface: (canvas: HTMLCanvasElement, _maxDpr?: number) => CanvasSurface;
2109
2115
  /**
2110
2116
  * Resizes the surface to a new CSS-pixel size, picking up the current DPR.
2111
2117
  * Returns true if anything changed (caller should redraw).
2112
2118
  */
2113
- declare const sizeSurface: (surface: CanvasSurface, cssW: number, cssH: number) => boolean;
2119
+ declare const sizeSurface: (surface: CanvasSurface, cssW: number, cssH: number, maxDpr?: number) => boolean;
2114
2120
  /**
2115
2121
  * Clears the entire backing store. Call before setting the camera transform.
2116
2122
  */
@@ -2202,6 +2208,18 @@ type RendererOptions = {
2202
2208
  * fill tints via globalAlpha — no parsing needed.
2203
2209
  */
2204
2210
  selectionColor?: string;
2211
+ /**
2212
+ * Cap on the canvas backing-store DPR (device-pixel ratio). At
2213
+ * native DPR on hi-DPI displays, the backing buffer can hit
2214
+ * 20-30 megapixels per frame; the GPU-upload step alone dominates
2215
+ * the frame budget. Defaults to `1` for consistent perf across
2216
+ * hardware. Bump to `2` (or `window.devicePixelRatio`) for
2217
+ * pixel-crisp rendering at the cost of FPS on hi-DPI displays.
2218
+ *
2219
+ * Text is unaffected — the text bitmap cache renders glyphs at
2220
+ * its own DPR-aware scale.
2221
+ */
2222
+ maxDpr?: number;
2205
2223
  /**
2206
2224
  * Fires when the set of custom nodes that should be rendered in the DOM
2207
2225
  * overlay changes. Consumers use this to mount/unmount React subtrees
package/dist/index.d.ts CHANGED
@@ -2097,7 +2097,13 @@ type CanvasSurface = {
2097
2097
  /** Device pixels — backing-store size. */
2098
2098
  dpr: number;
2099
2099
  };
2100
- declare const getDpr: () => number;
2100
+ /**
2101
+ * Resolved DPR for the canvas backing store. Clamped by `maxDpr`
2102
+ * (consumer-supplied) and the absolute `HARD_MAX_DPR` ceiling. When
2103
+ * `maxDpr` is omitted, the tier-based default (see
2104
+ * `defaultMaxDprForSize`) is used — pass `cssW`/`cssH` to enable it.
2105
+ */
2106
+ declare const getDpr: (maxDpr?: number, cssW?: number, cssH?: number) => number;
2101
2107
  /**
2102
2108
  * Builds a managed canvas surface. Caller pins the canvas element; we size
2103
2109
  * it and reset the 2d context's transform to logical-pixel space.
@@ -2105,12 +2111,12 @@ declare const getDpr: () => number;
2105
2111
  * Subsequent calls to `setSize` re-allocate the backing store if the new
2106
2112
  * `cssW × cssH × DPR` differs from the current.
2107
2113
  */
2108
- declare const setupSurface: (canvas: HTMLCanvasElement) => CanvasSurface;
2114
+ declare const setupSurface: (canvas: HTMLCanvasElement, _maxDpr?: number) => CanvasSurface;
2109
2115
  /**
2110
2116
  * Resizes the surface to a new CSS-pixel size, picking up the current DPR.
2111
2117
  * Returns true if anything changed (caller should redraw).
2112
2118
  */
2113
- declare const sizeSurface: (surface: CanvasSurface, cssW: number, cssH: number) => boolean;
2119
+ declare const sizeSurface: (surface: CanvasSurface, cssW: number, cssH: number, maxDpr?: number) => boolean;
2114
2120
  /**
2115
2121
  * Clears the entire backing store. Call before setting the camera transform.
2116
2122
  */
@@ -2202,6 +2208,18 @@ type RendererOptions = {
2202
2208
  * fill tints via globalAlpha — no parsing needed.
2203
2209
  */
2204
2210
  selectionColor?: string;
2211
+ /**
2212
+ * Cap on the canvas backing-store DPR (device-pixel ratio). At
2213
+ * native DPR on hi-DPI displays, the backing buffer can hit
2214
+ * 20-30 megapixels per frame; the GPU-upload step alone dominates
2215
+ * the frame budget. Defaults to `1` for consistent perf across
2216
+ * hardware. Bump to `2` (or `window.devicePixelRatio`) for
2217
+ * pixel-crisp rendering at the cost of FPS on hi-DPI displays.
2218
+ *
2219
+ * Text is unaffected — the text bitmap cache renders glyphs at
2220
+ * its own DPR-aware scale.
2221
+ */
2222
+ maxDpr?: number;
2205
2223
  /**
2206
2224
  * Fires when the set of custom nodes that should be rendered in the DOM
2207
2225
  * overlay changes. Consumers use this to mount/unmount React subtrees
package/dist/index.js CHANGED
@@ -855,23 +855,12 @@ var darkenHex = (hex) => {
855
855
 
856
856
  // src/render/shapes/path-helpers.ts
857
857
  var buildRectPath = (ctx, w, h, radius) => {
858
+ ctx.beginPath();
858
859
  if (radius <= 0) {
859
- ctx.beginPath();
860
860
  ctx.rect(0, 0, w, h);
861
861
  return;
862
862
  }
863
- const r = Math.min(radius, w / 2, h / 2);
864
- ctx.beginPath();
865
- ctx.moveTo(r, 0);
866
- ctx.lineTo(w - r, 0);
867
- ctx.quadraticCurveTo(w, 0, w, r);
868
- ctx.lineTo(w, h - r);
869
- ctx.quadraticCurveTo(w, h, w - r, h);
870
- ctx.lineTo(r, h);
871
- ctx.quadraticCurveTo(0, h, 0, h - r);
872
- ctx.lineTo(0, r);
873
- ctx.quadraticCurveTo(0, 0, r, 0);
874
- ctx.closePath();
863
+ ctx.roundRect(0, 0, w, h, radius);
875
864
  };
876
865
  var buildEllipsePath = (ctx, w, h) => {
877
866
  const rx = w / 2;
@@ -4253,13 +4242,21 @@ var storeToJSON = (store) => ({
4253
4242
  });
4254
4243
 
4255
4244
  // src/render/canvas-setup.ts
4256
- var MAX_DPR = 3;
4257
- var getDpr = () => {
4245
+ var HARD_MAX_DPR = 3;
4246
+ var defaultMaxDprForSize = (cssW, cssH) => {
4247
+ const cssPx = cssW * cssH;
4248
+ if (cssPx >= 25e5) return 1;
4249
+ if (cssPx >= 15e5) return 1.5;
4250
+ return 2;
4251
+ };
4252
+ var getDpr = (maxDpr, cssW = 0, cssH = 0) => {
4258
4253
  if (typeof window === "undefined") return 1;
4259
4254
  const raw = window.devicePixelRatio || 1;
4260
- return Math.max(1, Math.min(MAX_DPR, raw));
4255
+ const resolvedMax = maxDpr === void 0 && cssW > 0 && cssH > 0 ? defaultMaxDprForSize(cssW, cssH) : maxDpr ?? 1;
4256
+ const cap = Math.max(1, Math.min(HARD_MAX_DPR, resolvedMax));
4257
+ return Math.max(1, Math.min(cap, raw));
4261
4258
  };
4262
- var setupSurface = (canvas) => {
4259
+ var setupSurface = (canvas, _maxDpr) => {
4263
4260
  const ctx = canvas.getContext("2d");
4264
4261
  if (!ctx) throw new Error("Canvas 2d context unavailable");
4265
4262
  return {
@@ -4267,11 +4264,12 @@ var setupSurface = (canvas) => {
4267
4264
  ctx,
4268
4265
  cssWidth: 0,
4269
4266
  cssHeight: 0,
4270
- dpr: getDpr()
4267
+ dpr: 1
4268
+ // placeholder; `sizeSurface` writes the real value
4271
4269
  };
4272
4270
  };
4273
- var sizeSurface = (surface, cssW, cssH) => {
4274
- const dpr = getDpr();
4271
+ var sizeSurface = (surface, cssW, cssH, maxDpr) => {
4272
+ const dpr = getDpr(maxDpr, cssW, cssH);
4275
4273
  if (surface.cssWidth === cssW && surface.cssHeight === cssH && surface.dpr === dpr) {
4276
4274
  return false;
4277
4275
  }
@@ -4891,13 +4889,14 @@ var MIN_ON_SCREEN_SIZE_PX = 1.5;
4891
4889
  var MIN_READABLE_FONT_PX = 3;
4892
4890
  var createRenderer = (opts) => {
4893
4891
  const { store, theme, onOverlayChange } = opts;
4892
+ const maxDpr = opts.maxDpr;
4894
4893
  const staticSurface = setupSurface(opts.staticCanvas);
4895
4894
  const interactiveSurface = setupSurface(opts.interactiveCanvas);
4896
4895
  let background = opts.background;
4897
4896
  let selectionColor = opts.selectionColor ?? DEFAULT_SELECTION_COLOR;
4898
4897
  let hideFrames = false;
4899
- sizeSurface(staticSurface, opts.width, opts.height);
4900
- sizeSurface(interactiveSurface, opts.width, opts.height);
4898
+ sizeSurface(staticSurface, opts.width, opts.height, maxDpr);
4899
+ sizeSurface(interactiveSurface, opts.width, opts.height, maxDpr);
4901
4900
  let staticDirty = true;
4902
4901
  let interactiveDirty = false;
4903
4902
  let overlaySet = /* @__PURE__ */ new Set();
@@ -5388,8 +5387,8 @@ var createRenderer = (opts) => {
5388
5387
  loop.requestFrame();
5389
5388
  },
5390
5389
  setSize(cssW, cssH) {
5391
- const a = sizeSurface(staticSurface, cssW, cssH);
5392
- const b = sizeSurface(interactiveSurface, cssW, cssH);
5390
+ const a = sizeSurface(staticSurface, cssW, cssH, maxDpr);
5391
+ const b = sizeSurface(interactiveSurface, cssW, cssH, maxDpr);
5393
5392
  if (a || b) {
5394
5393
  staticDirty = true;
5395
5394
  interactiveDirty = true;