js-draw 1.7.2 → 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.
- package/LICENSE +1 -1
- package/dist/Editor.css +1 -0
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/EventDispatcher.js +2 -1
- package/dist/cjs/SVGLoader.d.ts +1 -1
- package/dist/cjs/SVGLoader.js +11 -2
- package/dist/cjs/Viewport.d.ts +2 -0
- package/dist/cjs/Viewport.js +2 -0
- package/dist/cjs/components/AbstractComponent.d.ts +9 -0
- package/dist/cjs/components/AbstractComponent.js +11 -0
- package/dist/cjs/components/Stroke.d.ts +3 -0
- package/dist/cjs/components/Stroke.js +55 -1
- package/dist/cjs/image/EditorImage.d.ts +20 -0
- package/dist/cjs/image/EditorImage.js +48 -3
- package/dist/cjs/rendering/Display.d.ts +9 -0
- package/dist/cjs/rendering/Display.js +39 -5
- package/dist/cjs/rendering/RenderablePathSpec.d.ts +20 -2
- package/dist/cjs/rendering/RenderablePathSpec.js +72 -9
- package/dist/cjs/rendering/caching/CacheRecord.js +5 -3
- package/dist/cjs/rendering/caching/CacheRecordManager.js +3 -4
- package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
- package/dist/cjs/rendering/caching/RenderingCache.js +4 -0
- package/dist/cjs/rendering/caching/RenderingCacheNode.js +19 -11
- package/dist/cjs/rendering/caching/types.d.ts +1 -0
- package/dist/cjs/rendering/renderers/AbstractRenderer.d.ts +10 -0
- package/dist/cjs/rendering/renderers/AbstractRenderer.js +12 -3
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +5 -2
- package/dist/cjs/rendering/renderers/SVGRenderer.d.ts +0 -10
- package/dist/cjs/rendering/renderers/SVGRenderer.js +0 -14
- package/dist/cjs/testing/startPinchGesture.d.ts +14 -0
- package/dist/cjs/testing/startPinchGesture.js +41 -0
- package/dist/cjs/tools/PanZoom.d.ts +4 -0
- package/dist/cjs/tools/PanZoom.js +21 -2
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/EventDispatcher.mjs +2 -1
- package/dist/mjs/SVGLoader.d.ts +1 -1
- package/dist/mjs/SVGLoader.mjs +11 -2
- package/dist/mjs/Viewport.d.ts +2 -0
- package/dist/mjs/Viewport.mjs +2 -0
- package/dist/mjs/components/AbstractComponent.d.ts +9 -0
- package/dist/mjs/components/AbstractComponent.mjs +11 -0
- package/dist/mjs/components/Stroke.d.ts +3 -0
- package/dist/mjs/components/Stroke.mjs +56 -2
- package/dist/mjs/image/EditorImage.d.ts +20 -0
- package/dist/mjs/image/EditorImage.mjs +46 -2
- package/dist/mjs/rendering/Display.d.ts +9 -0
- package/dist/mjs/rendering/Display.mjs +39 -5
- package/dist/mjs/rendering/RenderablePathSpec.d.ts +20 -2
- package/dist/mjs/rendering/RenderablePathSpec.mjs +71 -9
- package/dist/mjs/rendering/caching/CacheRecord.mjs +5 -3
- package/dist/mjs/rendering/caching/CacheRecordManager.mjs +3 -4
- package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
- package/dist/mjs/rendering/caching/RenderingCache.mjs +4 -0
- package/dist/mjs/rendering/caching/RenderingCacheNode.mjs +20 -12
- package/dist/mjs/rendering/caching/types.d.ts +1 -0
- package/dist/mjs/rendering/renderers/AbstractRenderer.d.ts +10 -0
- package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +12 -3
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +5 -2
- package/dist/mjs/rendering/renderers/SVGRenderer.d.ts +0 -10
- package/dist/mjs/rendering/renderers/SVGRenderer.mjs +0 -14
- package/dist/mjs/testing/startPinchGesture.d.ts +14 -0
- package/dist/mjs/testing/startPinchGesture.mjs +36 -0
- package/dist/mjs/tools/PanZoom.d.ts +4 -0
- package/dist/mjs/tools/PanZoom.mjs +21 -2
- package/dist/mjs/version.mjs +1 -1
- package/package.json +4 -4
- package/src/Editor.scss +2 -0
@@ -71,12 +71,19 @@ class PanZoom extends BaseTool_1.default {
|
|
71
71
|
this.editor = editor;
|
72
72
|
this.mode = mode;
|
73
73
|
this.transform = null;
|
74
|
+
// Constants
|
75
|
+
// initialRotationSnapAngle is larger than afterRotationStartSnapAngle to
|
76
|
+
// make it more difficult to start rotating (and easier to continue rotating).
|
77
|
+
this.initialRotationSnapAngle = 0.22; // radians
|
78
|
+
this.afterRotationStartSnapAngle = 0.07; // radians
|
79
|
+
this.pinchZoomStartThreshold = 1.08; // scale factor
|
74
80
|
this.lastPointerDownTimestamp = 0;
|
75
81
|
this.initialTouchAngle = 0;
|
76
82
|
this.initialViewportRotation = 0;
|
77
83
|
// Set to `true` only when scaling has started (if two fingers are down and have moved
|
78
84
|
// far enough).
|
79
85
|
this.isScaling = false;
|
86
|
+
this.isRotating = false;
|
80
87
|
this.inertialScroller = null;
|
81
88
|
this.velocity = null;
|
82
89
|
}
|
@@ -118,6 +125,9 @@ class PanZoom extends BaseTool_1.default {
|
|
118
125
|
this.initialTouchAngle = angle;
|
119
126
|
this.initialViewportRotation = this.editor.viewport.getRotationAngle();
|
120
127
|
this.isScaling = false;
|
128
|
+
// We're initially rotated if `initialViewportRotation` isn't near a multiple of pi/2.
|
129
|
+
// In other words, if sin(2 initialViewportRotation) is near zero.
|
130
|
+
this.isRotating = Math.abs(Math.sin(this.initialViewportRotation * 2)) > 1e-3;
|
121
131
|
handlingGesture = true;
|
122
132
|
}
|
123
133
|
else if (pointers.length === 1 && ((this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
|
@@ -174,7 +184,9 @@ class PanZoom extends BaseTool_1.default {
|
|
174
184
|
const roundedFullRotation = Math.round(fullRotation / snapToMultipleOf) * snapToMultipleOf;
|
175
185
|
// The maximum angle for which we snap the given angle to a multiple of
|
176
186
|
// `snapToMultipleOf`.
|
177
|
-
|
187
|
+
// Use a smaller snap angle if already rotated (to avoid pinch zoom gestures from
|
188
|
+
// starting rotation).
|
189
|
+
const maxSnapAngle = this.isRotating ? this.afterRotationStartSnapAngle : this.initialRotationSnapAngle;
|
178
190
|
// Snap the rotation
|
179
191
|
if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
|
180
192
|
fullRotation = roundedFullRotation;
|
@@ -197,6 +209,11 @@ class PanZoom extends BaseTool_1.default {
|
|
197
209
|
else {
|
198
210
|
deltaRotation = this.toSnappedRotationDelta(angle);
|
199
211
|
}
|
212
|
+
// If any rotation, make a note of this (affects rotation snap
|
213
|
+
// angles).
|
214
|
+
if (Math.abs(deltaRotation) > 1e-8) {
|
215
|
+
this.isRotating = true;
|
216
|
+
}
|
200
217
|
this.updateVelocity(screenCenter);
|
201
218
|
let scaleFactor = 1;
|
202
219
|
if (this.isScaling) {
|
@@ -205,7 +222,9 @@ class PanZoom extends BaseTool_1.default {
|
|
205
222
|
else {
|
206
223
|
const initialScaleFactor = dist / this.startDist;
|
207
224
|
// Only start scaling if scaling done so far exceeds some threshold.
|
208
|
-
|
225
|
+
const upperBound = this.pinchZoomStartThreshold;
|
226
|
+
const lowerBound = 1 / this.pinchZoomStartThreshold;
|
227
|
+
if (initialScaleFactor > upperBound || initialScaleFactor < lowerBound) {
|
209
228
|
scaleFactor = initialScaleFactor;
|
210
229
|
this.isScaling = true;
|
211
230
|
}
|
package/dist/cjs/version.js
CHANGED
package/dist/mjs/SVGLoader.d.ts
CHANGED
@@ -18,7 +18,7 @@ export declare enum SVGLoaderLoadMethod {
|
|
18
18
|
export interface SVGLoaderOptions {
|
19
19
|
sanitize?: boolean;
|
20
20
|
disableUnknownObjectWarnings?: boolean;
|
21
|
-
loadMethod?:
|
21
|
+
loadMethod?: SVGLoaderLoadMethod;
|
22
22
|
}
|
23
23
|
export default class SVGLoader implements ImageLoader {
|
24
24
|
private source;
|
package/dist/mjs/SVGLoader.mjs
CHANGED
@@ -6,6 +6,7 @@ import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject.
|
|
6
6
|
import TextComponent, { TextTransformMode } from './components/TextComponent.mjs';
|
7
7
|
import UnknownSVGObject from './components/UnknownSVGObject.mjs';
|
8
8
|
import { pathToRenderable } from './rendering/RenderablePathSpec.mjs';
|
9
|
+
import { renderedStylesheetId } from './rendering/renderers/SVGRenderer.mjs';
|
9
10
|
// Size of a loaded image if no size is specified.
|
10
11
|
export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
|
11
12
|
// Key to retrieve unrecognised attributes from an AbstractComponent
|
@@ -475,7 +476,13 @@ export default class SVGLoader {
|
|
475
476
|
this.updateSVGAttrs(node);
|
476
477
|
break;
|
477
478
|
case 'style':
|
478
|
-
|
479
|
+
// Keeping unnecessary style sheets can cause the browser to keep all
|
480
|
+
// SVG elements *referenced* by the style sheet in some browsers.
|
481
|
+
//
|
482
|
+
// Only keep the style sheet if it won't be discarded on save.
|
483
|
+
if (node.getAttribute('id') !== renderedStylesheetId) {
|
484
|
+
await this.addUnknownNode(node);
|
485
|
+
}
|
479
486
|
break;
|
480
487
|
default:
|
481
488
|
if (!this.disableUnknownObjectWarnings) {
|
@@ -518,6 +525,7 @@ export default class SVGLoader {
|
|
518
525
|
this.onDetermineExportRect?.(defaultSVGViewRect);
|
519
526
|
}
|
520
527
|
this.onFinish?.();
|
528
|
+
this.onFinish = null;
|
521
529
|
}
|
522
530
|
/**
|
523
531
|
* Create an `SVGLoader` from the content of an SVG image. SVGs are loaded within a sandboxed
|
@@ -529,7 +537,7 @@ export default class SVGLoader {
|
|
529
537
|
* @param options - if `true` or `false`, treated as the `sanitize` option -- don't store unknown attributes.
|
530
538
|
*/
|
531
539
|
static fromString(text, options = false) {
|
532
|
-
const domParserLoad = typeof options !== 'boolean' && options?.loadMethod ===
|
540
|
+
const domParserLoad = typeof options !== 'boolean' && options?.loadMethod === SVGLoaderLoadMethod.DOMParser;
|
533
541
|
const { svgElem, cleanUp } = (() => {
|
534
542
|
// If the user requested an iframe load (the default) try to load with an iframe.
|
535
543
|
// There are some cases (e.g. in a sandboxed iframe) where this doesn't work.
|
@@ -577,6 +585,7 @@ export default class SVGLoader {
|
|
577
585
|
const cleanUp = () => {
|
578
586
|
svgElem.remove();
|
579
587
|
sandbox.remove();
|
588
|
+
sandbox.src = '';
|
580
589
|
};
|
581
590
|
return { svgElem, cleanUp };
|
582
591
|
}
|
package/dist/mjs/Viewport.d.ts
CHANGED
@@ -56,6 +56,8 @@ export declare class Viewport {
|
|
56
56
|
/**
|
57
57
|
* @returns the angle of the canvas in radians.
|
58
58
|
* This is the angle by which the canvas is rotated relative to the screen.
|
59
|
+
*
|
60
|
+
* Returns an angle in the range $[-\pi, \pi]$ (the same range as {@link Vec3.angle}).
|
59
61
|
*/
|
60
62
|
getRotationAngle(): number;
|
61
63
|
/**
|
package/dist/mjs/Viewport.mjs
CHANGED
@@ -114,6 +114,8 @@ export class Viewport {
|
|
114
114
|
/**
|
115
115
|
* @returns the angle of the canvas in radians.
|
116
116
|
* This is the angle by which the canvas is rotated relative to the screen.
|
117
|
+
*
|
118
|
+
* Returns an angle in the range $[-\pi, \pi]$ (the same range as {@link Vec3.angle}).
|
117
119
|
*/
|
118
120
|
getRotationAngle() {
|
119
121
|
return this.transform.transformVec3(Vec3.unitX).angle();
|
@@ -77,6 +77,15 @@ export default abstract class AbstractComponent {
|
|
77
77
|
* {@link EditorImage}.
|
78
78
|
*/
|
79
79
|
getSizingMode(): ComponentSizingMode;
|
80
|
+
/**
|
81
|
+
* **Optimization**
|
82
|
+
*
|
83
|
+
* Should return `true` if this component covers the entire `visibleRect`
|
84
|
+
* and would prevent anything below this component from being visible.
|
85
|
+
*
|
86
|
+
* Should return `false` otherwise.
|
87
|
+
*/
|
88
|
+
occludesEverythingBelowWhenRenderedInRect(_visibleRect: Rect2): boolean;
|
80
89
|
/** Called when this component is added to the given image. */
|
81
90
|
onAddToImage(_image: EditorImage): void;
|
82
91
|
onRemoveFromImage(): void;
|
@@ -102,6 +102,17 @@ class AbstractComponent {
|
|
102
102
|
getSizingMode() {
|
103
103
|
return ComponentSizingMode.BoundingBox;
|
104
104
|
}
|
105
|
+
/**
|
106
|
+
* **Optimization**
|
107
|
+
*
|
108
|
+
* Should return `true` if this component covers the entire `visibleRect`
|
109
|
+
* and would prevent anything below this component from being visible.
|
110
|
+
*
|
111
|
+
* Should return `false` otherwise.
|
112
|
+
*/
|
113
|
+
occludesEverythingBelowWhenRenderedInRect(_visibleRect) {
|
114
|
+
return false;
|
115
|
+
}
|
105
116
|
/** Called when this component is added to the given image. */
|
106
117
|
onAddToImage(_image) { }
|
107
118
|
onRemoveFromImage() { }
|
@@ -49,6 +49,9 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
|
|
49
49
|
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
50
50
|
intersects(line: LineSegment2): boolean;
|
51
51
|
intersectsRect(rect: Rect2): boolean;
|
52
|
+
private simplifiedPath;
|
53
|
+
private computeSimplifiedPathFor;
|
54
|
+
occludesEverythingBelowWhenRenderedInRect(rect: Rect2): boolean;
|
52
55
|
render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
53
56
|
getProportionalRenderingTime(): number;
|
54
57
|
private bboxForPart;
|
@@ -2,7 +2,7 @@ import { Path, Rect2 } from '@js-draw/math';
|
|
2
2
|
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle.mjs';
|
3
3
|
import AbstractComponent from './AbstractComponent.mjs';
|
4
4
|
import { createRestyleComponentCommand } from './RestylableComponent.mjs';
|
5
|
-
import { pathFromRenderable, pathToRenderable } from '../rendering/RenderablePathSpec.mjs';
|
5
|
+
import { pathFromRenderable, pathToRenderable, simplifyPathToFullScreenOrEmpty } from '../rendering/RenderablePathSpec.mjs';
|
6
6
|
/**
|
7
7
|
* Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
|
8
8
|
*
|
@@ -41,6 +41,9 @@ export default class Stroke extends AbstractComponent {
|
|
41
41
|
// @internal
|
42
42
|
// eslint-disable-next-line @typescript-eslint/prefer-as-const
|
43
43
|
this.isRestylableComponent = true;
|
44
|
+
// A simplification of the path for a given visibleRect. Intended
|
45
|
+
// to help check for occlusion.
|
46
|
+
this.simplifiedPath = null;
|
44
47
|
this.approximateRenderingTime = 0;
|
45
48
|
this.parts = [];
|
46
49
|
for (const section of parts) {
|
@@ -166,9 +169,60 @@ export default class Stroke extends AbstractComponent {
|
|
166
169
|
}
|
167
170
|
return super.intersectsRect(rect);
|
168
171
|
}
|
172
|
+
computeSimplifiedPathFor(visibleRect) {
|
173
|
+
const simplifiedParts = [];
|
174
|
+
let occludes = false;
|
175
|
+
let skipSimplification = false;
|
176
|
+
for (const part of this.parts) {
|
177
|
+
if (skipSimplification
|
178
|
+
// Simplification currently only works for stroked paths
|
179
|
+
|| !part.style.stroke
|
180
|
+
// One of the main purposes of this is to check for occlusion.
|
181
|
+
// We can't occlude things if the stroke is partially transparent.
|
182
|
+
|| part.style.stroke.color.a < 0.99) {
|
183
|
+
simplifiedParts.push(part);
|
184
|
+
continue;
|
185
|
+
}
|
186
|
+
const mapping = simplifyPathToFullScreenOrEmpty(part, visibleRect);
|
187
|
+
if (mapping) {
|
188
|
+
simplifiedParts.push(mapping.path);
|
189
|
+
if (mapping.fullScreen) {
|
190
|
+
occludes = true;
|
191
|
+
skipSimplification = true;
|
192
|
+
}
|
193
|
+
}
|
194
|
+
else {
|
195
|
+
simplifiedParts.push(part);
|
196
|
+
}
|
197
|
+
}
|
198
|
+
return {
|
199
|
+
forVisibleRect: visibleRect,
|
200
|
+
parts: simplifiedParts,
|
201
|
+
occludes,
|
202
|
+
};
|
203
|
+
}
|
204
|
+
occludesEverythingBelowWhenRenderedInRect(rect) {
|
205
|
+
// Can't occlude if doesn't contain.
|
206
|
+
if (!this.getBBox().containsRect(rect)) {
|
207
|
+
return false;
|
208
|
+
}
|
209
|
+
if (!this.simplifiedPath || !this.simplifiedPath.forVisibleRect.eq(rect)) {
|
210
|
+
this.simplifiedPath = this.computeSimplifiedPathFor(rect);
|
211
|
+
}
|
212
|
+
return this.simplifiedPath.occludes;
|
213
|
+
}
|
169
214
|
render(canvas, visibleRect) {
|
170
215
|
canvas.startObject(this.getBBox());
|
171
|
-
|
216
|
+
// Can we use a cached simplified path for faster rendering?
|
217
|
+
let parts = this.parts;
|
218
|
+
if (visibleRect && this.simplifiedPath?.forVisibleRect?.containsRect(visibleRect)) {
|
219
|
+
parts = this.simplifiedPath.parts;
|
220
|
+
}
|
221
|
+
else {
|
222
|
+
// Save memory
|
223
|
+
this.simplifiedPath = null;
|
224
|
+
}
|
225
|
+
for (const part of parts) {
|
172
226
|
const bbox = this.bboxForPart(part.path.bbox, part.style);
|
173
227
|
if (visibleRect) {
|
174
228
|
if (!bbox.intersects(visibleRect)) {
|
@@ -123,8 +123,28 @@ export default class EditorImage {
|
|
123
123
|
*/
|
124
124
|
private setExportRectDirectly;
|
125
125
|
private onExportViewportChanged;
|
126
|
+
/**
|
127
|
+
* @internal
|
128
|
+
*
|
129
|
+
* Enables debug mode for **all** `EditorImage`s.
|
130
|
+
*
|
131
|
+
* **Only use for debugging**.
|
132
|
+
*
|
133
|
+
* @internal
|
134
|
+
*/
|
135
|
+
setDebugMode(newDebugMode: boolean): void;
|
126
136
|
private static SetImportExportRectCommand;
|
127
137
|
}
|
138
|
+
/**
|
139
|
+
* Determines the first index in `sortedLeaves` that needs to be rendered
|
140
|
+
* (based on occlusion -- everything before that index can be skipped and
|
141
|
+
* produce a visually-equivalent image).
|
142
|
+
*
|
143
|
+
* Does nothing if visibleRect is not provided
|
144
|
+
*
|
145
|
+
* @internal
|
146
|
+
*/
|
147
|
+
export declare const computeFirstIndexToRender: (sortedLeaves: Array<ImageNode>, visibleRect?: Rect2) => number;
|
128
148
|
type TooSmallToRenderCheck = (rect: Rect2) => boolean;
|
129
149
|
/**
|
130
150
|
* Part of the Editor's image. Does not handle fullscreen/invisible components.
|
@@ -19,7 +19,7 @@ export var EditorImageEventType;
|
|
19
19
|
EditorImageEventType[EditorImageEventType["ExportViewportChanged"] = 0] = "ExportViewportChanged";
|
20
20
|
EditorImageEventType[EditorImageEventType["AutoresizeModeChanged"] = 1] = "AutoresizeModeChanged";
|
21
21
|
})(EditorImageEventType || (EditorImageEventType = {}));
|
22
|
-
|
22
|
+
let debugMode = false;
|
23
23
|
// Handles lookup/storage of elements in the image
|
24
24
|
class EditorImage {
|
25
25
|
// @internal
|
@@ -271,6 +271,18 @@ class EditorImage {
|
|
271
271
|
});
|
272
272
|
}
|
273
273
|
}
|
274
|
+
/**
|
275
|
+
* @internal
|
276
|
+
*
|
277
|
+
* Enables debug mode for **all** `EditorImage`s.
|
278
|
+
*
|
279
|
+
* **Only use for debugging**.
|
280
|
+
*
|
281
|
+
* @internal
|
282
|
+
*/
|
283
|
+
setDebugMode(newDebugMode) {
|
284
|
+
debugMode = newDebugMode;
|
285
|
+
}
|
274
286
|
}
|
275
287
|
_a = EditorImage;
|
276
288
|
// A Command that can access private [EditorImage] functionality
|
@@ -410,6 +422,30 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
|
|
410
422
|
})(),
|
411
423
|
_c);
|
412
424
|
export default EditorImage;
|
425
|
+
/**
|
426
|
+
* Determines the first index in `sortedLeaves` that needs to be rendered
|
427
|
+
* (based on occlusion -- everything before that index can be skipped and
|
428
|
+
* produce a visually-equivalent image).
|
429
|
+
*
|
430
|
+
* Does nothing if visibleRect is not provided
|
431
|
+
*
|
432
|
+
* @internal
|
433
|
+
*/
|
434
|
+
export const computeFirstIndexToRender = (sortedLeaves, visibleRect) => {
|
435
|
+
let startIndex = 0;
|
436
|
+
if (visibleRect) {
|
437
|
+
for (let i = sortedLeaves.length - 1; i >= 1; i--) {
|
438
|
+
if (
|
439
|
+
// Check for occlusion
|
440
|
+
sortedLeaves[i].getBBox().containsRect(visibleRect)
|
441
|
+
&& sortedLeaves[i].getContent()?.occludesEverythingBelowWhenRenderedInRect(visibleRect)) {
|
442
|
+
startIndex = i;
|
443
|
+
break;
|
444
|
+
}
|
445
|
+
}
|
446
|
+
}
|
447
|
+
return startIndex;
|
448
|
+
};
|
413
449
|
/**
|
414
450
|
* Part of the Editor's image. Does not handle fullscreen/invisible components.
|
415
451
|
* @internal
|
@@ -663,12 +699,20 @@ export class ImageNode {
|
|
663
699
|
leaves = this.getLeaves();
|
664
700
|
}
|
665
701
|
sortLeavesByZIndex(leaves);
|
666
|
-
|
702
|
+
// If some components hide others (and we're permitted to simplify,
|
703
|
+
// which is true in the case of visibleRect being defined), then only
|
704
|
+
// draw the non-hidden components:
|
705
|
+
const startIndex = computeFirstIndexToRender(leaves);
|
706
|
+
for (let i = startIndex; i < leaves.length; i++) {
|
707
|
+
const leaf = leaves[i];
|
667
708
|
// Leaves by definition have content
|
668
709
|
leaf.getContent().render(renderer, visibleRect);
|
669
710
|
}
|
670
711
|
// Show debug information
|
671
712
|
if (debugMode && visibleRect) {
|
713
|
+
if (startIndex !== 0) {
|
714
|
+
console.log('EditorImage: skipped ', startIndex, 'nodes due to occlusion');
|
715
|
+
}
|
672
716
|
this.renderDebugBoundingBoxes(renderer, visibleRect);
|
673
717
|
}
|
674
718
|
}
|
@@ -26,6 +26,7 @@ export default class Display {
|
|
26
26
|
private textRenderer;
|
27
27
|
private textRerenderOutput;
|
28
28
|
private cache;
|
29
|
+
private devicePixelRatio;
|
29
30
|
private resizeSurfacesCallback?;
|
30
31
|
private flattenCallback?;
|
31
32
|
/** @internal */
|
@@ -46,6 +47,14 @@ export default class Display {
|
|
46
47
|
getColorAt: (_screenPos: Point2) => Color4 | null;
|
47
48
|
private initializeCanvasRendering;
|
48
49
|
private initializeTextRendering;
|
50
|
+
/**
|
51
|
+
* Sets the device-pixel-ratio.
|
52
|
+
*
|
53
|
+
* Intended for debugging. Users do not need to call this manually.
|
54
|
+
*
|
55
|
+
* @internal
|
56
|
+
*/
|
57
|
+
setDevicePixelRatio(dpr: number): Promise<void> | undefined;
|
49
58
|
/**
|
50
59
|
* Rerenders the text-based display.
|
51
60
|
* The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
|
@@ -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
|
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
|
122
|
-
dryInkCanvas.height = dryInkCanvas
|
123
|
-
wetInkCanvas.width = wetInkCanvas
|
124
|
-
wetInkCanvas.height = wetInkCanvas
|
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`.
|
@@ -6,11 +6,29 @@ interface RenderablePathSpec {
|
|
6
6
|
style: RenderingStyle;
|
7
7
|
path?: Path;
|
8
8
|
}
|
9
|
+
interface RenderablePathSpecWithPath extends RenderablePathSpec {
|
10
|
+
path: Path;
|
11
|
+
}
|
9
12
|
/** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
|
10
13
|
export declare const pathFromRenderable: (renderable: RenderablePathSpec) => Path;
|
11
|
-
export declare const pathToRenderable: (path: Path, style: RenderingStyle) =>
|
14
|
+
export declare const pathToRenderable: (path: Path, style: RenderingStyle) => RenderablePathSpecWithPath;
|
15
|
+
interface RectangleSimplificationResult {
|
16
|
+
rectangle: Rect2;
|
17
|
+
path: RenderablePathSpecWithPath;
|
18
|
+
fullScreen: boolean;
|
19
|
+
}
|
20
|
+
/**
|
21
|
+
* Tries to simplify the given path to a fullscreen rectangle.
|
22
|
+
* Returns `null` on failure.
|
23
|
+
*
|
24
|
+
* @internal
|
25
|
+
*/
|
26
|
+
export declare const simplifyPathToFullScreenOrEmpty: (renderablePath: RenderablePathSpec, visibleRect: Rect2, options?: {
|
27
|
+
fastCheck: boolean;
|
28
|
+
expensiveCheck: boolean;
|
29
|
+
}) => RectangleSimplificationResult | null;
|
12
30
|
/**
|
13
31
|
* @returns a Path that, when rendered, looks roughly equivalent to the given path.
|
14
32
|
*/
|
15
|
-
export declare const visualEquivalent: (renderablePath: RenderablePathSpec, visibleRect: Rect2) =>
|
33
|
+
export declare const visualEquivalent: (renderablePath: RenderablePathSpec, visibleRect: Rect2) => RenderablePathSpecWithPath;
|
16
34
|
export default RenderablePathSpec;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Mat33, Path, PathCommandType } from '@js-draw/math';
|
1
|
+
import { Color4, Mat33, Path, PathCommandType, Rect2 } from '@js-draw/math';
|
2
2
|
/** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
|
3
3
|
export const pathFromRenderable = (renderable) => {
|
4
4
|
if (renderable.path) {
|
@@ -15,33 +15,89 @@ export const pathToRenderable = (path, style) => {
|
|
15
15
|
};
|
16
16
|
};
|
17
17
|
/**
|
18
|
-
*
|
18
|
+
* Fills the optional `path` field in `RenderablePathSpec`
|
19
|
+
* with `path` if not already filled
|
19
20
|
*/
|
20
|
-
|
21
|
+
const pathIncluded = (renderablePath, path) => {
|
22
|
+
if (renderablePath.path) {
|
23
|
+
return renderablePath;
|
24
|
+
}
|
25
|
+
return {
|
26
|
+
...renderablePath,
|
27
|
+
path,
|
28
|
+
};
|
29
|
+
};
|
30
|
+
/**
|
31
|
+
* Tries to simplify the given path to a fullscreen rectangle.
|
32
|
+
* Returns `null` on failure.
|
33
|
+
*
|
34
|
+
* @internal
|
35
|
+
*/
|
36
|
+
export const simplifyPathToFullScreenOrEmpty = (renderablePath, visibleRect, options = { fastCheck: true, expensiveCheck: true }) => {
|
21
37
|
const path = pathFromRenderable(renderablePath);
|
22
38
|
const strokeWidth = renderablePath.style.stroke?.width ?? 0;
|
23
39
|
const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
|
24
40
|
const styledPathBBox = path.bbox.grownBy(strokeWidth);
|
25
41
|
// Are we close enough to the path that it fills the entire screen?
|
26
|
-
|
27
|
-
&& renderablePath.style.stroke
|
42
|
+
const isOnlyStrokedAndCouldFillScreen = (onlyStroked
|
28
43
|
&& strokeWidth > visibleRect.maxDimension
|
29
|
-
&& styledPathBBox.containsRect(visibleRect))
|
44
|
+
&& styledPathBBox.containsRect(visibleRect));
|
45
|
+
if (options.fastCheck && isOnlyStrokedAndCouldFillScreen && renderablePath.style.stroke) {
|
30
46
|
const strokeRadius = strokeWidth / 2;
|
47
|
+
// Are we completely within the stroke?
|
31
48
|
// Do a fast, but with many false negatives, check.
|
32
49
|
for (const point of path.startEndPoints()) {
|
33
50
|
// If within the strokeRadius of any point
|
34
51
|
if (visibleRect.isWithinRadiusOf(strokeRadius, point)) {
|
35
|
-
return
|
52
|
+
return {
|
53
|
+
rectangle: visibleRect,
|
54
|
+
path: pathToRenderable(Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color }),
|
55
|
+
fullScreen: true,
|
56
|
+
};
|
36
57
|
}
|
37
58
|
}
|
38
59
|
}
|
60
|
+
// Try filtering again, but with slightly more expensive checks
|
61
|
+
if (options.expensiveCheck &&
|
62
|
+
isOnlyStrokedAndCouldFillScreen && renderablePath.style.stroke
|
63
|
+
&& strokeWidth > visibleRect.maxDimension * 3) {
|
64
|
+
const signedDist = path.signedDistance(visibleRect.center, strokeWidth / 2);
|
65
|
+
const margin = strokeWidth / 6;
|
66
|
+
if (signedDist < -visibleRect.maxDimension / 2 - margin) {
|
67
|
+
return {
|
68
|
+
path: pathToRenderable(Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color }),
|
69
|
+
rectangle: visibleRect,
|
70
|
+
fullScreen: true,
|
71
|
+
};
|
72
|
+
}
|
73
|
+
else if (signedDist > visibleRect.maxDimension / 2 + margin) {
|
74
|
+
return {
|
75
|
+
path: pathToRenderable(Path.empty, { fill: Color4.transparent }),
|
76
|
+
rectangle: Rect2.empty,
|
77
|
+
fullScreen: false,
|
78
|
+
};
|
79
|
+
}
|
80
|
+
}
|
81
|
+
return null;
|
82
|
+
};
|
83
|
+
/**
|
84
|
+
* @returns a Path that, when rendered, looks roughly equivalent to the given path.
|
85
|
+
*/
|
86
|
+
export const visualEquivalent = (renderablePath, visibleRect) => {
|
87
|
+
const path = pathFromRenderable(renderablePath);
|
88
|
+
const strokeWidth = renderablePath.style.stroke?.width ?? 0;
|
89
|
+
const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
|
90
|
+
const styledPathBBox = path.bbox.grownBy(strokeWidth);
|
91
|
+
let rectangleSimplification = simplifyPathToFullScreenOrEmpty(renderablePath, visibleRect, { fastCheck: true, expensiveCheck: false, });
|
92
|
+
if (rectangleSimplification) {
|
93
|
+
return rectangleSimplification.path;
|
94
|
+
}
|
39
95
|
// Scale the expanded rect --- the visual equivalent is only close for huge strokes.
|
40
96
|
const expandedRect = visibleRect.grownBy(strokeWidth)
|
41
97
|
.transformedBoundingBox(Mat33.scaling2D(4, visibleRect.center));
|
42
98
|
// TODO: Handle simplifying very small paths.
|
43
99
|
if (expandedRect.containsRect(styledPathBBox)) {
|
44
|
-
return renderablePath;
|
100
|
+
return pathIncluded(renderablePath, path);
|
45
101
|
}
|
46
102
|
const parts = [];
|
47
103
|
let startPoint = path.startPoint;
|
@@ -76,5 +132,11 @@ export const visualEquivalent = (renderablePath, visibleRect) => {
|
|
76
132
|
}
|
77
133
|
startPoint = endPoint;
|
78
134
|
}
|
79
|
-
|
135
|
+
const newPath = new Path(path.startPoint, parts);
|
136
|
+
const newStyle = renderablePath.style;
|
137
|
+
rectangleSimplification = simplifyPathToFullScreenOrEmpty(renderablePath, visibleRect, { fastCheck: false, expensiveCheck: true, });
|
138
|
+
if (rectangleSimplification) {
|
139
|
+
return rectangleSimplification.path;
|
140
|
+
}
|
141
|
+
return pathToRenderable(newPath, newStyle);
|
80
142
|
};
|
@@ -47,8 +47,10 @@ export default class CacheRecord {
|
|
47
47
|
return transform;
|
48
48
|
}
|
49
49
|
setRenderingRegion(drawTo) {
|
50
|
-
this.
|
51
|
-
|
52
|
-
|
50
|
+
const transform = this.getTransform(drawTo);
|
51
|
+
this.renderer.setTransform(transform);
|
52
|
+
// The visible region may be slightly larger than where we're actually drawing
|
53
|
+
// to (because of rounding).
|
54
|
+
this.renderer.overrideVisibleRect(drawTo.grownBy(1 / transform.getScaleFactor()));
|
53
55
|
}
|
54
56
|
}
|