js-draw 0.9.2 → 0.10.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/.firebase/hosting.ZG9jcw.cache +338 -0
- package/.github/ISSUE_TEMPLATE/translation.yml +9 -1
- package/CHANGELOG.md +12 -0
- package/build_tools/buildTranslationTemplate.ts +6 -4
- package/dist/build_tools/buildTranslationTemplate.js +5 -4
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +1 -0
- package/dist/src/Color4.js +34 -15
- package/dist/src/Editor.js +2 -3
- package/dist/src/SVGLoader.js +25 -11
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +2 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +12 -4
- package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -0
- package/dist/src/rendering/renderers/SVGRenderer.js +24 -7
- package/dist/src/toolbar/HTMLToolbar.d.ts +6 -1
- package/dist/src/toolbar/HTMLToolbar.js +24 -27
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -3
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +1 -1
- package/dist/src/toolbar/widgets/BaseWidget.js +9 -4
- package/dist/src/toolbar/widgets/TextToolWidget.js +23 -2
- package/dist/src/tools/PanZoom.d.ts +5 -1
- package/dist/src/tools/PanZoom.js +108 -10
- package/dist/src/tools/SelectionTool/SelectionHandle.js +1 -1
- package/dist/src/tools/TextTool.js +8 -2
- package/dist/src/{language → util}/assertions.d.ts +0 -0
- package/dist/src/{language → util}/assertions.js +1 -0
- package/dist/src/util/untilNextAnimationFrame.d.ts +3 -0
- package/dist/src/util/untilNextAnimationFrame.js +7 -0
- package/package.json +16 -16
- package/src/Color4.test.ts +7 -0
- package/src/Color4.ts +47 -18
- package/src/Editor.toSVG.test.ts +84 -0
- package/src/Editor.ts +2 -3
- package/src/SVGLoader.ts +26 -10
- package/src/components/builders/FreehandLineBuilder.ts +14 -4
- package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +1 -1
- package/src/rendering/renderers/SVGRenderer.ts +24 -6
- package/src/toolbar/HTMLToolbar.ts +33 -30
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/widgets/ActionButtonWidget.ts +2 -2
- package/src/toolbar/widgets/BaseWidget.ts +9 -4
- package/src/toolbar/widgets/TextToolWidget.ts +29 -1
- package/src/tools/PanZoom.ts +124 -7
- package/src/tools/SelectionTool/SelectionHandle.ts +1 -1
- package/src/tools/TextTool.ts +10 -2
- package/src/{language → util}/assertions.ts +1 -0
- package/src/util/untilNextAnimationFrame.ts +9 -0
@@ -102,7 +102,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
102
102
|
|
103
103
|
if (style.stroke) {
|
104
104
|
pathElem.setAttribute('stroke', style.stroke.color.toHexString());
|
105
|
-
pathElem.setAttribute('stroke-width', style.stroke.width
|
105
|
+
pathElem.setAttribute('stroke-width', toRoundedString(style.stroke.width));
|
106
106
|
}
|
107
107
|
|
108
108
|
this.elem.appendChild(pathElem);
|
@@ -150,13 +150,29 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
150
150
|
|
151
151
|
private textContainer: SVGTextElement|null = null;
|
152
152
|
private textContainerTransform: Mat33|null = null;
|
153
|
+
private textParentStyle: TextStyle|null = null;
|
153
154
|
public drawText(text: string, transform: Mat33, style: TextStyle): void {
|
154
155
|
const applyTextStyles = (elem: SVGTextElement|SVGTSpanElement, style: TextStyle) => {
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
156
|
+
if (style.fontFamily !== this.textParentStyle?.fontFamily) {
|
157
|
+
elem.style.fontFamily = style.fontFamily;
|
158
|
+
}
|
159
|
+
if (style.fontVariant !== this.textParentStyle?.fontVariant) {
|
160
|
+
elem.style.fontVariant = style.fontVariant ?? '';
|
161
|
+
}
|
162
|
+
if (style.fontWeight !== this.textParentStyle?.fontWeight) {
|
163
|
+
elem.style.fontWeight = style.fontWeight ?? '';
|
164
|
+
}
|
165
|
+
if (style.size !== this.textParentStyle?.size) {
|
166
|
+
elem.style.fontSize = style.size + 'px';
|
167
|
+
}
|
168
|
+
|
169
|
+
const fillString = style.renderingStyle.fill.toHexString();
|
170
|
+
// TODO: Uncomment at some future major version release --- currently causes incompatibility due
|
171
|
+
// to an SVG parsing bug in older versions.
|
172
|
+
//const parentFillString = this.textParentStyle?.renderingStyle?.fill?.toHexString();
|
173
|
+
//if (fillString !== parentFillString) {
|
174
|
+
elem.style.fill = fillString;
|
175
|
+
//}
|
160
176
|
|
161
177
|
if (style.renderingStyle.stroke) {
|
162
178
|
const strokeStyle = style.renderingStyle.stroke;
|
@@ -181,6 +197,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
181
197
|
if (this.objectLevel > 0) {
|
182
198
|
this.textContainer = container;
|
183
199
|
this.textContainerTransform = transform;
|
200
|
+
this.textParentStyle = style;
|
184
201
|
}
|
185
202
|
} else {
|
186
203
|
const elem = document.createElementNS(svgNameSpace, 'tspan');
|
@@ -218,6 +235,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
218
235
|
this.lastPathString = [];
|
219
236
|
this.lastPathStyle = null;
|
220
237
|
this.textContainer = null;
|
238
|
+
this.textParentStyle = null;
|
221
239
|
this.objectElems = [];
|
222
240
|
}
|
223
241
|
|
@@ -16,6 +16,7 @@ import SelectionToolWidget from './widgets/SelectionToolWidget';
|
|
16
16
|
import TextToolWidget from './widgets/TextToolWidget';
|
17
17
|
import HandToolWidget from './widgets/HandToolWidget';
|
18
18
|
import BaseWidget from './widgets/BaseWidget';
|
19
|
+
import { ActionButtonWidget } from './lib';
|
19
20
|
|
20
21
|
export const toolbarCSSPrefix = 'toolbar-';
|
21
22
|
|
@@ -158,57 +159,59 @@ export default class HTMLToolbar {
|
|
158
159
|
}
|
159
160
|
}
|
160
161
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
iconElem.classList.add('toolbar-icon');
|
175
|
-
|
176
|
-
button.replaceChildren(iconElem, labelElem);
|
177
|
-
}
|
162
|
+
/**
|
163
|
+
* Adds an action button with `title` to this toolbar (or to the given `parent` element).
|
164
|
+
*
|
165
|
+
* @return The added button.
|
166
|
+
*/
|
167
|
+
public addActionButton(title: string|ActionButtonIcon, command: ()=> void): BaseWidget {
|
168
|
+
const titleString = typeof title === 'string' ? title : title.label;
|
169
|
+
const widgetId = 'action-button';
|
170
|
+
|
171
|
+
const makeIcon = () => {
|
172
|
+
if (typeof title === 'string') {
|
173
|
+
return null;
|
174
|
+
}
|
178
175
|
|
179
|
-
|
180
|
-
|
176
|
+
return title.icon;
|
177
|
+
};
|
181
178
|
|
182
|
-
|
179
|
+
const widget = new ActionButtonWidget(
|
180
|
+
this.editor,
|
181
|
+
widgetId,
|
182
|
+
makeIcon,
|
183
|
+
titleString,
|
184
|
+
command,
|
185
|
+
this.editor.localization
|
186
|
+
);
|
187
|
+
|
188
|
+
this.addWidget(widget);
|
189
|
+
return widget;
|
183
190
|
}
|
184
191
|
|
185
192
|
public addUndoRedoButtons() {
|
186
|
-
const undoRedoGroup = document.createElement('div');
|
187
|
-
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
188
|
-
|
189
193
|
const undoButton = this.addActionButton({
|
190
194
|
label: this.localizationTable.undo,
|
191
195
|
icon: this.editor.icons.makeUndoIcon()
|
192
196
|
}, () => {
|
193
197
|
this.editor.history.undo();
|
194
|
-
}
|
198
|
+
});
|
195
199
|
const redoButton = this.addActionButton({
|
196
200
|
label: this.localizationTable.redo,
|
197
201
|
icon: this.editor.icons.makeRedoIcon(),
|
198
202
|
}, () => {
|
199
203
|
this.editor.history.redo();
|
200
|
-
}
|
201
|
-
this.container.appendChild(undoRedoGroup);
|
204
|
+
});
|
202
205
|
|
203
|
-
undoButton.
|
204
|
-
redoButton.
|
206
|
+
undoButton.setDisabled(true);
|
207
|
+
redoButton.setDisabled(true);
|
205
208
|
this.editor.notifier.on(EditorEventType.UndoRedoStackUpdated, event => {
|
206
209
|
if (event.kind !== EditorEventType.UndoRedoStackUpdated) {
|
207
210
|
throw new Error('Wrong event type!');
|
208
211
|
}
|
209
212
|
|
210
|
-
undoButton.
|
211
|
-
redoButton.
|
213
|
+
undoButton.setDisabled(event.undoStackSize === 0);
|
214
|
+
redoButton.setDisabled(event.redoStackSize === 0);
|
212
215
|
});
|
213
216
|
}
|
214
217
|
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
export interface ToolbarLocalization {
|
4
4
|
fontLabel: string;
|
5
|
+
textSize: string;
|
5
6
|
touchPanning: string;
|
6
7
|
lockRotation: string;
|
7
8
|
outlinedRectanglePen: string;
|
@@ -44,6 +45,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
|
|
44
45
|
thicknessLabel: 'Thickness: ',
|
45
46
|
colorLabel: 'Color: ',
|
46
47
|
fontLabel: 'Font: ',
|
48
|
+
textSize: 'Size: ',
|
47
49
|
resizeImageToSelection: 'Resize image to selection',
|
48
50
|
deleteSelection: 'Delete selection',
|
49
51
|
duplicateSelection: 'Duplicate selection',
|
@@ -7,7 +7,7 @@ export default class ActionButtonWidget extends BaseWidget {
|
|
7
7
|
editor: Editor,
|
8
8
|
id: string,
|
9
9
|
|
10
|
-
protected makeIcon: ()=> Element,
|
10
|
+
protected makeIcon: ()=> Element|null,
|
11
11
|
protected title: string,
|
12
12
|
protected clickAction: ()=>void,
|
13
13
|
|
@@ -24,7 +24,7 @@ export default class ActionButtonWidget extends BaseWidget {
|
|
24
24
|
return this.title;
|
25
25
|
}
|
26
26
|
|
27
|
-
protected createIcon(): Element {
|
27
|
+
protected createIcon(): Element|null {
|
28
28
|
return this.makeIcon();
|
29
29
|
}
|
30
30
|
|
@@ -78,7 +78,7 @@ export default abstract class BaseWidget {
|
|
78
78
|
}
|
79
79
|
|
80
80
|
protected abstract getTitle(): string;
|
81
|
-
protected abstract createIcon(): Element;
|
81
|
+
protected abstract createIcon(): Element|null;
|
82
82
|
|
83
83
|
// Add content to the widget's associated dropdown menu.
|
84
84
|
// Returns true if such a menu should be created, false otherwise.
|
@@ -200,9 +200,14 @@ export default abstract class BaseWidget {
|
|
200
200
|
|
201
201
|
protected updateIcon() {
|
202
202
|
const newIcon = this.createIcon();
|
203
|
-
|
204
|
-
|
205
|
-
|
203
|
+
|
204
|
+
if (newIcon) {
|
205
|
+
this.icon?.replaceWith(newIcon);
|
206
|
+
this.icon = newIcon;
|
207
|
+
this.icon.classList.add(`${toolbarCSSPrefix}icon`);
|
208
|
+
} else {
|
209
|
+
this.icon?.remove();
|
210
|
+
}
|
206
211
|
}
|
207
212
|
|
208
213
|
public setDisabled(disabled: boolean) {
|
@@ -34,10 +34,14 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
34
34
|
protected fillDropdown(dropdown: HTMLElement): boolean {
|
35
35
|
const fontRow = document.createElement('div');
|
36
36
|
const colorRow = document.createElement('div');
|
37
|
+
const sizeRow = document.createElement('div');
|
37
38
|
|
38
39
|
const fontInput = document.createElement('select');
|
39
40
|
const fontLabel = document.createElement('label');
|
40
41
|
|
42
|
+
const sizeInput = document.createElement('input');
|
43
|
+
const sizeLabel = document.createElement('label');
|
44
|
+
|
41
45
|
const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
|
42
46
|
this.tool.setColor(color);
|
43
47
|
});
|
@@ -52,12 +56,20 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
52
56
|
fontsInInput.add(fontName);
|
53
57
|
};
|
54
58
|
|
59
|
+
sizeInput.setAttribute('type', 'number');
|
60
|
+
sizeInput.min = '1';
|
61
|
+
sizeInput.max = '128';
|
62
|
+
|
55
63
|
fontLabel.innerText = this.localizationTable.fontLabel;
|
56
64
|
colorLabel.innerText = this.localizationTable.colorLabel;
|
65
|
+
sizeLabel.innerText = this.localizationTable.textSize;
|
57
66
|
|
58
67
|
colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
|
59
68
|
colorLabel.setAttribute('for', colorInput.id);
|
60
69
|
|
70
|
+
sizeInput.id = `${toolbarCSSPrefix}-text-size-input-${TextToolWidget.idCounter++}`;
|
71
|
+
sizeLabel.setAttribute('for', sizeInput.id);
|
72
|
+
|
61
73
|
addFontToInput('monospace');
|
62
74
|
addFontToInput('serif');
|
63
75
|
addFontToInput('sans-serif');
|
@@ -68,12 +80,22 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
68
80
|
this.tool.setFontFamily(fontInput.value);
|
69
81
|
};
|
70
82
|
|
83
|
+
sizeInput.onchange = () => {
|
84
|
+
const size = parseInt(sizeInput.value);
|
85
|
+
if (!isNaN(size) && size > 0) {
|
86
|
+
this.tool.setFontSize(size);
|
87
|
+
}
|
88
|
+
};
|
89
|
+
|
71
90
|
colorRow.appendChild(colorLabel);
|
72
91
|
colorRow.appendChild(colorInputContainer);
|
73
92
|
|
74
93
|
fontRow.appendChild(fontLabel);
|
75
94
|
fontRow.appendChild(fontInput);
|
76
95
|
|
96
|
+
sizeRow.appendChild(sizeLabel);
|
97
|
+
sizeRow.appendChild(sizeInput);
|
98
|
+
|
77
99
|
this.updateDropdownInputs = () => {
|
78
100
|
const style = this.tool.getTextStyle();
|
79
101
|
setColorInputValue(style.renderingStyle.fill);
|
@@ -82,10 +104,11 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
82
104
|
addFontToInput(style.fontFamily);
|
83
105
|
}
|
84
106
|
fontInput.value = style.fontFamily;
|
107
|
+
sizeInput.value = `${style.size}`;
|
85
108
|
};
|
86
109
|
this.updateDropdownInputs();
|
87
110
|
|
88
|
-
dropdown.replaceChildren(colorRow, fontRow);
|
111
|
+
dropdown.replaceChildren(colorRow, sizeRow, fontRow);
|
89
112
|
return true;
|
90
113
|
}
|
91
114
|
|
@@ -96,6 +119,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
96
119
|
...super.serializeState(),
|
97
120
|
|
98
121
|
fontFamily: textStyle.fontFamily,
|
122
|
+
textSize: textStyle.size,
|
99
123
|
color: textStyle.renderingStyle.fill.toHexString(),
|
100
124
|
};
|
101
125
|
}
|
@@ -109,6 +133,10 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
109
133
|
this.tool.setColor(Color4.fromHex(state.color));
|
110
134
|
}
|
111
135
|
|
136
|
+
if (state.textSize && typeof(state.textSize) === 'number') {
|
137
|
+
this.tool.setFontSize(state.textSize);
|
138
|
+
}
|
139
|
+
|
112
140
|
super.deserializeFrom(state);
|
113
141
|
}
|
114
142
|
}
|
package/src/tools/PanZoom.ts
CHANGED
@@ -5,6 +5,7 @@ import { Point2, Vec2 } from '../math/Vec2';
|
|
5
5
|
import Vec3 from '../math/Vec3';
|
6
6
|
import Pointer, { PointerDevice } from '../Pointer';
|
7
7
|
import { EditorEventType, KeyPressEvent, PointerEvt, WheelEvt } from '../types';
|
8
|
+
import untilNextAnimationFrame from '../util/untilNextAnimationFrame';
|
8
9
|
import { Viewport, ViewportTransform } from '../Viewport';
|
9
10
|
import BaseTool from './BaseTool';
|
10
11
|
|
@@ -25,12 +26,68 @@ export enum PanZoomMode {
|
|
25
26
|
RotationLocked = 0x1 << 5,
|
26
27
|
}
|
27
28
|
|
29
|
+
type ScrollByCallback = (delta: Vec2) => void;
|
30
|
+
|
31
|
+
class InertialScroller {
|
32
|
+
private running: boolean = false;
|
33
|
+
|
34
|
+
public constructor(
|
35
|
+
private initialVelocity: Vec2,
|
36
|
+
private scrollBy: ScrollByCallback,
|
37
|
+
private onComplete: ()=> void
|
38
|
+
) {
|
39
|
+
this.start();
|
40
|
+
}
|
41
|
+
|
42
|
+
private async start() {
|
43
|
+
if (this.running) {
|
44
|
+
return;
|
45
|
+
}
|
46
|
+
|
47
|
+
let currentVelocity = this.initialVelocity;
|
48
|
+
let lastTime = (new Date()).getTime();
|
49
|
+
this.running = true;
|
50
|
+
|
51
|
+
const maxSpeed = 8000; // units/s
|
52
|
+
const minSpeed = 200; // units/s
|
53
|
+
if (currentVelocity.magnitude() > maxSpeed) {
|
54
|
+
currentVelocity = currentVelocity.normalized().times(maxSpeed);
|
55
|
+
}
|
56
|
+
|
57
|
+
while (this.running && currentVelocity.magnitude() > minSpeed) {
|
58
|
+
const nowTime = (new Date()).getTime();
|
59
|
+
const dt = (nowTime - lastTime) / 1000;
|
60
|
+
|
61
|
+
currentVelocity = currentVelocity.times(Math.pow(1/8, dt));
|
62
|
+
this.scrollBy(currentVelocity.times(dt));
|
63
|
+
|
64
|
+
await untilNextAnimationFrame();
|
65
|
+
lastTime = nowTime;
|
66
|
+
}
|
67
|
+
|
68
|
+
if (this.running) {
|
69
|
+
this.stop();
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
public stop(): void {
|
74
|
+
if (this.running) {
|
75
|
+
this.running = false;
|
76
|
+
this.onComplete();
|
77
|
+
}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
28
81
|
export default class PanZoom extends BaseTool {
|
29
82
|
private transform: ViewportTransform|null = null;
|
30
83
|
|
31
84
|
private lastAngle: number;
|
32
85
|
private lastDist: number;
|
33
86
|
private lastScreenCenter: Point2;
|
87
|
+
private lastTimestamp: number;
|
88
|
+
|
89
|
+
private inertialScroller: InertialScroller|null = null;
|
90
|
+
private velocity: Vec2|null = null;
|
34
91
|
|
35
92
|
public constructor(private editor: Editor, private mode: PanZoomMode, description: string) {
|
36
93
|
super(editor.notifier, description);
|
@@ -54,6 +111,8 @@ export default class PanZoom extends BaseTool {
|
|
54
111
|
public onPointerDown({ allPointers: pointers }: PointerEvt): boolean {
|
55
112
|
let handlingGesture = false;
|
56
113
|
|
114
|
+
this.inertialScroller?.stop();
|
115
|
+
|
57
116
|
const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
|
58
117
|
const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
|
59
118
|
|
@@ -73,6 +132,7 @@ export default class PanZoom extends BaseTool {
|
|
73
132
|
}
|
74
133
|
|
75
134
|
if (handlingGesture) {
|
135
|
+
this.lastTimestamp = (new Date()).getTime();
|
76
136
|
this.transform ??= Viewport.transformBy(Mat33.identity);
|
77
137
|
this.editor.display.setDraftMode(true);
|
78
138
|
}
|
@@ -80,6 +140,23 @@ export default class PanZoom extends BaseTool {
|
|
80
140
|
return handlingGesture;
|
81
141
|
}
|
82
142
|
|
143
|
+
private updateVelocity(currentCenter: Point2) {
|
144
|
+
const deltaPos = currentCenter.minus(this.lastScreenCenter);
|
145
|
+
const deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
|
146
|
+
const currentVelocity = deltaPos.times(1 / deltaTime);
|
147
|
+
let smoothedVelocity = currentVelocity;
|
148
|
+
|
149
|
+
if (deltaTime === 0) {
|
150
|
+
return;
|
151
|
+
}
|
152
|
+
|
153
|
+
if (this.velocity) {
|
154
|
+
smoothedVelocity = this.velocity.lerp(smoothedVelocity, 0.5);
|
155
|
+
}
|
156
|
+
|
157
|
+
this.velocity = smoothedVelocity;
|
158
|
+
}
|
159
|
+
|
83
160
|
// Returns the change in position of the center of the given group of pointers.
|
84
161
|
// Assumes this.lastScreenCenter has been set appropriately.
|
85
162
|
private getCenterDelta(screenCenter: Point2): Vec2 {
|
@@ -98,6 +175,8 @@ export default class PanZoom extends BaseTool {
|
|
98
175
|
rotation = 0;
|
99
176
|
}
|
100
177
|
|
178
|
+
this.updateVelocity(screenCenter);
|
179
|
+
|
101
180
|
const transformUpdate = Mat33.translation(delta)
|
102
181
|
.rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
|
103
182
|
.rightMul(Mat33.zRotation(rotation, canvasCenter));
|
@@ -116,6 +195,7 @@ export default class PanZoom extends BaseTool {
|
|
116
195
|
Mat33.translation(delta)
|
117
196
|
)
|
118
197
|
);
|
198
|
+
this.updateVelocity(pointer.screenPos);
|
119
199
|
this.lastScreenCenter = pointer.screenPos;
|
120
200
|
}
|
121
201
|
|
@@ -130,19 +210,52 @@ export default class PanZoom extends BaseTool {
|
|
130
210
|
}
|
131
211
|
lastTransform.unapply(this.editor);
|
132
212
|
this.transform.apply(this.editor);
|
213
|
+
|
214
|
+
this.lastTimestamp = (new Date()).getTime();
|
133
215
|
}
|
134
216
|
|
135
|
-
public onPointerUp(
|
136
|
-
|
137
|
-
this.transform
|
138
|
-
|
217
|
+
public onPointerUp(event: PointerEvt): void {
|
218
|
+
const onComplete = () => {
|
219
|
+
if (this.transform) {
|
220
|
+
this.transform.unapply(this.editor);
|
221
|
+
this.editor.dispatch(this.transform, false);
|
222
|
+
}
|
223
|
+
|
224
|
+
this.editor.display.setDraftMode(false);
|
225
|
+
this.transform = null;
|
226
|
+
this.velocity = Vec2.zero;
|
227
|
+
};
|
228
|
+
|
229
|
+
const shouldInertialScroll =
|
230
|
+
event.current.device === PointerDevice.Touch && event.allPointers.length === 1;
|
231
|
+
|
232
|
+
if (shouldInertialScroll && this.velocity !== null) {
|
233
|
+
this.inertialScroller?.stop();
|
234
|
+
|
235
|
+
this.inertialScroller = new InertialScroller(this.velocity, (scrollDelta: Vec2) => {
|
236
|
+
if (!this.transform) {
|
237
|
+
return;
|
238
|
+
}
|
239
|
+
|
240
|
+
const canvasDelta = this.editor.viewport.screenToCanvasTransform.transformVec3(scrollDelta);
|
241
|
+
|
242
|
+
// Scroll by scrollDelta
|
243
|
+
this.transform.unapply(this.editor);
|
244
|
+
this.transform = Viewport.transformBy(
|
245
|
+
this.transform.transform.rightMul(
|
246
|
+
Mat33.translation(canvasDelta)
|
247
|
+
)
|
248
|
+
);
|
249
|
+
this.transform.apply(this.editor);
|
250
|
+
}, onComplete);
|
251
|
+
} else {
|
252
|
+
onComplete();
|
139
253
|
}
|
140
|
-
|
141
|
-
this.editor.display.setDraftMode(false);
|
142
|
-
this.transform = null;
|
143
254
|
}
|
144
255
|
|
145
256
|
public onGestureCancel(): void {
|
257
|
+
this.inertialScroller?.stop();
|
258
|
+
this.velocity = Vec2.zero;
|
146
259
|
this.transform?.unapply(this.editor);
|
147
260
|
this.editor.display.setDraftMode(false);
|
148
261
|
this.transform = null;
|
@@ -166,6 +279,8 @@ export default class PanZoom extends BaseTool {
|
|
166
279
|
}
|
167
280
|
|
168
281
|
public onWheel({ delta, screenPos }: WheelEvt): boolean {
|
282
|
+
this.inertialScroller?.stop();
|
283
|
+
|
169
284
|
// Reset the transformation -- wheel events are individual events, so we don't
|
170
285
|
// need to unapply/reapply.
|
171
286
|
this.transform = Viewport.transformBy(Mat33.identity);
|
@@ -190,6 +305,8 @@ export default class PanZoom extends BaseTool {
|
|
190
305
|
}
|
191
306
|
|
192
307
|
public onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean {
|
308
|
+
this.inertialScroller?.stop();
|
309
|
+
|
193
310
|
if (!(this.mode & PanZoomMode.Keyboard)) {
|
194
311
|
return false;
|
195
312
|
}
|
package/src/tools/TextTool.ts
CHANGED
@@ -135,8 +135,11 @@ export default class TextTool extends BaseTool {
|
|
135
135
|
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
|
136
136
|
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
|
137
137
|
|
138
|
+
// Get the ascent based on the font, using a character that is tall in most fonts.
|
139
|
+
const tallCharacter = '⎢';
|
140
|
+
const ascent = this.getTextAscent(tallCharacter, this.textStyle);
|
141
|
+
|
138
142
|
const rotation = this.textRotation + viewport.getRotationAngle();
|
139
|
-
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
|
140
143
|
const scale: Mat33 = this.getTextScaleMatrix();
|
141
144
|
this.textInputElem.style.transform = `${scale.toCSSMatrix()} rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
|
142
145
|
this.textInputElem.style.transformOrigin = 'top left';
|
@@ -208,7 +211,12 @@ export default class TextTool extends BaseTool {
|
|
208
211
|
const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
|
209
212
|
const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
|
210
213
|
const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
|
211
|
-
|
214
|
+
let targetTextNodes = targetNodes.filter(node => node instanceof TextComponent) as TextComponent[];
|
215
|
+
|
216
|
+
// Don't try to edit text nodes that contain the viewport (this allows us
|
217
|
+
// to zoom in on text nodes and add text on top of them.)
|
218
|
+
const visibleRect = this.editor.viewport.visibleRect;
|
219
|
+
targetTextNodes = targetTextNodes.filter(node => !node.getBBox().containsRect(visibleRect));
|
212
220
|
|
213
221
|
// End any TextNodes we're currently editing.
|
214
222
|
this.flushInput();
|