js-draw 0.10.1 → 0.10.3
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/CHANGELOG.md +6 -0
- package/dist/bundle.js +1 -1
- package/dist/src/components/AbstractComponent.d.ts +1 -0
- package/dist/src/components/AbstractComponent.js +6 -0
- package/dist/src/components/ImageComponent.d.ts +1 -0
- package/dist/src/components/ImageComponent.js +4 -0
- package/dist/src/components/Stroke.d.ts +2 -0
- package/dist/src/components/Stroke.js +5 -0
- package/dist/src/components/TextComponent.d.ts +1 -0
- package/dist/src/components/TextComponent.js +3 -0
- package/dist/src/components/util/StrokeSmoother.js +9 -5
- package/dist/src/math/Mat33.d.ts +2 -0
- package/dist/src/math/Mat33.js +7 -0
- package/dist/src/math/Path.js +3 -0
- package/dist/src/rendering/Display.js +5 -2
- package/dist/src/rendering/caching/RenderingCache.js +5 -1
- package/dist/src/rendering/caching/RenderingCacheNode.js +5 -2
- package/dist/src/rendering/caching/testUtils.js +1 -1
- package/dist/src/rendering/caching/types.d.ts +2 -2
- package/dist/src/toolbar/IconProvider.d.ts +1 -0
- package/dist/src/toolbar/IconProvider.js +12 -0
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/tools/PanZoom.d.ts +2 -1
- package/dist/src/tools/PanZoom.js +33 -17
- package/package.json +11 -11
- package/src/components/AbstractComponent.ts +8 -0
- package/src/components/ImageComponent.ts +5 -0
- package/src/components/Stroke.ts +11 -0
- package/src/components/TextComponent.ts +4 -0
- package/src/components/util/StrokeSmoother.ts +12 -9
- package/src/math/Mat33.ts +9 -0
- package/src/math/Path.ts +4 -0
- package/src/rendering/Display.ts +7 -2
- package/src/rendering/caching/RenderingCache.ts +10 -2
- package/src/rendering/caching/RenderingCacheNode.ts +6 -2
- package/src/rendering/caching/testUtils.ts +2 -2
- package/src/rendering/caching/types.ts +2 -2
- package/src/toolbar/IconProvider.ts +13 -0
- package/src/toolbar/localization.ts +3 -0
- package/src/tools/PanZoom.ts +34 -14
@@ -29,6 +29,7 @@ export default abstract class AbstractComponent {
|
|
29
29
|
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
30
30
|
transformBy(affineTransfm: Mat33): SerializableCommand;
|
31
31
|
isSelectable(): boolean;
|
32
|
+
getProportionalRenderingTime(): number;
|
32
33
|
private static transformElementCommandId;
|
33
34
|
private static UnresolvedTransformElementCommand;
|
34
35
|
private static TransformElementCommand;
|
@@ -52,6 +52,12 @@ export default class AbstractComponent {
|
|
52
52
|
isSelectable() {
|
53
53
|
return true;
|
54
54
|
}
|
55
|
+
// @returns an approximation of the proportional time it takes to render this component.
|
56
|
+
// This is intended to be a rough estimate, but, for example, a stroke with two points sould have
|
57
|
+
// a renderingWeight approximately twice that of a stroke with one point.
|
58
|
+
getProportionalRenderingTime() {
|
59
|
+
return 1;
|
60
|
+
}
|
55
61
|
// Returns a copy of this component.
|
56
62
|
clone() {
|
57
63
|
const clone = this.createClone();
|
@@ -12,6 +12,7 @@ export default class ImageComponent extends AbstractComponent {
|
|
12
12
|
private recomputeBBox;
|
13
13
|
static fromImage(elem: HTMLImageElement, transform: Mat33): Promise<ImageComponent>;
|
14
14
|
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
15
|
+
getProportionalRenderingTime(): number;
|
15
16
|
intersects(lineSegment: LineSegment2): boolean;
|
16
17
|
protected serializeToJSON(): {
|
17
18
|
src: string;
|
@@ -80,6 +80,10 @@ export default class ImageComponent extends AbstractComponent {
|
|
80
80
|
render(canvas, _visibleRect) {
|
81
81
|
canvas.drawImage(this.image);
|
82
82
|
}
|
83
|
+
getProportionalRenderingTime() {
|
84
|
+
// Estimate: Equivalent to a stroke with 10 segments.
|
85
|
+
return 10;
|
86
|
+
}
|
83
87
|
intersects(lineSegment) {
|
84
88
|
const rect = this.getImageRect();
|
85
89
|
const edges = rect.getEdges().map(edge => edge.transformedBy(this.image.transform));
|
@@ -8,9 +8,11 @@ import { ImageComponentLocalization } from './localization';
|
|
8
8
|
export default class Stroke extends AbstractComponent {
|
9
9
|
private parts;
|
10
10
|
protected contentBBox: Rect2;
|
11
|
+
private approximateRenderingTime;
|
11
12
|
constructor(parts: RenderablePathSpec[]);
|
12
13
|
intersects(line: LineSegment2): boolean;
|
13
14
|
render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
15
|
+
getProportionalRenderingTime(): number;
|
14
16
|
private bboxForPart;
|
15
17
|
protected applyTransformation(affineTransfm: Mat33): void;
|
16
18
|
getPath(): Path;
|
@@ -7,6 +7,7 @@ export default class Stroke extends AbstractComponent {
|
|
7
7
|
constructor(parts) {
|
8
8
|
var _a;
|
9
9
|
super('stroke');
|
10
|
+
this.approximateRenderingTime = 0;
|
10
11
|
this.parts = [];
|
11
12
|
for (const section of parts) {
|
12
13
|
const path = Path.fromRenderable(section);
|
@@ -24,6 +25,7 @@ export default class Stroke extends AbstractComponent {
|
|
24
25
|
style: section.style,
|
25
26
|
commands: path.parts,
|
26
27
|
});
|
28
|
+
this.approximateRenderingTime += path.parts.length;
|
27
29
|
}
|
28
30
|
(_a = this.contentBBox) !== null && _a !== void 0 ? _a : (this.contentBBox = Rect2.empty);
|
29
31
|
}
|
@@ -53,6 +55,9 @@ export default class Stroke extends AbstractComponent {
|
|
53
55
|
}
|
54
56
|
canvas.endObject(this.getLoadSaveData());
|
55
57
|
}
|
58
|
+
getProportionalRenderingTime() {
|
59
|
+
return this.approximateRenderingTime;
|
60
|
+
}
|
56
61
|
// Grows the bounding box for a given stroke part based on that part's style.
|
57
62
|
bboxForPart(origBBox, style) {
|
58
63
|
if (!style.stroke) {
|
@@ -26,6 +26,7 @@ export default class TextComponent extends AbstractComponent {
|
|
26
26
|
private recomputeBBox;
|
27
27
|
private renderInternal;
|
28
28
|
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
29
|
+
getProportionalRenderingTime(): number;
|
29
30
|
intersects(lineSegment: LineSegment2): boolean;
|
30
31
|
getBaselinePos(): import("../lib").Vec3;
|
31
32
|
getTextStyle(): TextStyle;
|
@@ -91,6 +91,9 @@ export default class TextComponent extends AbstractComponent {
|
|
91
91
|
this.renderInternal(canvas);
|
92
92
|
canvas.endObject(this.getLoadSaveData());
|
93
93
|
}
|
94
|
+
getProportionalRenderingTime() {
|
95
|
+
return this.textObjects.length;
|
96
|
+
}
|
94
97
|
intersects(lineSegment) {
|
95
98
|
// Convert canvas space to internal space.
|
96
99
|
const invTransform = this.transform.inverse();
|
@@ -60,6 +60,7 @@ export class StrokeSmoother {
|
|
60
60
|
this.buffer[this.buffer.length - 2], lastPoint,
|
61
61
|
];
|
62
62
|
this.currentCurve = null;
|
63
|
+
this.isFirstSegment = false;
|
63
64
|
}
|
64
65
|
// Returns [upper curve, connector, lower curve]
|
65
66
|
currentSegmentToPath() {
|
@@ -116,20 +117,23 @@ export class StrokeSmoother {
|
|
116
117
|
const pointRadius = newPoint.width;
|
117
118
|
const prevEndWidth = this.curveEndWidth;
|
118
119
|
this.curveEndWidth = pointRadius;
|
119
|
-
if (this.isFirstSegment) {
|
120
|
-
// The start of a curve often lacks accurate pressure information. Update it.
|
121
|
-
this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
|
122
|
-
}
|
123
120
|
// recompute bbox
|
124
121
|
this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
|
122
|
+
// If the last curve just ended or it's the first curve,
|
125
123
|
if (this.currentCurve === null) {
|
126
124
|
const p1 = lastPoint.pos;
|
127
125
|
const p2 = lastPoint.pos.plus((_b = this.lastExitingVec) !== null && _b !== void 0 ? _b : Vec2.unitX);
|
128
126
|
const p3 = newPoint.pos;
|
129
127
|
// Quadratic Bézier curve
|
130
128
|
this.currentCurve = new Bezier(p1.xy, p2.xy, p3.xy);
|
131
|
-
this.curveStartWidth = lastPoint.width;
|
132
129
|
console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
|
130
|
+
if (this.isFirstSegment) {
|
131
|
+
// The start of a curve often lacks accurate pressure information. Update it.
|
132
|
+
this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
|
133
|
+
}
|
134
|
+
else {
|
135
|
+
this.curveStartWidth = prevEndWidth;
|
136
|
+
}
|
133
137
|
}
|
134
138
|
// If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
|
135
139
|
let enteringVec = this.lastExitingVec;
|
package/dist/src/math/Mat33.d.ts
CHANGED
@@ -69,6 +69,8 @@ export default class Mat33 {
|
|
69
69
|
* This is the standard way of transforming vectors in ℝ³.
|
70
70
|
*/
|
71
71
|
transformVec3(other: Vec3): Vec3;
|
72
|
+
/** @returns true iff this is the identity matrix. */
|
73
|
+
isIdentity(): boolean;
|
72
74
|
/** Returns true iff this = other ± fuzz */
|
73
75
|
eq(other: Mat33, fuzz?: number): boolean;
|
74
76
|
toString(): string;
|
package/dist/src/math/Mat33.js
CHANGED
@@ -153,6 +153,13 @@ export default class Mat33 {
|
|
153
153
|
transformVec3(other) {
|
154
154
|
return Vec3.of(this.rows[0].dot(other), this.rows[1].dot(other), this.rows[2].dot(other));
|
155
155
|
}
|
156
|
+
/** @returns true iff this is the identity matrix. */
|
157
|
+
isIdentity() {
|
158
|
+
if (this === Mat33.identity) {
|
159
|
+
return true;
|
160
|
+
}
|
161
|
+
return this.eq(Mat33.identity);
|
162
|
+
}
|
156
163
|
/** Returns true iff this = other ± fuzz */
|
157
164
|
eq(other, fuzz = 0) {
|
158
165
|
for (let i = 0; i < 3; i++) {
|
package/dist/src/math/Path.js
CHANGED
@@ -179,6 +179,9 @@ export default class Path {
|
|
179
179
|
return new Path(startPoint, newParts);
|
180
180
|
}
|
181
181
|
transformedBy(affineTransfm) {
|
182
|
+
if (affineTransfm.isIdentity()) {
|
183
|
+
return this;
|
184
|
+
}
|
182
185
|
return this.mapPoints(point => affineTransfm.transformVec2(point));
|
183
186
|
}
|
184
187
|
// Creates a new path by joining [other] to the end of this path
|
@@ -73,8 +73,11 @@ export default class Display {
|
|
73
73
|
blockResolution: cacheBlockResolution,
|
74
74
|
cacheSize: 600 * 600 * 4 * 90,
|
75
75
|
maxScale: 1.4,
|
76
|
-
|
77
|
-
|
76
|
+
// Require about 20 strokes with 4 parts each to cache an image in one of the
|
77
|
+
// parts of the cache grid.
|
78
|
+
minProportionalRenderTimePerCache: 20 * 4,
|
79
|
+
// Require about 105 strokes with 4 parts each to use the cache at all.
|
80
|
+
minProportionalRenderTimeToUseCache: 105 * 4,
|
78
81
|
});
|
79
82
|
this.editor.notifier.on(EditorEventType.DisplayResized, event => {
|
80
83
|
var _a;
|
@@ -31,7 +31,11 @@ export default class RenderingCache {
|
|
31
31
|
}
|
32
32
|
this.rootNode = (_a = this.rootNode.smallestChildContaining(visibleRect)) !== null && _a !== void 0 ? _a : this.rootNode;
|
33
33
|
const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
|
34
|
-
|
34
|
+
let approxVisibleRenderTime = 0;
|
35
|
+
for (const leaf of visibleLeaves) {
|
36
|
+
approxVisibleRenderTime += leaf.getContent().getProportionalRenderingTime();
|
37
|
+
}
|
38
|
+
if (approxVisibleRenderTime > this.sharedState.props.minProportionalRenderTimeToUseCache) {
|
35
39
|
this.rootNode.renderItems(screenRenderer, [image], viewport);
|
36
40
|
}
|
37
41
|
else {
|
@@ -197,9 +197,12 @@ export default class RenderingCacheNode {
|
|
197
197
|
}
|
198
198
|
return;
|
199
199
|
}
|
200
|
+
let leafApproxRenderTime = 0;
|
201
|
+
for (const leaf of leavesByIds) {
|
202
|
+
leafApproxRenderTime += leaf.getContent().getProportionalRenderingTime();
|
203
|
+
}
|
200
204
|
// Is it worth it to render the items?
|
201
|
-
|
202
|
-
if (leavesByIds.length > this.cacheState.props.minComponentsPerCache) {
|
205
|
+
if (leafApproxRenderTime > this.cacheState.props.minProportionalRenderTimePerCache) {
|
203
206
|
let fullRerenderNeeded = true;
|
204
207
|
if (!this.cachedRenderer) {
|
205
208
|
this.cachedRenderer = this.cacheState.recordManager.allocCanvas(this.region, () => this.onRegionDealloc());
|
@@ -12,7 +12,7 @@ export const createCache = (onRenderAlloc, cacheOptions) => {
|
|
12
12
|
},
|
13
13
|
isOfCorrectType(renderer) {
|
14
14
|
return renderer instanceof DummyRenderer;
|
15
|
-
}, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2,
|
15
|
+
}, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2, minProportionalRenderTimePerCache: 0, minProportionalRenderTimeToUseCache: 0 }, cacheOptions));
|
16
16
|
return {
|
17
17
|
cache,
|
18
18
|
editor
|
@@ -9,8 +9,8 @@ export interface CacheProps {
|
|
9
9
|
blockResolution: Vec2;
|
10
10
|
cacheSize: number;
|
11
11
|
maxScale: number;
|
12
|
-
|
13
|
-
|
12
|
+
minProportionalRenderTimePerCache: number;
|
13
|
+
minProportionalRenderTimeToUseCache: number;
|
14
14
|
}
|
15
15
|
export interface CacheState {
|
16
16
|
currentRenderingCycle: number;
|
@@ -25,6 +25,7 @@ export default class IconProvider {
|
|
25
25
|
makePipetteIcon(color?: Color4): IconType;
|
26
26
|
makeResizeViewportIcon(): IconType;
|
27
27
|
makeDuplicateSelectionIcon(): IconType;
|
28
|
+
makePasteIcon(): IconType;
|
28
29
|
makeDeleteSelectionIcon(): IconType;
|
29
30
|
makeSaveIcon(): IconType;
|
30
31
|
}
|
@@ -433,6 +433,18 @@ export default class IconProvider {
|
|
433
433
|
M 10,25 10,90 70,90 70,60 40,60 40,25 10,25 z
|
434
434
|
`);
|
435
435
|
}
|
436
|
+
makePasteIcon() {
|
437
|
+
const icon = this.makeIconFromPath(`
|
438
|
+
M 50 0 L 50 5 L 35 5 L 40 24.75 L 20 25 L 20 100 L 85 100 L 100 90 L 100 24 L 75.1 24.3 L 80 5 L 65 5 L 65 0 L 50 0 z
|
439
|
+
M 10 15 L 10 115 L 110 115 L 110 15 L 85 15 L 83 20 L 105 20 L 105 110 L 15 110 L 15 20 L 32 20 L 30 15 L 10 15 z
|
440
|
+
M 25 35 L 90 35 L 90 40 L 25 40 L 25 35 z
|
441
|
+
M 25 45 L 90 45 L 90 50 L 25 50 L 25 45 z
|
442
|
+
M 25 55 L 85 55 L 85 60 L 25 60 L 25 55 z
|
443
|
+
M 25 65 L 90 65 L 90 70 L 25 70 L 25 65 z
|
444
|
+
`);
|
445
|
+
icon.setAttribute('viewBox', '0 0 120 120');
|
446
|
+
return icon;
|
447
|
+
}
|
436
448
|
makeDeleteSelectionIcon() {
|
437
449
|
const strokeWidth = '5px';
|
438
450
|
const strokeColor = 'var(--icon-color)';
|
@@ -26,6 +26,7 @@ export interface ToolbarLocalization {
|
|
26
26
|
zoom: string;
|
27
27
|
resetView: string;
|
28
28
|
selectionToolKeyboardShortcuts: string;
|
29
|
+
paste: string;
|
29
30
|
dropdownShown: (toolName: string) => string;
|
30
31
|
dropdownHidden: (toolName: string) => string;
|
31
32
|
zoomLevel: (zoomPercentage: number) => string;
|
@@ -26,6 +26,7 @@ export const defaultToolbarLocalization = {
|
|
26
26
|
outlinedRectanglePen: 'Outlined rectangle',
|
27
27
|
filledRectanglePen: 'Filled rectangle',
|
28
28
|
lockRotation: 'Lock rotation',
|
29
|
+
paste: 'Paste',
|
29
30
|
dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
|
30
31
|
dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
|
31
32
|
zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
|
@@ -25,12 +25,13 @@ export default class PanZoom extends BaseTool {
|
|
25
25
|
private lastDist;
|
26
26
|
private lastScreenCenter;
|
27
27
|
private lastTimestamp;
|
28
|
+
private lastPointerDownTimestamp;
|
28
29
|
private inertialScroller;
|
29
30
|
private velocity;
|
30
31
|
constructor(editor: Editor, mode: PanZoomMode, description: string);
|
31
32
|
computePinchData(p1: Pointer, p2: Pointer): PinchData;
|
32
33
|
private allPointersAreOfType;
|
33
|
-
onPointerDown({ allPointers: pointers }: PointerEvt): boolean;
|
34
|
+
onPointerDown({ allPointers: pointers, current: currentPointer }: PointerEvt): boolean;
|
34
35
|
private updateVelocity;
|
35
36
|
private getCenterDelta;
|
36
37
|
private handleTwoFingerMove;
|
@@ -37,19 +37,19 @@ class InertialScroller {
|
|
37
37
|
if (this.running) {
|
38
38
|
return;
|
39
39
|
}
|
40
|
-
|
40
|
+
this.currentVelocity = this.initialVelocity;
|
41
41
|
let lastTime = (new Date()).getTime();
|
42
42
|
this.running = true;
|
43
|
-
const maxSpeed =
|
43
|
+
const maxSpeed = 5000; // units/s
|
44
44
|
const minSpeed = 200; // units/s
|
45
|
-
if (currentVelocity.magnitude() > maxSpeed) {
|
46
|
-
currentVelocity = currentVelocity.normalized().times(maxSpeed);
|
45
|
+
if (this.currentVelocity.magnitude() > maxSpeed) {
|
46
|
+
this.currentVelocity = this.currentVelocity.normalized().times(maxSpeed);
|
47
47
|
}
|
48
|
-
while (this.running && currentVelocity.magnitude() > minSpeed) {
|
48
|
+
while (this.running && this.currentVelocity.magnitude() > minSpeed) {
|
49
49
|
const nowTime = (new Date()).getTime();
|
50
50
|
const dt = (nowTime - lastTime) / 1000;
|
51
|
-
currentVelocity = currentVelocity.times(Math.pow(1 / 8, dt));
|
52
|
-
this.scrollBy(currentVelocity.times(dt));
|
51
|
+
this.currentVelocity = this.currentVelocity.times(Math.pow(1 / 8, dt));
|
52
|
+
this.scrollBy(this.currentVelocity.times(dt));
|
53
53
|
yield untilNextAnimationFrame();
|
54
54
|
lastTime = nowTime;
|
55
55
|
}
|
@@ -58,6 +58,12 @@ class InertialScroller {
|
|
58
58
|
}
|
59
59
|
});
|
60
60
|
}
|
61
|
+
getCurrentVelocity() {
|
62
|
+
if (!this.running) {
|
63
|
+
return null;
|
64
|
+
}
|
65
|
+
return this.currentVelocity;
|
66
|
+
}
|
61
67
|
stop() {
|
62
68
|
if (this.running) {
|
63
69
|
this.running = false;
|
@@ -71,6 +77,7 @@ export default class PanZoom extends BaseTool {
|
|
71
77
|
this.editor = editor;
|
72
78
|
this.mode = mode;
|
73
79
|
this.transform = null;
|
80
|
+
this.lastPointerDownTimestamp = 0;
|
74
81
|
this.inertialScroller = null;
|
75
82
|
this.velocity = null;
|
76
83
|
}
|
@@ -86,10 +93,13 @@ export default class PanZoom extends BaseTool {
|
|
86
93
|
allPointersAreOfType(pointers, kind) {
|
87
94
|
return pointers.every(pointer => pointer.device === kind);
|
88
95
|
}
|
89
|
-
onPointerDown({ allPointers: pointers }) {
|
90
|
-
var _a, _b;
|
96
|
+
onPointerDown({ allPointers: pointers, current: currentPointer }) {
|
97
|
+
var _a, _b, _c, _d;
|
91
98
|
let handlingGesture = false;
|
92
|
-
(_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.
|
99
|
+
const inertialScrollerVelocity = (_b = (_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.getCurrentVelocity()) !== null && _b !== void 0 ? _b : Vec2.zero;
|
100
|
+
(_c = this.inertialScroller) === null || _c === void 0 ? void 0 : _c.stop();
|
101
|
+
this.velocity = inertialScrollerVelocity;
|
102
|
+
this.lastPointerDownTimestamp = currentPointer.timeStamp;
|
93
103
|
const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
|
94
104
|
const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
|
95
105
|
if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
|
@@ -107,22 +117,24 @@ export default class PanZoom extends BaseTool {
|
|
107
117
|
}
|
108
118
|
if (handlingGesture) {
|
109
119
|
this.lastTimestamp = (new Date()).getTime();
|
110
|
-
(
|
120
|
+
(_d = this.transform) !== null && _d !== void 0 ? _d : (this.transform = Viewport.transformBy(Mat33.identity));
|
111
121
|
this.editor.display.setDraftMode(true);
|
112
122
|
}
|
113
123
|
return handlingGesture;
|
114
124
|
}
|
115
125
|
updateVelocity(currentCenter) {
|
116
126
|
const deltaPos = currentCenter.minus(this.lastScreenCenter);
|
117
|
-
|
118
|
-
// We divide by deltaTime. Don't divide by zero.
|
119
|
-
if (deltaTime === 0) {
|
120
|
-
return;
|
121
|
-
}
|
127
|
+
let deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
|
122
128
|
// Ignore duplicate events, unless there has been enough time between them.
|
123
129
|
if (deltaPos.magnitude() === 0 && deltaTime < 0.1) {
|
124
130
|
return;
|
125
131
|
}
|
132
|
+
// We divide by deltaTime. Don't divide by zero.
|
133
|
+
if (deltaTime === 0) {
|
134
|
+
return;
|
135
|
+
}
|
136
|
+
// Don't divide by almost zero, either
|
137
|
+
deltaTime = Math.max(deltaTime, 0.01);
|
126
138
|
const currentVelocity = deltaPos.times(1 / deltaTime);
|
127
139
|
let smoothedVelocity = currentVelocity;
|
128
140
|
if (this.velocity) {
|
@@ -184,7 +196,11 @@ export default class PanZoom extends BaseTool {
|
|
184
196
|
this.transform = null;
|
185
197
|
this.velocity = Vec2.zero;
|
186
198
|
};
|
187
|
-
const
|
199
|
+
const minInertialScrollDt = 30;
|
200
|
+
const shouldInertialScroll = event.current.device === PointerDevice.Touch
|
201
|
+
&& event.allPointers.length === 1
|
202
|
+
&& this.velocity !== null
|
203
|
+
&& event.current.timeStamp - this.lastPointerDownTimestamp > minInertialScrollDt;
|
188
204
|
if (shouldInertialScroll && this.velocity !== null) {
|
189
205
|
// If the user drags the screen, then stops, then lifts the pointer,
|
190
206
|
// we want the final velocity to reflect the stop at the end (so the velocity
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.10.
|
3
|
+
"version": "0.10.3",
|
4
4
|
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
|
5
5
|
"main": "./dist/src/lib.d.ts",
|
6
6
|
"types": "./dist/src/lib.js",
|
@@ -83,26 +83,26 @@
|
|
83
83
|
},
|
84
84
|
"devDependencies": {
|
85
85
|
"@types/bezier-js": "^4.1.0",
|
86
|
-
"@types/jest": "^29.2.
|
86
|
+
"@types/jest": "^29.2.5",
|
87
87
|
"@types/jsdom": "^20.0.1",
|
88
|
-
"@types/node": "^18.11.
|
88
|
+
"@types/node": "^18.11.18",
|
89
89
|
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
90
90
|
"@typescript-eslint/parser": "^5.44.0",
|
91
|
-
"css-loader": "^6.7.
|
92
|
-
"eslint": "^8.
|
93
|
-
"husky": "^8.0.
|
94
|
-
"jest": "^29.
|
91
|
+
"css-loader": "^6.7.3",
|
92
|
+
"eslint": "^8.31.0",
|
93
|
+
"husky": "^8.0.3",
|
94
|
+
"jest": "^29.3.1",
|
95
95
|
"jest-environment-jsdom": "^29.3.1",
|
96
96
|
"jsdom": "^20.0.3",
|
97
|
-
"lint-staged": "^13.0
|
97
|
+
"lint-staged": "^13.1.0",
|
98
98
|
"pinst": "^3.0.0",
|
99
99
|
"style-loader": "^3.3.1",
|
100
100
|
"terser-webpack-plugin": "^5.3.6",
|
101
101
|
"ts-jest": "^29.0.3",
|
102
|
-
"ts-loader": "^9.4.
|
102
|
+
"ts-loader": "^9.4.2",
|
103
103
|
"ts-node": "^10.9.1",
|
104
|
-
"typedoc": "^0.23.
|
105
|
-
"typescript": "^4.9.
|
104
|
+
"typedoc": "^0.23.23",
|
105
|
+
"typescript": "^4.9.4",
|
106
106
|
"webpack": "^5.75.0"
|
107
107
|
},
|
108
108
|
"bugs": {
|
@@ -70,6 +70,7 @@ export default abstract class AbstractComponent {
|
|
70
70
|
public getZIndex(): number {
|
71
71
|
return this.zIndex;
|
72
72
|
}
|
73
|
+
|
73
74
|
public getBBox(): Rect2 {
|
74
75
|
return this.contentBBox;
|
75
76
|
}
|
@@ -94,6 +95,13 @@ export default abstract class AbstractComponent {
|
|
94
95
|
return true;
|
95
96
|
}
|
96
97
|
|
98
|
+
// @returns an approximation of the proportional time it takes to render this component.
|
99
|
+
// This is intended to be a rough estimate, but, for example, a stroke with two points sould have
|
100
|
+
// a renderingWeight approximately twice that of a stroke with one point.
|
101
|
+
public getProportionalRenderingTime(): number {
|
102
|
+
return 1;
|
103
|
+
}
|
104
|
+
|
97
105
|
private static transformElementCommandId = 'transform-element';
|
98
106
|
|
99
107
|
private static UnresolvedTransformElementCommand = class extends SerializableCommand {
|
@@ -88,6 +88,11 @@ export default class ImageComponent extends AbstractComponent {
|
|
88
88
|
canvas.drawImage(this.image);
|
89
89
|
}
|
90
90
|
|
91
|
+
public getProportionalRenderingTime(): number {
|
92
|
+
// Estimate: Equivalent to a stroke with 10 segments.
|
93
|
+
return 10;
|
94
|
+
}
|
95
|
+
|
91
96
|
public intersects(lineSegment: LineSegment2): boolean {
|
92
97
|
const rect = this.getImageRect();
|
93
98
|
const edges = rect.getEdges().map(edge => edge.transformedBy(this.image.transform));
|
package/src/components/Stroke.ts
CHANGED
@@ -15,11 +15,16 @@ export default class Stroke extends AbstractComponent {
|
|
15
15
|
private parts: StrokePart[];
|
16
16
|
protected contentBBox: Rect2;
|
17
17
|
|
18
|
+
// See `getProportionalRenderingTime`
|
19
|
+
private approximateRenderingTime: number;
|
20
|
+
|
18
21
|
// Creates a `Stroke` from the given `parts`.
|
19
22
|
public constructor(parts: RenderablePathSpec[]) {
|
20
23
|
super('stroke');
|
21
24
|
|
25
|
+
this.approximateRenderingTime = 0;
|
22
26
|
this.parts = [];
|
27
|
+
|
23
28
|
for (const section of parts) {
|
24
29
|
const path = Path.fromRenderable(section);
|
25
30
|
const pathBBox = this.bboxForPart(path.bbox, section.style);
|
@@ -38,6 +43,8 @@ export default class Stroke extends AbstractComponent {
|
|
38
43
|
style: section.style,
|
39
44
|
commands: path.parts,
|
40
45
|
});
|
46
|
+
|
47
|
+
this.approximateRenderingTime += path.parts.length;
|
41
48
|
}
|
42
49
|
this.contentBBox ??= Rect2.empty;
|
43
50
|
}
|
@@ -71,6 +78,10 @@ export default class Stroke extends AbstractComponent {
|
|
71
78
|
canvas.endObject(this.getLoadSaveData());
|
72
79
|
}
|
73
80
|
|
81
|
+
public getProportionalRenderingTime(): number {
|
82
|
+
return this.approximateRenderingTime;
|
83
|
+
}
|
84
|
+
|
74
85
|
// Grows the bounding box for a given stroke part based on that part's style.
|
75
86
|
private bboxForPart(origBBox: Rect2, style: RenderingStyle) {
|
76
87
|
if (!style.stroke) {
|
@@ -121,6 +121,10 @@ export default class TextComponent extends AbstractComponent {
|
|
121
121
|
canvas.endObject(this.getLoadSaveData());
|
122
122
|
}
|
123
123
|
|
124
|
+
public getProportionalRenderingTime(): number {
|
125
|
+
return this.textObjects.length;
|
126
|
+
}
|
127
|
+
|
124
128
|
public intersects(lineSegment: LineSegment2): boolean {
|
125
129
|
|
126
130
|
// Convert canvas space to internal space.
|
@@ -98,6 +98,8 @@ export class StrokeSmoother {
|
|
98
98
|
this.buffer[this.buffer.length - 2], lastPoint,
|
99
99
|
];
|
100
100
|
this.currentCurve = null;
|
101
|
+
|
102
|
+
this.isFirstSegment = false;
|
101
103
|
}
|
102
104
|
|
103
105
|
// Returns [upper curve, connector, lower curve]
|
@@ -165,25 +167,26 @@ export class StrokeSmoother {
|
|
165
167
|
const prevEndWidth = this.curveEndWidth;
|
166
168
|
this.curveEndWidth = pointRadius;
|
167
169
|
|
168
|
-
if (this.isFirstSegment) {
|
169
|
-
// The start of a curve often lacks accurate pressure information. Update it.
|
170
|
-
this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
|
171
|
-
}
|
172
|
-
|
173
170
|
// recompute bbox
|
174
171
|
this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
|
175
172
|
|
173
|
+
// If the last curve just ended or it's the first curve,
|
176
174
|
if (this.currentCurve === null) {
|
177
175
|
const p1 = lastPoint.pos;
|
178
176
|
const p2 = lastPoint.pos.plus(this.lastExitingVec ?? Vec2.unitX);
|
179
177
|
const p3 = newPoint.pos;
|
180
178
|
|
181
179
|
// Quadratic Bézier curve
|
182
|
-
this.currentCurve = new Bezier(
|
183
|
-
p1.xy, p2.xy, p3.xy
|
184
|
-
);
|
185
|
-
this.curveStartWidth = lastPoint.width;
|
180
|
+
this.currentCurve = new Bezier(p1.xy, p2.xy, p3.xy);
|
186
181
|
console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
|
182
|
+
|
183
|
+
if (this.isFirstSegment) {
|
184
|
+
// The start of a curve often lacks accurate pressure information. Update it.
|
185
|
+
this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
|
186
|
+
}
|
187
|
+
else {
|
188
|
+
this.curveStartWidth = prevEndWidth;
|
189
|
+
}
|
187
190
|
}
|
188
191
|
|
189
192
|
// If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
|
package/src/math/Mat33.ts
CHANGED
@@ -219,6 +219,15 @@ export default class Mat33 {
|
|
219
219
|
);
|
220
220
|
}
|
221
221
|
|
222
|
+
/** @returns true iff this is the identity matrix. */
|
223
|
+
public isIdentity(): boolean {
|
224
|
+
if (this === Mat33.identity) {
|
225
|
+
return true;
|
226
|
+
}
|
227
|
+
|
228
|
+
return this.eq(Mat33.identity);
|
229
|
+
}
|
230
|
+
|
222
231
|
/** Returns true iff this = other ± fuzz */
|
223
232
|
public eq(other: Mat33, fuzz: number = 0): boolean {
|
224
233
|
for (let i = 0; i < 3; i++) {
|
package/src/math/Path.ts
CHANGED
package/src/rendering/Display.ts
CHANGED
@@ -78,8 +78,13 @@ export default class Display {
|
|
78
78
|
blockResolution: cacheBlockResolution,
|
79
79
|
cacheSize: 600 * 600 * 4 * 90,
|
80
80
|
maxScale: 1.4,
|
81
|
-
|
82
|
-
|
81
|
+
|
82
|
+
// Require about 20 strokes with 4 parts each to cache an image in one of the
|
83
|
+
// parts of the cache grid.
|
84
|
+
minProportionalRenderTimePerCache: 20 * 4,
|
85
|
+
|
86
|
+
// Require about 105 strokes with 4 parts each to use the cache at all.
|
87
|
+
minProportionalRenderTimeToUseCache: 105 * 4,
|
83
88
|
});
|
84
89
|
|
85
90
|
this.editor.notifier.on(EditorEventType.DisplayResized, event => {
|