js-draw 1.8.0 → 1.9.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.
@@ -28,6 +28,7 @@ export default class Display {
28
28
  this.editor = editor;
29
29
  this.parent = parent;
30
30
  this.textRerenderOutput = null;
31
+ this.devicePixelRatio = window.devicePixelRatio ?? 1;
31
32
  /**
32
33
  * @returns the color at the given point on the dry ink renderer, or `null` if `screenPos`
33
34
  * is not on the display.
@@ -112,16 +113,29 @@ export default class Display {
112
113
  this.parent.appendChild(wetInkCanvas);
113
114
  }
114
115
  this.resizeSurfacesCallback = () => {
116
+ const expectedWidth = (canvas) => {
117
+ return Math.ceil(canvas.clientWidth * this.devicePixelRatio);
118
+ };
119
+ const expectedHeight = (canvas) => {
120
+ return Math.ceil(canvas.clientHeight * this.devicePixelRatio);
121
+ };
115
122
  const hasSizeMismatch = (canvas) => {
116
- return canvas.clientHeight !== canvas.height || canvas.clientWidth !== canvas.width;
123
+ return expectedHeight(canvas) !== canvas.height || expectedWidth(canvas) !== canvas.width;
117
124
  };
118
125
  // Ensure that the drawing surfaces sizes match the
119
126
  // canvas' sizes to prevent stretching.
120
127
  if (hasSizeMismatch(dryInkCanvas) || hasSizeMismatch(wetInkCanvas)) {
121
- dryInkCanvas.width = dryInkCanvas.clientWidth;
122
- dryInkCanvas.height = dryInkCanvas.clientHeight;
123
- wetInkCanvas.width = wetInkCanvas.clientWidth;
124
- wetInkCanvas.height = wetInkCanvas.clientHeight;
128
+ dryInkCanvas.width = expectedWidth(dryInkCanvas);
129
+ dryInkCanvas.height = expectedHeight(dryInkCanvas);
130
+ wetInkCanvas.width = expectedWidth(wetInkCanvas);
131
+ wetInkCanvas.height = expectedHeight(wetInkCanvas);
132
+ // Ensure correct drawing operations on high-resolution screens.
133
+ // See
134
+ // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas#scaling_for_high_resolution_displays
135
+ wetInkCtx.resetTransform();
136
+ dryInkCtx.resetTransform();
137
+ dryInkCtx.scale(this.devicePixelRatio, this.devicePixelRatio);
138
+ wetInkCtx.scale(this.devicePixelRatio, this.devicePixelRatio);
125
139
  this.editor.notifier.dispatch(EditorEventType.DisplayResized, {
126
140
  kind: EditorEventType.DisplayResized,
127
141
  newSize: Vec2.of(this.width, this.height),
@@ -130,7 +144,10 @@ export default class Display {
130
144
  };
131
145
  this.resizeSurfacesCallback();
132
146
  this.flattenCallback = () => {
147
+ dryInkCtx.save();
148
+ dryInkCtx.resetTransform();
133
149
  dryInkCtx.drawImage(wetInkCanvas, 0, 0);
150
+ dryInkCtx.restore();
134
151
  };
135
152
  this.getColorAt = (screenPos) => {
136
153
  const pixel = dryInkCtx.getImageData(screenPos.x, screenPos.y, 1, 1);
@@ -156,6 +173,23 @@ export default class Display {
156
173
  textRendererOutputContainer.replaceChildren(rerenderButton, this.textRerenderOutput);
157
174
  this.editor.createHTMLOverlay(textRendererOutputContainer);
158
175
  }
176
+ /**
177
+ * Sets the device-pixel-ratio.
178
+ *
179
+ * Intended for debugging. Users do not need to call this manually.
180
+ *
181
+ * @internal
182
+ */
183
+ setDevicePixelRatio(dpr) {
184
+ const minDpr = 0.001;
185
+ const maxDpr = 10;
186
+ if (isFinite(dpr) && dpr >= minDpr && dpr <= maxDpr && dpr !== this.devicePixelRatio) {
187
+ this.devicePixelRatio = dpr;
188
+ this.resizeSurfacesCallback?.();
189
+ return this.editor.queueRerender();
190
+ }
191
+ return undefined;
192
+ }
159
193
  /**
160
194
  * Rerenders the text-based display.
161
195
  * The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
@@ -1,5 +1,4 @@
1
1
  import CacheRecord from './CacheRecord.mjs';
2
- const debugMode = false;
3
2
  export class CacheRecordManager {
4
3
  constructor(cacheProps) {
5
4
  // Fixed-size array: Cache blocks are assigned indicies into [cachedCanvases].
@@ -16,19 +15,19 @@ export class CacheRecordManager {
16
15
  const record = new CacheRecord(onDealloc, this.cacheState);
17
16
  record.setRenderingRegion(drawTo);
18
17
  this.cacheRecords.push(record);
19
- if (debugMode) {
18
+ if (this.cacheState.debugMode) {
20
19
  console.log('[Cache] Cache spaces used: ', this.cacheRecords.length, ' of ', this.maxCanvases);
21
20
  }
22
21
  return record;
23
22
  }
24
23
  else {
25
24
  const lru = this.getLeastRecentlyUsedRecord();
26
- if (debugMode) {
25
+ if (this.cacheState.debugMode) {
27
26
  console.log('[Cache] Re-alloc. Times allocated: ', lru.allocCount, '\nLast used cycle: ', lru.getLastUsedCycle(), '\nCurrent cycle: ', this.cacheState.currentRenderingCycle);
28
27
  }
29
28
  lru.realloc(onDealloc);
30
29
  lru.setRenderingRegion(drawTo);
31
- if (debugMode) {
30
+ if (this.cacheState.debugMode) {
32
31
  console.log('[Cache] Now re-alloc\'d. Last used cycle: ', lru.getLastUsedCycle());
33
32
  console.assert(lru['cacheState'] === this.cacheState, '[Cache] Unequal cache states! cacheState should be a shared object!');
34
33
  }
@@ -9,4 +9,5 @@ export default class RenderingCache {
9
9
  constructor(cacheProps: CacheProps);
10
10
  render(screenRenderer: AbstractRenderer, image: ImageNode, viewport: Viewport): void;
11
11
  getDebugInfo(): string;
12
+ setIsDebugMode(debugMode: boolean): void;
12
13
  }
@@ -8,6 +8,7 @@ export default class RenderingCache {
8
8
  props: cacheProps,
9
9
  currentRenderingCycle: 0,
10
10
  recordManager: this.recordManager,
11
+ debugMode: false,
11
12
  };
12
13
  this.recordManager.setSharedState(this.sharedState);
13
14
  }
@@ -44,4 +45,7 @@ export default class RenderingCache {
44
45
  getDebugInfo() {
45
46
  return this.recordManager.getDebugInfo();
46
47
  }
48
+ setIsDebugMode(debugMode) {
49
+ this.sharedState.debugMode = debugMode;
50
+ }
47
51
  }
@@ -1,10 +1,8 @@
1
1
  // A cache record with sub-nodes.
2
- import { sortLeavesByZIndex } from '../../image/EditorImage.mjs';
2
+ import { computeFirstIndexToRender, sortLeavesByZIndex } from '../../image/EditorImage.mjs';
3
3
  import { Rect2, Color4 } from '@js-draw/math';
4
4
  // 3x3 divisions for each node.
5
5
  const cacheDivisionSize = 3;
6
- // True: Show rendering updates.
7
- const debugMode = false;
8
6
  export default class RenderingCacheNode {
9
7
  constructor(region, cacheState) {
10
8
  this.region = region;
@@ -161,8 +159,8 @@ export default class RenderingCacheNode {
161
159
  items.forEach(item => item.render(screenRenderer, viewport.visibleRect));
162
160
  return;
163
161
  }
164
- if (debugMode) {
165
- screenRenderer.drawRect(this.region, 0.5 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
162
+ if (this.cacheState.debugMode) {
163
+ screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
166
164
  }
167
165
  // Could we render direclty from [this] or do we need to recurse?
168
166
  const couldRender = this.renderingWouldBeHighEnoughResolution(viewport);
@@ -197,7 +195,9 @@ export default class RenderingCacheNode {
197
195
  }
198
196
  let leafApproxRenderTime = 0;
199
197
  for (const leaf of leavesByIds) {
200
- leafApproxRenderTime += leaf.getContent().getProportionalRenderingTime();
198
+ if (!tooSmallToRender(leaf.getBBox())) {
199
+ leafApproxRenderTime += leaf.getContent().getProportionalRenderingTime();
200
+ }
201
201
  }
202
202
  // Is it worth it to render the items?
203
203
  if (leafApproxRenderTime > this.cacheState.props.minProportionalRenderTimePerCache) {
@@ -233,26 +233,30 @@ export default class RenderingCacheNode {
233
233
  this.renderedMaxZIndex = zIndex;
234
234
  }
235
235
  }
236
- if (debugMode) {
237
- screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.clay });
236
+ if (this.cacheState.debugMode) {
237
+ // Clay for adding new elements
238
+ screenRenderer.drawRect(this.region, 2 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.clay });
238
239
  }
239
240
  }
240
241
  }
241
- else if (debugMode) {
242
+ else if (this.cacheState.debugMode) {
242
243
  console.log('Decided on a full re-render. Reason: At least one of the following is false:', '\n leafIds.length > this.renderedIds.length: ', leafIds.length > this.renderedIds.length, '\n this.allRenderedIdsIn(leafIds): ', this.allRenderedIdsIn(leafIds), '\n this.renderedMaxZIndex !== null: ', this.renderedMaxZIndex !== null, '\n\nthis.rerenderedIds: ', this.renderedIds, ', leafIds: ', leafIds);
243
244
  }
244
245
  if (fullRerenderNeeded) {
245
246
  thisRenderer = this.cachedRenderer.startRender();
246
247
  thisRenderer.clear();
247
248
  this.renderedMaxZIndex = null;
248
- for (const leaf of leaves) {
249
+ const startIndex = computeFirstIndexToRender(leaves, this.region);
250
+ for (let i = startIndex; i < leaves.length; i++) {
251
+ const leaf = leaves[i];
249
252
  const content = leaf.getContent();
250
253
  this.renderedMaxZIndex ??= content.getZIndex();
251
254
  this.renderedMaxZIndex = Math.max(this.renderedMaxZIndex, content.getZIndex());
252
255
  leaf.render(thisRenderer, this.region);
253
256
  }
254
- if (debugMode) {
255
- screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.red });
257
+ if (this.cacheState.debugMode) {
258
+ // Red for full rerender
259
+ screenRenderer.drawRect(this.region, 3 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.red });
256
260
  }
257
261
  }
258
262
  this.renderedIds = leafIds;
@@ -269,6 +273,10 @@ export default class RenderingCacheNode {
269
273
  leaf.render(screenRenderer, this.region.intersection(viewport.visibleRect));
270
274
  }
271
275
  screenRenderer.endObject();
276
+ if (this.cacheState.debugMode) {
277
+ // Green for no cache needed render
278
+ screenRenderer.drawRect(this.region, 2 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.green });
279
+ }
272
280
  }
273
281
  }
274
282
  else {
@@ -16,4 +16,5 @@ export interface CacheState {
16
16
  currentRenderingCycle: number;
17
17
  props: CacheProps;
18
18
  recordManager: CacheRecordManager;
19
+ debugMode: boolean;
19
20
  }
@@ -77,7 +77,10 @@ export default class CanvasRenderer extends AbstractRenderer {
77
77
  return Vec2.of(this.ctx.canvas.clientWidth, this.ctx.canvas.clientHeight);
78
78
  }
79
79
  clear() {
80
+ this.ctx.save();
81
+ this.ctx.resetTransform();
80
82
  this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
83
+ this.ctx.restore();
81
84
  }
82
85
  beginPath(startPoint) {
83
86
  startPoint = this.canvasToScreen(startPoint);
@@ -1,3 +1,3 @@
1
1
  export default {
2
- number: '1.8.0',
2
+ number: '1.9.0',
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -55,7 +55,7 @@
55
55
  "license": "MIT",
56
56
  "private": false,
57
57
  "scripts": {
58
- "dist-test": "npm run build && cd dist-test/test_imports && npm install && npm run test",
58
+ "dist-test": "cd dist-test/test_imports && npm install && npm run test",
59
59
  "dist": "npm run build && npm run dist-test",
60
60
  "build": "rm -rf ./dist/* && mkdir -p dist && build-tool build",
61
61
  "watch": "rm -rf ./dist/* && mkdir -p dist && build-tool watch",
@@ -64,7 +64,7 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.8.0",
67
+ "@js-draw/math": "^1.9.0",
68
68
  "@melloware/coloris": "0.21.0"
69
69
  },
70
70
  "devDependencies": {
@@ -86,5 +86,5 @@
86
86
  "freehand",
87
87
  "svg"
88
88
  ],
89
- "gitHead": "89f7991833dec5c17ce0890321286198ff7b1900"
89
+ "gitHead": "e824c37e9f216852cf096976e3a74fb4f177ead3"
90
90
  }