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.
Files changed (50) hide show
  1. package/.firebase/hosting.ZG9jcw.cache +338 -0
  2. package/.github/ISSUE_TEMPLATE/translation.yml +9 -1
  3. package/CHANGELOG.md +12 -0
  4. package/build_tools/buildTranslationTemplate.ts +6 -4
  5. package/dist/build_tools/buildTranslationTemplate.js +5 -4
  6. package/dist/bundle.js +1 -1
  7. package/dist/src/Color4.d.ts +1 -0
  8. package/dist/src/Color4.js +34 -15
  9. package/dist/src/Editor.js +2 -3
  10. package/dist/src/SVGLoader.js +25 -11
  11. package/dist/src/components/builders/FreehandLineBuilder.d.ts +2 -0
  12. package/dist/src/components/builders/FreehandLineBuilder.js +12 -4
  13. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
  14. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -0
  15. package/dist/src/rendering/renderers/SVGRenderer.js +24 -7
  16. package/dist/src/toolbar/HTMLToolbar.d.ts +6 -1
  17. package/dist/src/toolbar/HTMLToolbar.js +24 -27
  18. package/dist/src/toolbar/localization.d.ts +1 -0
  19. package/dist/src/toolbar/localization.js +1 -0
  20. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -3
  21. package/dist/src/toolbar/widgets/BaseWidget.d.ts +1 -1
  22. package/dist/src/toolbar/widgets/BaseWidget.js +9 -4
  23. package/dist/src/toolbar/widgets/TextToolWidget.js +23 -2
  24. package/dist/src/tools/PanZoom.d.ts +5 -1
  25. package/dist/src/tools/PanZoom.js +108 -10
  26. package/dist/src/tools/SelectionTool/SelectionHandle.js +1 -1
  27. package/dist/src/tools/TextTool.js +8 -2
  28. package/dist/src/{language → util}/assertions.d.ts +0 -0
  29. package/dist/src/{language → util}/assertions.js +1 -0
  30. package/dist/src/util/untilNextAnimationFrame.d.ts +3 -0
  31. package/dist/src/util/untilNextAnimationFrame.js +7 -0
  32. package/package.json +16 -16
  33. package/src/Color4.test.ts +7 -0
  34. package/src/Color4.ts +47 -18
  35. package/src/Editor.toSVG.test.ts +84 -0
  36. package/src/Editor.ts +2 -3
  37. package/src/SVGLoader.ts +26 -10
  38. package/src/components/builders/FreehandLineBuilder.ts +14 -4
  39. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +1 -1
  40. package/src/rendering/renderers/SVGRenderer.ts +24 -6
  41. package/src/toolbar/HTMLToolbar.ts +33 -30
  42. package/src/toolbar/localization.ts +2 -0
  43. package/src/toolbar/widgets/ActionButtonWidget.ts +2 -2
  44. package/src/toolbar/widgets/BaseWidget.ts +9 -4
  45. package/src/toolbar/widgets/TextToolWidget.ts +29 -1
  46. package/src/tools/PanZoom.ts +124 -7
  47. package/src/tools/SelectionTool/SelectionHandle.ts +1 -1
  48. package/src/tools/TextTool.ts +10 -2
  49. package/src/{language → util}/assertions.ts +1 -0
  50. 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.toString());
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
- elem.style.fontFamily = style.fontFamily;
156
- elem.style.fontVariant = style.fontVariant ?? '';
157
- elem.style.fontWeight = style.fontWeight ?? '';
158
- elem.style.fontSize = style.size + 'px';
159
- elem.style.fill = style.renderingStyle.fill.toHexString();
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
- public addActionButton(title: string|ActionButtonIcon, command: ()=> void, parent?: Element) {
162
- const button = document.createElement('button');
163
- button.classList.add(`${toolbarCSSPrefix}button`);
164
-
165
- if (typeof title === 'string') {
166
- button.innerText = title;
167
- } else {
168
- const iconElem = title.icon.cloneNode(true) as HTMLElement;
169
- const labelElem = document.createElement('label');
170
-
171
- // Use the label to describe the icon -- no additional description should be necessary.
172
- iconElem.setAttribute('alt', '');
173
- labelElem.innerText = title.label;
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
- button.onclick = command;
180
- (parent ?? this.container).appendChild(button);
176
+ return title.icon;
177
+ };
181
178
 
182
- return button;
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
- }, undoRedoGroup);
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
- }, undoRedoGroup);
201
- this.container.appendChild(undoRedoGroup);
204
+ });
202
205
 
203
- undoButton.disabled = true;
204
- redoButton.disabled = true;
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.disabled = event.undoStackSize === 0;
211
- redoButton.disabled = event.redoStackSize === 0;
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
- this.icon?.replaceWith(newIcon);
204
- this.icon = newIcon;
205
- this.icon.classList.add(`${toolbarCSSPrefix}icon`);
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
  }
@@ -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(_event: PointerEvt): void {
136
- if (this.transform) {
137
- this.transform.unapply(this.editor);
138
- this.editor.dispatch(this.transform, false);
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
  }
@@ -1,4 +1,4 @@
1
- import { assertUnreachable } from '../../language/assertions';
1
+ import { assertUnreachable } from '../../util/assertions';
2
2
  import { Point2, Vec2 } from '../../math/Vec2';
3
3
  import { cssPrefix } from './SelectionTool';
4
4
  import Selection from './Selection';
@@ -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
- const targetTextNodes = targetNodes.filter(node => node instanceof TextComponent) as TextComponent[];
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();
@@ -1,6 +1,7 @@
1
1
 
2
2
  // Compile-time assertion that a branch of code is unreachable.
3
3
  // See https://stackoverflow.com/a/39419171/17055750
4
+ // @internal
4
5
  export const assertUnreachable = (key: never): never => {
5
6
  throw new Error(`Should be unreachable. Key: ${key}.`);
6
7
  };
@@ -0,0 +1,9 @@
1
+
2
+ /** @internal */
3
+ const untilNextAnimationFrame = (): Promise<void> => {
4
+ return new Promise((resolve) => {
5
+ requestAnimationFrame(() => resolve());
6
+ });
7
+ };
8
+
9
+ export default untilNextAnimationFrame;