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.
- package/LICENSE +21 -0
- package/dist/bundle.js +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/version.mjs +1 -1
- package/docs/img/readme-images/js-draw.jpg +0 -0
- package/docs/img/readme-images/unsupported-elements--in-editor.png +0 -0
- package/package.json +5 -4
- package/dist-test/test_imports/package-lock.json +0 -13
- package/dist-test/test_imports/package.json +0 -12
- package/dist-test/test_imports/test-imports.js +0 -11
- package/dist-test/test_imports/test-require.cjs +0 -14
- package/src/Editor.loadFrom.test.ts +0 -24
- package/src/Editor.test.ts +0 -107
- package/src/Editor.toSVG.test.ts +0 -294
- package/src/Editor.ts +0 -1443
- package/src/EditorImage.test.ts +0 -117
- package/src/EditorImage.ts +0 -609
- package/src/EventDispatcher.test.ts +0 -123
- package/src/EventDispatcher.ts +0 -72
- package/src/Pointer.ts +0 -183
- package/src/SVGLoader.test.ts +0 -114
- package/src/SVGLoader.ts +0 -672
- package/src/UndoRedoHistory.test.ts +0 -34
- package/src/UndoRedoHistory.ts +0 -102
- package/src/Viewport.ts +0 -322
- package/src/bundle/bundled.ts +0 -7
- package/src/commands/Command.ts +0 -45
- package/src/commands/Duplicate.ts +0 -75
- package/src/commands/Erase.ts +0 -95
- package/src/commands/SerializableCommand.ts +0 -49
- package/src/commands/UnresolvedCommand.ts +0 -37
- package/src/commands/invertCommand.ts +0 -58
- package/src/commands/lib.ts +0 -16
- package/src/commands/localization.ts +0 -47
- package/src/commands/uniteCommands.test.ts +0 -23
- package/src/commands/uniteCommands.ts +0 -140
- package/src/components/AbstractComponent.transformBy.test.ts +0 -23
- package/src/components/AbstractComponent.ts +0 -383
- package/src/components/BackgroundComponent.test.ts +0 -44
- package/src/components/BackgroundComponent.ts +0 -348
- package/src/components/ImageComponent.ts +0 -176
- package/src/components/RestylableComponent.ts +0 -161
- package/src/components/SVGGlobalAttributesObject.ts +0 -79
- package/src/components/Stroke.test.ts +0 -137
- package/src/components/Stroke.ts +0 -294
- package/src/components/TextComponent.test.ts +0 -202
- package/src/components/TextComponent.ts +0 -429
- package/src/components/UnknownSVGObject.test.ts +0 -10
- package/src/components/UnknownSVGObject.ts +0 -60
- package/src/components/builders/ArrowBuilder.ts +0 -106
- package/src/components/builders/CircleBuilder.ts +0 -100
- package/src/components/builders/FreehandLineBuilder.test.ts +0 -24
- package/src/components/builders/FreehandLineBuilder.ts +0 -210
- package/src/components/builders/LineBuilder.ts +0 -77
- package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +0 -453
- package/src/components/builders/RectangleBuilder.ts +0 -73
- package/src/components/builders/types.ts +0 -15
- package/src/components/lib.ts +0 -31
- package/src/components/localization.ts +0 -24
- package/src/components/util/StrokeSmoother.ts +0 -302
- package/src/components/util/describeComponentList.ts +0 -18
- package/src/dialogs/makeAboutDialog.ts +0 -82
- package/src/inputEvents.ts +0 -143
- package/src/lib.ts +0 -91
- package/src/localization.ts +0 -34
- package/src/localizations/de.ts +0 -146
- package/src/localizations/en.ts +0 -8
- package/src/localizations/es.ts +0 -74
- package/src/localizations/getLocalizationTable.test.ts +0 -27
- package/src/localizations/getLocalizationTable.ts +0 -74
- package/src/rendering/Display.ts +0 -247
- package/src/rendering/RenderablePathSpec.ts +0 -88
- package/src/rendering/RenderingStyle.test.ts +0 -68
- package/src/rendering/RenderingStyle.ts +0 -55
- package/src/rendering/TextRenderingStyle.ts +0 -55
- package/src/rendering/caching/CacheRecord.test.ts +0 -48
- package/src/rendering/caching/CacheRecord.ts +0 -76
- package/src/rendering/caching/CacheRecordManager.ts +0 -71
- package/src/rendering/caching/RenderingCache.test.ts +0 -43
- package/src/rendering/caching/RenderingCache.ts +0 -66
- package/src/rendering/caching/RenderingCacheNode.ts +0 -404
- package/src/rendering/caching/testUtils.ts +0 -35
- package/src/rendering/caching/types.ts +0 -34
- package/src/rendering/lib.ts +0 -8
- package/src/rendering/localization.ts +0 -20
- package/src/rendering/renderers/AbstractRenderer.ts +0 -232
- package/src/rendering/renderers/CanvasRenderer.ts +0 -312
- package/src/rendering/renderers/DummyRenderer.test.ts +0 -41
- package/src/rendering/renderers/DummyRenderer.ts +0 -142
- package/src/rendering/renderers/SVGRenderer.ts +0 -434
- package/src/rendering/renderers/TextOnlyRenderer.test.ts +0 -34
- package/src/rendering/renderers/TextOnlyRenderer.ts +0 -68
- package/src/shortcuts/KeyBinding.test.ts +0 -61
- package/src/shortcuts/KeyBinding.ts +0 -257
- package/src/shortcuts/KeyboardShortcutManager.test.ts +0 -95
- package/src/shortcuts/KeyboardShortcutManager.ts +0 -163
- package/src/shortcuts/lib.ts +0 -3
- package/src/testing/createEditor.ts +0 -11
- package/src/testing/getUniquePointerId.ts +0 -18
- package/src/testing/lib.ts +0 -3
- package/src/testing/sendPenEvent.ts +0 -36
- package/src/testing/sendTouchEvent.ts +0 -71
- package/src/toolbar/AbstractToolbar.ts +0 -542
- package/src/toolbar/DropdownToolbar.ts +0 -220
- package/src/toolbar/EdgeToolbar.test.ts +0 -54
- package/src/toolbar/EdgeToolbar.ts +0 -543
- package/src/toolbar/IconProvider.ts +0 -861
- package/src/toolbar/constants.ts +0 -1
- package/src/toolbar/lib.ts +0 -6
- package/src/toolbar/localization.ts +0 -136
- package/src/toolbar/types.ts +0 -13
- package/src/toolbar/widgets/ActionButtonWidget.ts +0 -39
- package/src/toolbar/widgets/BaseToolWidget.ts +0 -81
- package/src/toolbar/widgets/BaseWidget.ts +0 -495
- package/src/toolbar/widgets/DocumentPropertiesWidget.ts +0 -250
- package/src/toolbar/widgets/EraserToolWidget.ts +0 -84
- package/src/toolbar/widgets/HandToolWidget.ts +0 -239
- package/src/toolbar/widgets/InsertImageWidget.ts +0 -248
- package/src/toolbar/widgets/OverflowWidget.ts +0 -92
- package/src/toolbar/widgets/PenToolWidget.ts +0 -369
- package/src/toolbar/widgets/SelectionToolWidget.ts +0 -195
- package/src/toolbar/widgets/TextToolWidget.ts +0 -149
- package/src/toolbar/widgets/components/makeColorInput.ts +0 -184
- package/src/toolbar/widgets/components/makeFileInput.ts +0 -128
- package/src/toolbar/widgets/components/makeGridSelector.ts +0 -179
- package/src/toolbar/widgets/components/makeSeparator.ts +0 -17
- package/src/toolbar/widgets/components/makeThicknessSlider.ts +0 -62
- package/src/toolbar/widgets/keybindings.ts +0 -19
- package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +0 -262
- package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +0 -71
- package/src/toolbar/widgets/layout/types.ts +0 -74
- package/src/toolbar/widgets/lib.ts +0 -13
- package/src/tools/BaseTool.ts +0 -169
- package/src/tools/Eraser.test.ts +0 -103
- package/src/tools/Eraser.ts +0 -173
- package/src/tools/FindTool.test.ts +0 -67
- package/src/tools/FindTool.ts +0 -153
- package/src/tools/InputFilter/FunctionMapper.ts +0 -17
- package/src/tools/InputFilter/InputMapper.ts +0 -41
- package/src/tools/InputFilter/InputPipeline.test.ts +0 -41
- package/src/tools/InputFilter/InputPipeline.ts +0 -34
- package/src/tools/InputFilter/InputStabilizer.ts +0 -254
- package/src/tools/InputFilter/StrokeKeyboardControl.ts +0 -104
- package/src/tools/PanZoom.test.ts +0 -339
- package/src/tools/PanZoom.ts +0 -525
- package/src/tools/PasteHandler.ts +0 -94
- package/src/tools/Pen.test.ts +0 -260
- package/src/tools/Pen.ts +0 -284
- package/src/tools/PipetteTool.ts +0 -84
- package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +0 -29
- package/src/tools/SelectionTool/Selection.ts +0 -647
- package/src/tools/SelectionTool/SelectionHandle.ts +0 -142
- package/src/tools/SelectionTool/SelectionTool.test.ts +0 -370
- package/src/tools/SelectionTool/SelectionTool.ts +0 -510
- package/src/tools/SelectionTool/TransformMode.ts +0 -112
- package/src/tools/SelectionTool/types.ts +0 -11
- package/src/tools/SoundUITool.ts +0 -221
- package/src/tools/TextTool.ts +0 -339
- package/src/tools/ToolController.ts +0 -224
- package/src/tools/ToolEnabledGroup.ts +0 -14
- package/src/tools/ToolSwitcherShortcut.ts +0 -39
- package/src/tools/ToolbarShortcutHandler.ts +0 -39
- package/src/tools/UndoRedoShortcut.test.ts +0 -62
- package/src/tools/UndoRedoShortcut.ts +0 -24
- package/src/tools/keybindings.ts +0 -85
- package/src/tools/lib.ts +0 -22
- package/src/tools/localization.ts +0 -76
- package/src/types.ts +0 -151
- package/src/util/ReactiveValue.test.ts +0 -168
- package/src/util/ReactiveValue.ts +0 -241
- package/src/util/assertions.ts +0 -55
- package/src/util/fileToBase64.ts +0 -18
- package/src/util/guessKeyCodeFromKey.ts +0 -36
- package/src/util/listPrefixMatch.ts +0 -19
- package/src/util/stopPropagationOfScrollingWheelEvents.ts +0 -20
- package/src/util/untilNextAnimationFrame.ts +0 -9
- package/src/util/waitForAll.ts +0 -18
- package/src/util/waitForTimeout.ts +0 -9
- package/src/version.test.ts +0 -12
- package/src/version.ts +0 -3
- package/tools/allLocales.js +0 -4
- package/tools/copyREADME.ts +0 -62
package/src/tools/SoundUITool.ts
DELETED
@@ -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
|
-
}
|
package/src/tools/TextTool.ts
DELETED
@@ -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
|
-
}
|