js-draw 1.0.1 → 1.1.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 (197) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Editor.css +1 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/toolbar/AbstractToolbar.d.ts +9 -13
  6. package/dist/cjs/toolbar/AbstractToolbar.js +14 -19
  7. package/dist/cjs/toolbar/widgets/SaveActionWidget.d.ts +10 -0
  8. package/dist/cjs/toolbar/widgets/SaveActionWidget.js +26 -0
  9. package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
  10. package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
  11. package/dist/cjs/version.js +1 -1
  12. package/dist/mjs/toolbar/AbstractToolbar.d.ts +9 -13
  13. package/dist/mjs/toolbar/AbstractToolbar.mjs +14 -19
  14. package/dist/mjs/toolbar/widgets/SaveActionWidget.d.ts +10 -0
  15. package/dist/mjs/toolbar/widgets/SaveActionWidget.mjs +21 -0
  16. package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
  17. package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
  18. package/dist/mjs/version.mjs +1 -1
  19. package/docs/img/readme-images/js-draw.jpg +0 -0
  20. package/docs/img/readme-images/unsupported-elements--in-editor.png +0 -0
  21. package/package.json +5 -4
  22. package/src/toolbar/EdgeToolbar.scss +1 -0
  23. package/dist-test/test_imports/package-lock.json +0 -13
  24. package/dist-test/test_imports/package.json +0 -12
  25. package/dist-test/test_imports/test-imports.js +0 -11
  26. package/dist-test/test_imports/test-require.cjs +0 -14
  27. package/src/Editor.loadFrom.test.ts +0 -24
  28. package/src/Editor.test.ts +0 -107
  29. package/src/Editor.toSVG.test.ts +0 -294
  30. package/src/Editor.ts +0 -1443
  31. package/src/EditorImage.test.ts +0 -117
  32. package/src/EditorImage.ts +0 -609
  33. package/src/EventDispatcher.test.ts +0 -123
  34. package/src/EventDispatcher.ts +0 -72
  35. package/src/Pointer.ts +0 -183
  36. package/src/SVGLoader.test.ts +0 -114
  37. package/src/SVGLoader.ts +0 -672
  38. package/src/UndoRedoHistory.test.ts +0 -34
  39. package/src/UndoRedoHistory.ts +0 -102
  40. package/src/Viewport.ts +0 -322
  41. package/src/bundle/bundled.ts +0 -7
  42. package/src/commands/Command.ts +0 -45
  43. package/src/commands/Duplicate.ts +0 -75
  44. package/src/commands/Erase.ts +0 -95
  45. package/src/commands/SerializableCommand.ts +0 -49
  46. package/src/commands/UnresolvedCommand.ts +0 -37
  47. package/src/commands/invertCommand.ts +0 -58
  48. package/src/commands/lib.ts +0 -16
  49. package/src/commands/localization.ts +0 -47
  50. package/src/commands/uniteCommands.test.ts +0 -23
  51. package/src/commands/uniteCommands.ts +0 -140
  52. package/src/components/AbstractComponent.transformBy.test.ts +0 -23
  53. package/src/components/AbstractComponent.ts +0 -383
  54. package/src/components/BackgroundComponent.test.ts +0 -44
  55. package/src/components/BackgroundComponent.ts +0 -348
  56. package/src/components/ImageComponent.ts +0 -176
  57. package/src/components/RestylableComponent.ts +0 -161
  58. package/src/components/SVGGlobalAttributesObject.ts +0 -79
  59. package/src/components/Stroke.test.ts +0 -137
  60. package/src/components/Stroke.ts +0 -294
  61. package/src/components/TextComponent.test.ts +0 -202
  62. package/src/components/TextComponent.ts +0 -429
  63. package/src/components/UnknownSVGObject.test.ts +0 -10
  64. package/src/components/UnknownSVGObject.ts +0 -60
  65. package/src/components/builders/ArrowBuilder.ts +0 -106
  66. package/src/components/builders/CircleBuilder.ts +0 -100
  67. package/src/components/builders/FreehandLineBuilder.test.ts +0 -24
  68. package/src/components/builders/FreehandLineBuilder.ts +0 -210
  69. package/src/components/builders/LineBuilder.ts +0 -77
  70. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +0 -453
  71. package/src/components/builders/RectangleBuilder.ts +0 -73
  72. package/src/components/builders/types.ts +0 -15
  73. package/src/components/lib.ts +0 -31
  74. package/src/components/localization.ts +0 -24
  75. package/src/components/util/StrokeSmoother.ts +0 -302
  76. package/src/components/util/describeComponentList.ts +0 -18
  77. package/src/dialogs/makeAboutDialog.ts +0 -82
  78. package/src/inputEvents.ts +0 -143
  79. package/src/lib.ts +0 -91
  80. package/src/localization.ts +0 -34
  81. package/src/localizations/de.ts +0 -146
  82. package/src/localizations/en.ts +0 -8
  83. package/src/localizations/es.ts +0 -74
  84. package/src/localizations/getLocalizationTable.test.ts +0 -27
  85. package/src/localizations/getLocalizationTable.ts +0 -74
  86. package/src/rendering/Display.ts +0 -247
  87. package/src/rendering/RenderablePathSpec.ts +0 -88
  88. package/src/rendering/RenderingStyle.test.ts +0 -68
  89. package/src/rendering/RenderingStyle.ts +0 -55
  90. package/src/rendering/TextRenderingStyle.ts +0 -55
  91. package/src/rendering/caching/CacheRecord.test.ts +0 -48
  92. package/src/rendering/caching/CacheRecord.ts +0 -76
  93. package/src/rendering/caching/CacheRecordManager.ts +0 -71
  94. package/src/rendering/caching/RenderingCache.test.ts +0 -43
  95. package/src/rendering/caching/RenderingCache.ts +0 -66
  96. package/src/rendering/caching/RenderingCacheNode.ts +0 -404
  97. package/src/rendering/caching/testUtils.ts +0 -35
  98. package/src/rendering/caching/types.ts +0 -34
  99. package/src/rendering/lib.ts +0 -8
  100. package/src/rendering/localization.ts +0 -20
  101. package/src/rendering/renderers/AbstractRenderer.ts +0 -232
  102. package/src/rendering/renderers/CanvasRenderer.ts +0 -312
  103. package/src/rendering/renderers/DummyRenderer.test.ts +0 -41
  104. package/src/rendering/renderers/DummyRenderer.ts +0 -142
  105. package/src/rendering/renderers/SVGRenderer.ts +0 -434
  106. package/src/rendering/renderers/TextOnlyRenderer.test.ts +0 -34
  107. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -68
  108. package/src/shortcuts/KeyBinding.test.ts +0 -61
  109. package/src/shortcuts/KeyBinding.ts +0 -257
  110. package/src/shortcuts/KeyboardShortcutManager.test.ts +0 -95
  111. package/src/shortcuts/KeyboardShortcutManager.ts +0 -163
  112. package/src/shortcuts/lib.ts +0 -3
  113. package/src/testing/createEditor.ts +0 -11
  114. package/src/testing/getUniquePointerId.ts +0 -18
  115. package/src/testing/lib.ts +0 -3
  116. package/src/testing/sendPenEvent.ts +0 -36
  117. package/src/testing/sendTouchEvent.ts +0 -71
  118. package/src/toolbar/AbstractToolbar.ts +0 -542
  119. package/src/toolbar/DropdownToolbar.ts +0 -220
  120. package/src/toolbar/EdgeToolbar.test.ts +0 -54
  121. package/src/toolbar/EdgeToolbar.ts +0 -543
  122. package/src/toolbar/IconProvider.ts +0 -861
  123. package/src/toolbar/constants.ts +0 -1
  124. package/src/toolbar/lib.ts +0 -6
  125. package/src/toolbar/localization.ts +0 -136
  126. package/src/toolbar/types.ts +0 -13
  127. package/src/toolbar/widgets/ActionButtonWidget.ts +0 -39
  128. package/src/toolbar/widgets/BaseToolWidget.ts +0 -81
  129. package/src/toolbar/widgets/BaseWidget.ts +0 -495
  130. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +0 -250
  131. package/src/toolbar/widgets/EraserToolWidget.ts +0 -84
  132. package/src/toolbar/widgets/HandToolWidget.ts +0 -239
  133. package/src/toolbar/widgets/InsertImageWidget.ts +0 -248
  134. package/src/toolbar/widgets/OverflowWidget.ts +0 -92
  135. package/src/toolbar/widgets/PenToolWidget.ts +0 -369
  136. package/src/toolbar/widgets/SelectionToolWidget.ts +0 -195
  137. package/src/toolbar/widgets/TextToolWidget.ts +0 -149
  138. package/src/toolbar/widgets/components/makeColorInput.ts +0 -184
  139. package/src/toolbar/widgets/components/makeFileInput.ts +0 -128
  140. package/src/toolbar/widgets/components/makeGridSelector.ts +0 -179
  141. package/src/toolbar/widgets/components/makeSeparator.ts +0 -17
  142. package/src/toolbar/widgets/components/makeThicknessSlider.ts +0 -62
  143. package/src/toolbar/widgets/keybindings.ts +0 -19
  144. package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +0 -262
  145. package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +0 -71
  146. package/src/toolbar/widgets/layout/types.ts +0 -74
  147. package/src/toolbar/widgets/lib.ts +0 -13
  148. package/src/tools/BaseTool.ts +0 -169
  149. package/src/tools/Eraser.test.ts +0 -103
  150. package/src/tools/Eraser.ts +0 -173
  151. package/src/tools/FindTool.test.ts +0 -67
  152. package/src/tools/FindTool.ts +0 -153
  153. package/src/tools/InputFilter/FunctionMapper.ts +0 -17
  154. package/src/tools/InputFilter/InputMapper.ts +0 -41
  155. package/src/tools/InputFilter/InputPipeline.test.ts +0 -41
  156. package/src/tools/InputFilter/InputPipeline.ts +0 -34
  157. package/src/tools/InputFilter/InputStabilizer.ts +0 -254
  158. package/src/tools/InputFilter/StrokeKeyboardControl.ts +0 -104
  159. package/src/tools/PanZoom.test.ts +0 -339
  160. package/src/tools/PanZoom.ts +0 -525
  161. package/src/tools/PasteHandler.ts +0 -94
  162. package/src/tools/Pen.test.ts +0 -260
  163. package/src/tools/Pen.ts +0 -284
  164. package/src/tools/PipetteTool.ts +0 -84
  165. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +0 -29
  166. package/src/tools/SelectionTool/Selection.ts +0 -647
  167. package/src/tools/SelectionTool/SelectionHandle.ts +0 -142
  168. package/src/tools/SelectionTool/SelectionTool.test.ts +0 -370
  169. package/src/tools/SelectionTool/SelectionTool.ts +0 -510
  170. package/src/tools/SelectionTool/TransformMode.ts +0 -112
  171. package/src/tools/SelectionTool/types.ts +0 -11
  172. package/src/tools/SoundUITool.ts +0 -221
  173. package/src/tools/TextTool.ts +0 -339
  174. package/src/tools/ToolController.ts +0 -224
  175. package/src/tools/ToolEnabledGroup.ts +0 -14
  176. package/src/tools/ToolSwitcherShortcut.ts +0 -39
  177. package/src/tools/ToolbarShortcutHandler.ts +0 -39
  178. package/src/tools/UndoRedoShortcut.test.ts +0 -62
  179. package/src/tools/UndoRedoShortcut.ts +0 -24
  180. package/src/tools/keybindings.ts +0 -85
  181. package/src/tools/lib.ts +0 -22
  182. package/src/tools/localization.ts +0 -76
  183. package/src/types.ts +0 -151
  184. package/src/util/ReactiveValue.test.ts +0 -168
  185. package/src/util/ReactiveValue.ts +0 -241
  186. package/src/util/assertions.ts +0 -55
  187. package/src/util/fileToBase64.ts +0 -18
  188. package/src/util/guessKeyCodeFromKey.ts +0 -36
  189. package/src/util/listPrefixMatch.ts +0 -19
  190. package/src/util/stopPropagationOfScrollingWheelEvents.ts +0 -20
  191. package/src/util/untilNextAnimationFrame.ts +0 -9
  192. package/src/util/waitForAll.ts +0 -18
  193. package/src/util/waitForTimeout.ts +0 -9
  194. package/src/version.test.ts +0 -12
  195. package/src/version.ts +0 -3
  196. package/tools/allLocales.js +0 -4
  197. 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
- }