js-draw 1.0.1 → 1.0.2

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/cjs/version.js +1 -1
  4. package/dist/mjs/version.mjs +1 -1
  5. package/docs/img/readme-images/js-draw.jpg +0 -0
  6. package/docs/img/readme-images/unsupported-elements--in-editor.png +0 -0
  7. package/package.json +5 -4
  8. package/dist-test/test_imports/package-lock.json +0 -13
  9. package/dist-test/test_imports/package.json +0 -12
  10. package/dist-test/test_imports/test-imports.js +0 -11
  11. package/dist-test/test_imports/test-require.cjs +0 -14
  12. package/src/Editor.loadFrom.test.ts +0 -24
  13. package/src/Editor.test.ts +0 -107
  14. package/src/Editor.toSVG.test.ts +0 -294
  15. package/src/Editor.ts +0 -1443
  16. package/src/EditorImage.test.ts +0 -117
  17. package/src/EditorImage.ts +0 -609
  18. package/src/EventDispatcher.test.ts +0 -123
  19. package/src/EventDispatcher.ts +0 -72
  20. package/src/Pointer.ts +0 -183
  21. package/src/SVGLoader.test.ts +0 -114
  22. package/src/SVGLoader.ts +0 -672
  23. package/src/UndoRedoHistory.test.ts +0 -34
  24. package/src/UndoRedoHistory.ts +0 -102
  25. package/src/Viewport.ts +0 -322
  26. package/src/bundle/bundled.ts +0 -7
  27. package/src/commands/Command.ts +0 -45
  28. package/src/commands/Duplicate.ts +0 -75
  29. package/src/commands/Erase.ts +0 -95
  30. package/src/commands/SerializableCommand.ts +0 -49
  31. package/src/commands/UnresolvedCommand.ts +0 -37
  32. package/src/commands/invertCommand.ts +0 -58
  33. package/src/commands/lib.ts +0 -16
  34. package/src/commands/localization.ts +0 -47
  35. package/src/commands/uniteCommands.test.ts +0 -23
  36. package/src/commands/uniteCommands.ts +0 -140
  37. package/src/components/AbstractComponent.transformBy.test.ts +0 -23
  38. package/src/components/AbstractComponent.ts +0 -383
  39. package/src/components/BackgroundComponent.test.ts +0 -44
  40. package/src/components/BackgroundComponent.ts +0 -348
  41. package/src/components/ImageComponent.ts +0 -176
  42. package/src/components/RestylableComponent.ts +0 -161
  43. package/src/components/SVGGlobalAttributesObject.ts +0 -79
  44. package/src/components/Stroke.test.ts +0 -137
  45. package/src/components/Stroke.ts +0 -294
  46. package/src/components/TextComponent.test.ts +0 -202
  47. package/src/components/TextComponent.ts +0 -429
  48. package/src/components/UnknownSVGObject.test.ts +0 -10
  49. package/src/components/UnknownSVGObject.ts +0 -60
  50. package/src/components/builders/ArrowBuilder.ts +0 -106
  51. package/src/components/builders/CircleBuilder.ts +0 -100
  52. package/src/components/builders/FreehandLineBuilder.test.ts +0 -24
  53. package/src/components/builders/FreehandLineBuilder.ts +0 -210
  54. package/src/components/builders/LineBuilder.ts +0 -77
  55. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +0 -453
  56. package/src/components/builders/RectangleBuilder.ts +0 -73
  57. package/src/components/builders/types.ts +0 -15
  58. package/src/components/lib.ts +0 -31
  59. package/src/components/localization.ts +0 -24
  60. package/src/components/util/StrokeSmoother.ts +0 -302
  61. package/src/components/util/describeComponentList.ts +0 -18
  62. package/src/dialogs/makeAboutDialog.ts +0 -82
  63. package/src/inputEvents.ts +0 -143
  64. package/src/lib.ts +0 -91
  65. package/src/localization.ts +0 -34
  66. package/src/localizations/de.ts +0 -146
  67. package/src/localizations/en.ts +0 -8
  68. package/src/localizations/es.ts +0 -74
  69. package/src/localizations/getLocalizationTable.test.ts +0 -27
  70. package/src/localizations/getLocalizationTable.ts +0 -74
  71. package/src/rendering/Display.ts +0 -247
  72. package/src/rendering/RenderablePathSpec.ts +0 -88
  73. package/src/rendering/RenderingStyle.test.ts +0 -68
  74. package/src/rendering/RenderingStyle.ts +0 -55
  75. package/src/rendering/TextRenderingStyle.ts +0 -55
  76. package/src/rendering/caching/CacheRecord.test.ts +0 -48
  77. package/src/rendering/caching/CacheRecord.ts +0 -76
  78. package/src/rendering/caching/CacheRecordManager.ts +0 -71
  79. package/src/rendering/caching/RenderingCache.test.ts +0 -43
  80. package/src/rendering/caching/RenderingCache.ts +0 -66
  81. package/src/rendering/caching/RenderingCacheNode.ts +0 -404
  82. package/src/rendering/caching/testUtils.ts +0 -35
  83. package/src/rendering/caching/types.ts +0 -34
  84. package/src/rendering/lib.ts +0 -8
  85. package/src/rendering/localization.ts +0 -20
  86. package/src/rendering/renderers/AbstractRenderer.ts +0 -232
  87. package/src/rendering/renderers/CanvasRenderer.ts +0 -312
  88. package/src/rendering/renderers/DummyRenderer.test.ts +0 -41
  89. package/src/rendering/renderers/DummyRenderer.ts +0 -142
  90. package/src/rendering/renderers/SVGRenderer.ts +0 -434
  91. package/src/rendering/renderers/TextOnlyRenderer.test.ts +0 -34
  92. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -68
  93. package/src/shortcuts/KeyBinding.test.ts +0 -61
  94. package/src/shortcuts/KeyBinding.ts +0 -257
  95. package/src/shortcuts/KeyboardShortcutManager.test.ts +0 -95
  96. package/src/shortcuts/KeyboardShortcutManager.ts +0 -163
  97. package/src/shortcuts/lib.ts +0 -3
  98. package/src/testing/createEditor.ts +0 -11
  99. package/src/testing/getUniquePointerId.ts +0 -18
  100. package/src/testing/lib.ts +0 -3
  101. package/src/testing/sendPenEvent.ts +0 -36
  102. package/src/testing/sendTouchEvent.ts +0 -71
  103. package/src/toolbar/AbstractToolbar.ts +0 -542
  104. package/src/toolbar/DropdownToolbar.ts +0 -220
  105. package/src/toolbar/EdgeToolbar.test.ts +0 -54
  106. package/src/toolbar/EdgeToolbar.ts +0 -543
  107. package/src/toolbar/IconProvider.ts +0 -861
  108. package/src/toolbar/constants.ts +0 -1
  109. package/src/toolbar/lib.ts +0 -6
  110. package/src/toolbar/localization.ts +0 -136
  111. package/src/toolbar/types.ts +0 -13
  112. package/src/toolbar/widgets/ActionButtonWidget.ts +0 -39
  113. package/src/toolbar/widgets/BaseToolWidget.ts +0 -81
  114. package/src/toolbar/widgets/BaseWidget.ts +0 -495
  115. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +0 -250
  116. package/src/toolbar/widgets/EraserToolWidget.ts +0 -84
  117. package/src/toolbar/widgets/HandToolWidget.ts +0 -239
  118. package/src/toolbar/widgets/InsertImageWidget.ts +0 -248
  119. package/src/toolbar/widgets/OverflowWidget.ts +0 -92
  120. package/src/toolbar/widgets/PenToolWidget.ts +0 -369
  121. package/src/toolbar/widgets/SelectionToolWidget.ts +0 -195
  122. package/src/toolbar/widgets/TextToolWidget.ts +0 -149
  123. package/src/toolbar/widgets/components/makeColorInput.ts +0 -184
  124. package/src/toolbar/widgets/components/makeFileInput.ts +0 -128
  125. package/src/toolbar/widgets/components/makeGridSelector.ts +0 -179
  126. package/src/toolbar/widgets/components/makeSeparator.ts +0 -17
  127. package/src/toolbar/widgets/components/makeThicknessSlider.ts +0 -62
  128. package/src/toolbar/widgets/keybindings.ts +0 -19
  129. package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +0 -262
  130. package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +0 -71
  131. package/src/toolbar/widgets/layout/types.ts +0 -74
  132. package/src/toolbar/widgets/lib.ts +0 -13
  133. package/src/tools/BaseTool.ts +0 -169
  134. package/src/tools/Eraser.test.ts +0 -103
  135. package/src/tools/Eraser.ts +0 -173
  136. package/src/tools/FindTool.test.ts +0 -67
  137. package/src/tools/FindTool.ts +0 -153
  138. package/src/tools/InputFilter/FunctionMapper.ts +0 -17
  139. package/src/tools/InputFilter/InputMapper.ts +0 -41
  140. package/src/tools/InputFilter/InputPipeline.test.ts +0 -41
  141. package/src/tools/InputFilter/InputPipeline.ts +0 -34
  142. package/src/tools/InputFilter/InputStabilizer.ts +0 -254
  143. package/src/tools/InputFilter/StrokeKeyboardControl.ts +0 -104
  144. package/src/tools/PanZoom.test.ts +0 -339
  145. package/src/tools/PanZoom.ts +0 -525
  146. package/src/tools/PasteHandler.ts +0 -94
  147. package/src/tools/Pen.test.ts +0 -260
  148. package/src/tools/Pen.ts +0 -284
  149. package/src/tools/PipetteTool.ts +0 -84
  150. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +0 -29
  151. package/src/tools/SelectionTool/Selection.ts +0 -647
  152. package/src/tools/SelectionTool/SelectionHandle.ts +0 -142
  153. package/src/tools/SelectionTool/SelectionTool.test.ts +0 -370
  154. package/src/tools/SelectionTool/SelectionTool.ts +0 -510
  155. package/src/tools/SelectionTool/TransformMode.ts +0 -112
  156. package/src/tools/SelectionTool/types.ts +0 -11
  157. package/src/tools/SoundUITool.ts +0 -221
  158. package/src/tools/TextTool.ts +0 -339
  159. package/src/tools/ToolController.ts +0 -224
  160. package/src/tools/ToolEnabledGroup.ts +0 -14
  161. package/src/tools/ToolSwitcherShortcut.ts +0 -39
  162. package/src/tools/ToolbarShortcutHandler.ts +0 -39
  163. package/src/tools/UndoRedoShortcut.test.ts +0 -62
  164. package/src/tools/UndoRedoShortcut.ts +0 -24
  165. package/src/tools/keybindings.ts +0 -85
  166. package/src/tools/lib.ts +0 -22
  167. package/src/tools/localization.ts +0 -76
  168. package/src/types.ts +0 -151
  169. package/src/util/ReactiveValue.test.ts +0 -168
  170. package/src/util/ReactiveValue.ts +0 -241
  171. package/src/util/assertions.ts +0 -55
  172. package/src/util/fileToBase64.ts +0 -18
  173. package/src/util/guessKeyCodeFromKey.ts +0 -36
  174. package/src/util/listPrefixMatch.ts +0 -19
  175. package/src/util/stopPropagationOfScrollingWheelEvents.ts +0 -20
  176. package/src/util/untilNextAnimationFrame.ts +0 -9
  177. package/src/util/waitForAll.ts +0 -18
  178. package/src/util/waitForTimeout.ts +0 -9
  179. package/src/version.test.ts +0 -12
  180. package/src/version.ts +0 -3
  181. package/tools/allLocales.js +0 -4
  182. package/tools/copyREADME.ts +0 -62
@@ -1,221 +0,0 @@
1
- import Editor from '../Editor';
2
- import { LineSegment2, Color4, Point2 } from '@js-draw/math';
3
- import { KeyPressEvent, PointerEvt } from '../inputEvents';
4
- import BaseTool from './BaseTool';
5
-
6
- class SoundFeedback {
7
- private ctx: AudioContext;
8
-
9
- // Feedback for the current color under the cursor
10
- private colorOscHue: OscillatorNode;
11
- private colorOscValue: OscillatorNode;
12
- private colorOscSaturation: OscillatorNode;
13
- private valueGain: GainNode;
14
- private colorGain: GainNode;
15
-
16
- // Feedback for when the cursor crosses the boundary of some
17
- // component of the image.
18
- private boundaryOsc: OscillatorNode;
19
- private boundaryGain: GainNode;
20
-
21
- private closed: boolean = false;
22
-
23
- public constructor() {
24
- // No AudioContext? Exit!
25
- if (!window.AudioContext) {
26
- console.warn('Accessibility sound UI: Unable to open AudioContext.');
27
- this.closed = true;
28
- return;
29
- }
30
-
31
- this.ctx = new AudioContext();
32
-
33
- // Color oscillator and gain
34
- this.colorOscHue = this.ctx.createOscillator();
35
- this.colorOscValue = this.ctx.createOscillator();
36
- this.colorOscSaturation = this.ctx.createOscillator();
37
- this.colorOscHue.type = 'triangle';
38
- this.colorOscSaturation.type = 'sine';
39
- this.colorOscValue.type = 'sawtooth';
40
-
41
- this.valueGain = this.ctx.createGain();
42
- this.colorOscValue.connect(this.valueGain);
43
- this.valueGain.gain.setValueAtTime(0.18, this.ctx.currentTime);
44
-
45
- this.colorGain = this.ctx.createGain();
46
-
47
- this.colorOscHue.connect(this.colorGain);
48
- this.valueGain.connect(this.colorGain);
49
- this.colorOscSaturation.connect(this.colorGain);
50
- this.colorGain.connect(this.ctx.destination);
51
-
52
- // Boundary oscillator and gain
53
- this.boundaryGain = this.ctx.createGain();
54
- this.boundaryOsc = this.ctx.createOscillator();
55
- this.boundaryOsc.type = 'sawtooth';
56
- this.boundaryGain.gain.setValueAtTime(0, this.ctx.currentTime);
57
-
58
- this.boundaryOsc.connect(this.boundaryGain);
59
- this.boundaryGain.connect(this.ctx.destination);
60
-
61
- // Prepare for the first announcement/feedback.
62
- this.colorOscHue.start();
63
- this.colorOscSaturation.start();
64
- this.colorOscValue.start();
65
- this.boundaryOsc.start();
66
- this.pause();
67
- }
68
-
69
- public pause() {
70
- if (this.closed) return;
71
- this.colorGain.gain.setValueAtTime(0, this.ctx.currentTime);
72
- void this.ctx.suspend();
73
- }
74
-
75
- public play() {
76
- if (this.closed) return;
77
- void this.ctx.resume();
78
- }
79
-
80
- public setColor(color: Color4) {
81
- const hsv = color.asHSV();
82
-
83
- // Choose frequencies that roughly correspond to hue, saturation, and value.
84
- const hueFrequency = (-Math.cos(hsv.x / 2)) * 220 + 440;
85
- const saturationFrequency = hsv.y * 440 + 220;
86
- const valueFrequency = (hsv.z + 0.1) * 440;
87
-
88
- // Sigmoid with maximum 0.25 * alpha.
89
- // Louder for greater value.
90
- const gain = 0.25 * Math.min(1, color.a) / (1 + Math.exp(-(hsv.z - 0.5) * 3));
91
-
92
- this.colorOscHue.frequency.setValueAtTime(hueFrequency, this.ctx.currentTime);
93
- this.colorOscSaturation.frequency.setValueAtTime(saturationFrequency, this.ctx.currentTime);
94
- this.colorOscValue.frequency.setValueAtTime(valueFrequency, this.ctx.currentTime);
95
- this.valueGain.gain.setValueAtTime((1 - hsv.z) * 0.4, this.ctx.currentTime);
96
- this.colorGain.gain.setValueAtTime(gain, this.ctx.currentTime);
97
- }
98
-
99
- public announceBoundaryCross(boundaryCrossCount: number) {
100
- this.boundaryGain.gain.cancelScheduledValues(this.ctx.currentTime);
101
- this.boundaryGain.gain.setValueAtTime(0, this.ctx.currentTime);
102
- this.boundaryGain.gain.linearRampToValueAtTime(0.018, this.ctx.currentTime + 0.1);
103
- this.boundaryOsc.frequency.setValueAtTime(440 + Math.atan(boundaryCrossCount / 2) * 100, this.ctx.currentTime);
104
- this.boundaryGain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.25);
105
- }
106
-
107
- public close() {
108
- this.ctx.close();
109
- this.closed = true;
110
- }
111
- }
112
-
113
- /**
114
- * This tool, when enabled, plays a sound representing the color of the portion of the display
115
- * currently under the cursor. This tool adds a button that can be navigated to with the tab key
116
- * that enables/disables the tool.
117
- *
118
- * This allows the user to explore the content of the display without a working screen.
119
- */
120
- export default class SoundUITool extends BaseTool {
121
- private soundFeedback: SoundFeedback|null = null;
122
- private toggleButton: HTMLElement;
123
- private toggleButtonContainer: HTMLElement;
124
-
125
- public constructor(
126
- private editor: Editor,
127
- description: string,
128
- ) {
129
- super(editor.notifier, description);
130
-
131
-
132
- // Create a screen-reader-usable method of toggling the tool:
133
- this.toggleButtonContainer = document.createElement('div');
134
- this.toggleButtonContainer.classList.add('js-draw-sound-ui-toggle');
135
-
136
- this.toggleButton = document.createElement('button');
137
- this.toggleButton.onclick = () => {
138
- this.setEnabled(!this.isEnabled());
139
- };
140
- this.toggleButtonContainer.appendChild(this.toggleButton);
141
- this.updateToggleButtonText();
142
-
143
- editor.createHTMLOverlay(this.toggleButtonContainer);
144
- }
145
-
146
- private updateToggleButtonText() {
147
- const containerEnabledClass = 'sound-ui-tool-enabled';
148
- if (this.isEnabled()) {
149
- this.toggleButton.innerText = this.editor.localization.disableAccessibilityExploreTool;
150
- this.toggleButtonContainer.classList.add(containerEnabledClass);
151
- } else {
152
- this.toggleButton.innerText = this.editor.localization.enableAccessibilityExploreTool;
153
- this.toggleButtonContainer.classList.remove(containerEnabledClass);
154
- }
155
- }
156
-
157
- public override setEnabled(enabled: boolean): void {
158
- super.setEnabled(enabled);
159
-
160
- if (!enabled) {
161
- this.soundFeedback?.close();
162
- this.soundFeedback = null;
163
- } else {
164
- this.editor.announceForAccessibility(this.editor.localization.soundExplorerUsageAnnouncement);
165
- }
166
-
167
- this.updateToggleButtonText();
168
- }
169
-
170
- public override onKeyPress(event: KeyPressEvent): boolean {
171
- if (event.code === 'Escape') {
172
- this.setEnabled(false);
173
- return true;
174
- }
175
-
176
- return false;
177
- }
178
-
179
- private lastPointerPos: Point2;
180
-
181
- public override onPointerDown({ current, allPointers }: PointerEvt): boolean {
182
- if (!this.soundFeedback) {
183
- this.soundFeedback = new SoundFeedback();
184
- }
185
-
186
- // Allow two-finger gestures to move the screen.
187
- if (allPointers.length >= 2) {
188
- return false;
189
- }
190
-
191
- // Accept multiple cursors -- some screen readers require multiple (touch) pointers to interact with
192
- // an image instead of using the built-in navigation features.
193
-
194
- this.soundFeedback?.play();
195
- this.soundFeedback?.setColor(this.editor.display.getColorAt(current.screenPos) ?? Color4.black);
196
- this.lastPointerPos = current.canvasPos;
197
- return true;
198
- }
199
-
200
- public override onPointerMove({ current }: PointerEvt): void {
201
- this.soundFeedback?.setColor(this.editor.display.getColorAt(current.screenPos) ?? Color4.black);
202
-
203
- const pointerMotionLine = new LineSegment2(this.lastPointerPos, current.canvasPos);
204
- const collisions = this.editor.image.getElementsIntersectingRegion(pointerMotionLine.bbox).filter(
205
- component => component.intersects(pointerMotionLine)
206
- );
207
- this.lastPointerPos = current.canvasPos;
208
-
209
- if (collisions.length > 0) {
210
- this.soundFeedback?.announceBoundaryCross(collisions.length);
211
- }
212
- }
213
-
214
- public override onPointerUp(_event: PointerEvt): void {
215
- this.soundFeedback?.pause();
216
- }
217
-
218
- public override onGestureCancel(): void {
219
- this.soundFeedback?.pause();
220
- }
221
- }
@@ -1,339 +0,0 @@
1
- import TextComponent from '../components/TextComponent';
2
- import Editor from '../Editor';
3
- import EditorImage from '../EditorImage';
4
- import { Rect2, Mat33, Vec2, Color4 } from '@js-draw/math';
5
- import { PointerDevice } from '../Pointer';
6
- import { EditorEventType } from '../types';
7
- import { PointerEvt } from '../inputEvents';
8
- import BaseTool from './BaseTool';
9
- import { ToolLocalization } from './localization';
10
- import Erase from '../commands/Erase';
11
- import uniteCommands from '../commands/uniteCommands';
12
- import TextRenderingStyle from '../rendering/TextRenderingStyle';
13
- import { MutableReactiveValue, ReactiveValue } from '../util/ReactiveValue';
14
-
15
- const overlayCSSClass = 'textEditorOverlay';
16
- export default class TextTool extends BaseTool {
17
- private textStyleValue: MutableReactiveValue<TextRenderingStyle>;
18
-
19
- // A reference to the current value of `textStyleValue`.
20
- private textStyle: TextRenderingStyle;
21
-
22
- private textEditOverlay: HTMLElement;
23
- private textInputElem: HTMLTextAreaElement|null = null;
24
- private textTargetPosition: Vec2|null = null;
25
- private textMeasuringCtx: CanvasRenderingContext2D|null = null;
26
- private textRotation: number;
27
- private textScale: Vec2 = Vec2.of(1, 1);
28
-
29
- private removeExistingCommand: Erase|null = null;
30
-
31
- public constructor(private editor: Editor, description: string, private localizationTable: ToolLocalization) {
32
- super(editor.notifier, description);
33
- this.textStyleValue = ReactiveValue.fromInitialValue({
34
- size: 32,
35
- fontFamily: 'sans-serif',
36
- renderingStyle: {
37
- fill: Color4.purple,
38
- },
39
- });
40
- this.textStyleValue.onUpdateAndNow(() => {
41
- this.textStyle = this.textStyleValue.get();
42
-
43
- this.updateTextInput();
44
- this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
45
- kind: EditorEventType.ToolUpdated,
46
- tool: this,
47
- });
48
- });
49
-
50
- this.textEditOverlay = document.createElement('div');
51
- this.textEditOverlay.classList.add(overlayCSSClass);
52
- this.editor.addStyleSheet(`
53
- .${overlayCSSClass} {
54
- height: 0;
55
- overflow: visible;
56
- }
57
-
58
- .${overlayCSSClass} textarea {
59
- background-color: rgba(0, 0, 0, 0);
60
-
61
- white-space: pre;
62
- overflow: hidden;
63
-
64
- padding: 0;
65
- margin: 0;
66
- border: none;
67
- padding: 0;
68
-
69
- min-width: 100px;
70
- min-height: 1.1em;
71
- }
72
- `);
73
- this.editor.createHTMLOverlay(this.textEditOverlay);
74
- this.editor.notifier.on(EditorEventType.ViewportChanged, () => this.updateTextInput());
75
- }
76
-
77
- private initTextMeasuringCanvas() {
78
- this.textMeasuringCtx ??= document.createElement('canvas').getContext('2d');
79
- }
80
-
81
- private getTextAscent(text: string, style: TextRenderingStyle): number {
82
- this.initTextMeasuringCanvas();
83
- if (this.textMeasuringCtx) {
84
- this.textMeasuringCtx.textBaseline = 'alphabetic';
85
- TextComponent.applyTextStyles(this.textMeasuringCtx, style);
86
- const measurement = this.textMeasuringCtx.measureText(text);
87
- return measurement.fontBoundingBoxAscent ?? measurement.actualBoundingBoxAscent;
88
- }
89
-
90
- // Estimate
91
- return style.size * 2 / 3;
92
- }
93
-
94
- // Take input from this' textInputElem and add it to the EditorImage.
95
- // If [removeInput], the HTML input element is removed. Otherwise, its value
96
- // is cleared.
97
- private flushInput(removeInput: boolean = true) {
98
- if (this.textInputElem && this.textTargetPosition) {
99
- const content = this.textInputElem.value.trimEnd();
100
-
101
- this.textInputElem.value = '';
102
-
103
- if (removeInput) {
104
- // In some browsers, .remove() triggers a .blur event (synchronously).
105
- // Clear this.textInputElem before removal
106
- const input = this.textInputElem;
107
- this.textInputElem = null;
108
- input.remove();
109
- }
110
-
111
- if (content === '') {
112
- return;
113
- }
114
-
115
- const textTransform = Mat33.translation(
116
- this.textTargetPosition
117
- ).rightMul(
118
- this.getTextScaleMatrix()
119
- ).rightMul(
120
- Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())
121
- ).rightMul(
122
- Mat33.zRotation(this.textRotation)
123
- );
124
-
125
- const textComponent = TextComponent.fromLines(content.split('\n'), textTransform, this.textStyle);
126
-
127
- const action = EditorImage.addElement(textComponent);
128
- if (this.removeExistingCommand) {
129
- // Unapply so that `removeExistingCommand` can be added to the undo stack.
130
- this.removeExistingCommand.unapply(this.editor);
131
-
132
- this.editor.dispatch(uniteCommands([ this.removeExistingCommand, action ]));
133
- this.removeExistingCommand = null;
134
- } else {
135
- this.editor.dispatch(action);
136
- }
137
- }
138
- }
139
-
140
- private getTextScaleMatrix() {
141
- return Mat33.scaling2D(this.textScale.times(1/this.editor.viewport.getSizeOfPixelOnCanvas()));
142
- }
143
-
144
- private updateTextInput() {
145
- if (!this.textInputElem || !this.textTargetPosition) {
146
- this.textInputElem?.remove();
147
- return;
148
- }
149
-
150
- const viewport = this.editor.viewport;
151
- const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
152
- this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
153
- this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
154
- this.textInputElem.style.fontStyle = this.textStyle.fontStyle ?? '';
155
- this.textInputElem.style.fontVariant = this.textStyle.fontVariant ?? '';
156
- this.textInputElem.style.fontWeight = this.textStyle.fontWeight ?? '';
157
- this.textInputElem.style.fontSize = `${this.textStyle.size}px`;
158
- this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
159
-
160
- this.textInputElem.style.position = 'relative';
161
- this.textInputElem.style.left = `${textScreenPos.x}px`;
162
- this.textInputElem.style.top = `${textScreenPos.y}px`;
163
- this.textInputElem.style.margin = '0';
164
-
165
- this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
166
- this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
167
-
168
- // Get the ascent based on the font, using a string of characters
169
- // that is tall in most fonts.
170
- const tallText = 'Testing!';
171
- const ascent = this.getTextAscent(tallText, this.textStyle);
172
- const vertAdjust = ascent;
173
-
174
- const rotation = this.textRotation + viewport.getRotationAngle();
175
- const scale: Mat33 = this.getTextScaleMatrix();
176
- this.textInputElem.style.transform =
177
- `${scale.toCSSMatrix()} rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-vertAdjust}px)`;
178
- this.textInputElem.style.transformOrigin = 'top left';
179
-
180
- // Match the line height of default rendered text.
181
- const lineHeight = Math.floor(this.textStyle.size);
182
- this.textInputElem.style.lineHeight = `${lineHeight}px`;
183
- }
184
-
185
- private startTextInput(textCanvasPos: Vec2, initialText: string) {
186
- this.flushInput();
187
-
188
- this.textInputElem = document.createElement('textarea');
189
- this.textInputElem.value = initialText;
190
- this.textInputElem.style.display = 'inline-block';
191
- this.textTargetPosition = this.editor.viewport.roundPoint(textCanvasPos);
192
- this.textRotation = -this.editor.viewport.getRotationAngle();
193
- this.textScale = Vec2.of(1, 1).times(this.editor.viewport.getSizeOfPixelOnCanvas());
194
- this.updateTextInput();
195
-
196
- // Update the input size/position/etc. after the placeHolder has had time to appear.
197
- setTimeout(() => this.updateTextInput(), 0);
198
-
199
- this.textInputElem.oninput = () => {
200
- if (this.textInputElem) {
201
- this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
202
- this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
203
- }
204
- };
205
- this.textInputElem.onblur = () => {
206
- // Delay removing the input -- flushInput may be called within a blur()
207
- // event handler
208
- const removeInput = false;
209
- const input = this.textInputElem;
210
-
211
- this.flushInput(removeInput);
212
- this.textInputElem = null;
213
- setTimeout(() => {
214
- input?.remove();
215
- }, 0);
216
- };
217
- this.textInputElem.onkeyup = (evt) => {
218
- if (evt.key === 'Enter' && !evt.shiftKey) {
219
- this.flushInput();
220
- this.editor.focus();
221
- } else if (evt.key === 'Escape') {
222
- // Cancel input.
223
- this.textInputElem?.remove();
224
- this.textInputElem = null;
225
- this.editor.focus();
226
-
227
- this.removeExistingCommand?.unapply(this.editor);
228
- this.removeExistingCommand = null;
229
- }
230
- };
231
-
232
- this.textEditOverlay.replaceChildren(this.textInputElem);
233
- setTimeout(() => this.textInputElem?.focus(), 0);
234
- }
235
-
236
- public override setEnabled(enabled: boolean) {
237
- super.setEnabled(enabled);
238
-
239
- if (!enabled) {
240
- this.flushInput();
241
- }
242
-
243
- this.textEditOverlay.style.display = enabled ? 'block' : 'none';
244
- }
245
-
246
- public override onPointerDown({ current, allPointers }: PointerEvt): boolean {
247
- if (current.device === PointerDevice.Eraser) {
248
- return false;
249
- }
250
-
251
- if (allPointers.length === 1) {
252
-
253
- // Are we clicking on a text node?
254
- const canvasPos = current.canvasPos;
255
- const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
256
- const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
257
- const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
258
- let targetTextNodes = targetNodes.filter(node => node instanceof TextComponent) as TextComponent[];
259
-
260
- // Don't try to edit text nodes that contain the viewport (this allows us
261
- // to zoom in on text nodes and add text on top of them.)
262
- const visibleRect = this.editor.viewport.visibleRect;
263
- targetTextNodes = targetTextNodes.filter(node => !node.getBBox().containsRect(visibleRect));
264
-
265
- // End any TextNodes we're currently editing.
266
- this.flushInput();
267
-
268
- if (targetTextNodes.length > 0) {
269
- const targetNode = targetTextNodes[targetTextNodes.length - 1];
270
- this.setTextStyle(targetNode.getTextStyle());
271
-
272
- // Create and temporarily apply removeExistingCommand.
273
- this.removeExistingCommand = new Erase([ targetNode ]);
274
- this.removeExistingCommand.apply(this.editor);
275
-
276
- this.startTextInput(targetNode.getBaselinePos(), targetNode.getText());
277
-
278
- const transform = targetNode.getTransform();
279
- this.textRotation = transform.transformVec3(Vec2.unitX).angle();
280
- const scaleFactor = transform.transformVec3(Vec2.unitX).magnitude();
281
- this.textScale = Vec2.of(1, 1).times(scaleFactor);
282
- this.updateTextInput();
283
- } else {
284
- this.removeExistingCommand = null;
285
- this.startTextInput(current.canvasPos, '');
286
- }
287
- return true;
288
- }
289
-
290
- return false;
291
- }
292
-
293
- public override onGestureCancel(): void {
294
- this.flushInput();
295
- this.editor.focus();
296
- }
297
-
298
- public setFontFamily(fontFamily: string) {
299
- if (fontFamily !== this.textStyle.fontFamily) {
300
- this.textStyleValue.set({
301
- ...this.textStyle,
302
- fontFamily: fontFamily,
303
- });
304
- }
305
- }
306
-
307
- public setColor(color: Color4) {
308
- if (!color.eq(this.textStyle.renderingStyle.fill)) {
309
- this.textStyleValue.set({
310
- ...this.textStyle,
311
- renderingStyle: {
312
- ...this.textStyle.renderingStyle,
313
- fill: color,
314
- },
315
- });
316
- }
317
- }
318
-
319
- public setFontSize(size: number) {
320
- if (size !== this.textStyle.size) {
321
- this.textStyleValue.set({
322
- ...this.textStyle,
323
- size,
324
- });
325
- }
326
- }
327
-
328
- public getTextStyle(): TextRenderingStyle {
329
- return this.textStyle;
330
- }
331
-
332
- public getStyleValue(): MutableReactiveValue<TextRenderingStyle> {
333
- return this.textStyleValue;
334
- }
335
-
336
- private setTextStyle(style: TextRenderingStyle) {
337
- this.textStyleValue.set(style);
338
- }
339
- }