js-draw 0.1.3 → 0.1.6
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/CHANGELOG.md +13 -0
- package/README.md +21 -12
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +2 -1
- package/dist/src/Editor.js +19 -4
- package/dist/src/SVGLoader.js +6 -2
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +5 -5
- package/dist/src/components/Text.js +3 -1
- package/dist/src/localization.d.ts +2 -1
- package/dist/src/localization.js +2 -1
- package/dist/src/rendering/Display.d.ts +2 -0
- package/dist/src/rendering/Display.js +19 -0
- package/dist/src/rendering/localization.d.ts +5 -0
- package/dist/src/rendering/localization.js +4 -0
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +24 -0
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
- package/dist/src/toolbar/HTMLToolbar.js +27 -1
- package/dist/src/toolbar/icons.js +1 -0
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/tools/TextTool.d.ts +2 -0
- package/dist/src/tools/TextTool.js +25 -5
- package/dist-test/test-dist-bundle.html +8 -1
- package/package.json +1 -1
- package/src/Editor.css +12 -0
- package/src/Editor.ts +20 -5
- package/src/SVGLoader.ts +7 -2
- package/src/Viewport.ts +5 -5
- package/src/components/Text.ts +5 -1
- package/src/localization.ts +3 -1
- package/src/rendering/Display.ts +26 -0
- package/src/rendering/localization.ts +10 -0
- package/src/rendering/renderers/TextOnlyRenderer.ts +51 -0
- package/src/toolbar/HTMLToolbar.ts +34 -2
- package/src/toolbar/icons.ts +1 -0
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/toolbar.css +6 -3
- package/src/tools/TextTool.ts +27 -4
package/dist/src/Editor.d.ts
CHANGED
@@ -14,7 +14,7 @@ import { EditorLocalization } from './localization';
|
|
14
14
|
export interface EditorSettings {
|
15
15
|
renderingMode: RenderingMode;
|
16
16
|
localization: Partial<EditorLocalization>;
|
17
|
-
wheelEventsEnabled: boolean;
|
17
|
+
wheelEventsEnabled: boolean | 'only-if-focused';
|
18
18
|
}
|
19
19
|
export declare class Editor {
|
20
20
|
private container;
|
@@ -48,6 +48,7 @@ export declare class Editor {
|
|
48
48
|
rerender(showImageBounds?: boolean): void;
|
49
49
|
drawWetInk(...path: RenderablePathSpec[]): void;
|
50
50
|
clearWetInk(): void;
|
51
|
+
focus(): void;
|
51
52
|
createHTMLOverlay(overlay: HTMLElement): {
|
52
53
|
remove: () => void;
|
53
54
|
};
|
package/dist/src/Editor.js
CHANGED
@@ -183,13 +183,24 @@ export class Editor {
|
|
183
183
|
})) {
|
184
184
|
evt.preventDefault();
|
185
185
|
}
|
186
|
+
else if (evt.key === 'Escape') {
|
187
|
+
this.renderingRegion.blur();
|
188
|
+
}
|
186
189
|
});
|
187
190
|
this.container.addEventListener('wheel', evt => {
|
188
191
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
189
|
-
// Process wheel events if the ctrl key is down -- we do want to handle
|
192
|
+
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
190
193
|
// pinch-zooming.
|
191
|
-
if (!
|
192
|
-
|
194
|
+
if (!evt.ctrlKey) {
|
195
|
+
if (!this.settings.wheelEventsEnabled) {
|
196
|
+
return;
|
197
|
+
}
|
198
|
+
else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
199
|
+
const focusedChild = this.container.querySelector(':focus');
|
200
|
+
if (!focusedChild) {
|
201
|
+
return;
|
202
|
+
}
|
203
|
+
}
|
193
204
|
}
|
194
205
|
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
195
206
|
delta = delta.times(15);
|
@@ -200,7 +211,7 @@ export class Editor {
|
|
200
211
|
if (evt.ctrlKey) {
|
201
212
|
delta = Vec3.of(0, 0, evt.deltaY);
|
202
213
|
}
|
203
|
-
const pos = Vec2.of(evt.
|
214
|
+
const pos = Vec2.of(evt.offsetX, evt.offsetY);
|
204
215
|
if (this.toolController.dispatchInputEvent({
|
205
216
|
kind: InputEvtType.WheelEvt,
|
206
217
|
delta,
|
@@ -297,6 +308,10 @@ export class Editor {
|
|
297
308
|
clearWetInk() {
|
298
309
|
this.display.getWetInkRenderer().clear();
|
299
310
|
}
|
311
|
+
// Focuses the region used for text input
|
312
|
+
focus() {
|
313
|
+
this.renderingRegion.focus();
|
314
|
+
}
|
300
315
|
createHTMLOverlay(overlay) {
|
301
316
|
overlay.classList.add('overlay');
|
302
317
|
this.container.appendChild(overlay);
|
package/dist/src/SVGLoader.js
CHANGED
@@ -162,13 +162,17 @@ export default class SVGLoader {
|
|
162
162
|
}
|
163
163
|
const style = {
|
164
164
|
size: fontSize,
|
165
|
-
fontFamily: computedStyles.fontFamily || 'sans',
|
165
|
+
fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
|
166
166
|
renderingStyle: {
|
167
167
|
fill: Color4.fromString(computedStyles.fill)
|
168
168
|
},
|
169
169
|
};
|
170
|
+
let transformProperty = computedStyles.transform;
|
171
|
+
if (transformProperty === '' || transformProperty === 'none') {
|
172
|
+
transformProperty = elem.style.transform || 'none';
|
173
|
+
}
|
170
174
|
// Compute transform matrix
|
171
|
-
let transform = Mat33.fromCSSMatrix(
|
175
|
+
let transform = Mat33.fromCSSMatrix(transformProperty);
|
172
176
|
const supportedAttrs = [];
|
173
177
|
const elemX = elem.getAttribute('x');
|
174
178
|
const elemY = elem.getAttribute('y');
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -26,7 +26,7 @@ export declare class Viewport {
|
|
26
26
|
get visibleRect(): Rect2;
|
27
27
|
screenToCanvas(screenPoint: Point2): Point2;
|
28
28
|
canvasToScreen(canvasPoint: Point2): Point2;
|
29
|
-
resetTransform(newTransform
|
29
|
+
resetTransform(newTransform?: Mat33): void;
|
30
30
|
get screenToCanvasTransform(): Mat33;
|
31
31
|
get canvasToScreenTransform(): Mat33;
|
32
32
|
getResolution(): Vec2;
|
package/dist/src/Viewport.js
CHANGED
@@ -36,7 +36,7 @@ export class Viewport {
|
|
36
36
|
}
|
37
37
|
// Updates the transformation directly. Using ViewportTransform is preferred.
|
38
38
|
// [newTransform] should map from canvas coordinates to screen coordinates.
|
39
|
-
resetTransform(newTransform) {
|
39
|
+
resetTransform(newTransform = Mat33.identity) {
|
40
40
|
this.transform = newTransform;
|
41
41
|
this.inverseTransform = newTransform.inverse();
|
42
42
|
this.notifier.dispatch(EditorEventType.ViewportChanged, {
|
@@ -93,17 +93,17 @@ export class Viewport {
|
|
93
93
|
if (isNaN(toMakeVisible.size.magnitude())) {
|
94
94
|
throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
|
95
95
|
}
|
96
|
-
// Try to move the selection within the center
|
96
|
+
// Try to move the selection within the center 4/5ths of the viewport.
|
97
97
|
const recomputeTargetRect = () => {
|
98
98
|
// transform transforms objects on the canvas. As such, we need to invert it
|
99
99
|
// to transform the viewport.
|
100
100
|
const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
|
101
|
-
return visibleRect.transformedBoundingBox(Mat33.scaling2D(
|
101
|
+
return visibleRect.transformedBoundingBox(Mat33.scaling2D(4 / 5, visibleRect.center));
|
102
102
|
};
|
103
103
|
let targetRect = recomputeTargetRect();
|
104
104
|
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
|
105
|
-
// Ensure that toMakeVisible is at least 1/
|
106
|
-
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension <
|
105
|
+
// Ensure that toMakeVisible is at least 1/3rd of the visible region.
|
106
|
+
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 1 / 3;
|
107
107
|
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
|
108
108
|
// If larger than the target, ensure that the longest axis is visible.
|
109
109
|
// If smaller, shrink the visible rectangle as much as possible
|
@@ -11,10 +11,12 @@ export default class Text extends AbstractComponent {
|
|
11
11
|
}
|
12
12
|
static applyTextStyles(ctx, style) {
|
13
13
|
var _a, _b;
|
14
|
+
// Quote the font family if necessary.
|
15
|
+
const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
|
14
16
|
ctx.font = [
|
15
17
|
((_a = style.size) !== null && _a !== void 0 ? _a : 12) + 'px',
|
16
18
|
(_b = style.fontWeight) !== null && _b !== void 0 ? _b : '',
|
17
|
-
|
19
|
+
`${fontFamily}`,
|
18
20
|
style.fontWeight
|
19
21
|
].join(' ');
|
20
22
|
ctx.textAlign = 'left';
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import { CommandLocalization } from './commands/localization';
|
2
2
|
import { ImageComponentLocalization } from './components/localization';
|
3
|
+
import { TextRendererLocalization } from './rendering/localization';
|
3
4
|
import { ToolbarLocalization } from './toolbar/localization';
|
4
5
|
import { ToolLocalization } from './tools/localization';
|
5
|
-
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
|
6
|
+
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization, TextRendererLocalization {
|
6
7
|
undoAnnouncement: (actionDescription: string) => string;
|
7
8
|
redoAnnouncement: (actionDescription: string) => string;
|
8
9
|
doneLoading: string;
|
package/dist/src/localization.js
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { defaultCommandLocalization } from './commands/localization';
|
2
2
|
import { defaultComponentLocalization } from './components/localization';
|
3
|
+
import { defaultTextRendererLocalization } from './rendering/localization';
|
3
4
|
import { defaultToolbarLocalization } from './toolbar/localization';
|
4
5
|
import { defaultToolLocalization } from './tools/localization';
|
5
|
-
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
|
6
|
+
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), defaultTextRendererLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
|
@@ -10,6 +10,7 @@ export default class Display {
|
|
10
10
|
private parent;
|
11
11
|
private dryInkRenderer;
|
12
12
|
private wetInkRenderer;
|
13
|
+
private textRenderer;
|
13
14
|
private cache;
|
14
15
|
private resizeSurfacesCallback?;
|
15
16
|
private flattenCallback?;
|
@@ -18,6 +19,7 @@ export default class Display {
|
|
18
19
|
get height(): number;
|
19
20
|
getCache(): RenderingCache;
|
20
21
|
private initializeCanvasRendering;
|
22
|
+
private initializeTextRendering;
|
21
23
|
startRerender(): AbstractRenderer;
|
22
24
|
setDraftMode(draftMode: boolean): void;
|
23
25
|
getDryInkRenderer(): AbstractRenderer;
|
@@ -3,6 +3,7 @@ import { EditorEventType } from '../types';
|
|
3
3
|
import DummyRenderer from './renderers/DummyRenderer';
|
4
4
|
import { Vec2 } from '../geometry/Vec2';
|
5
5
|
import RenderingCache from './caching/RenderingCache';
|
6
|
+
import TextOnlyRenderer from './renderers/TextOnlyRenderer';
|
6
7
|
export var RenderingMode;
|
7
8
|
(function (RenderingMode) {
|
8
9
|
RenderingMode[RenderingMode["DummyRenderer"] = 0] = "DummyRenderer";
|
@@ -23,6 +24,8 @@ export default class Display {
|
|
23
24
|
else {
|
24
25
|
throw new Error(`Unknown rendering mode, ${mode}!`);
|
25
26
|
}
|
27
|
+
this.textRenderer = new TextOnlyRenderer(editor.viewport, editor.localization);
|
28
|
+
this.initializeTextRendering();
|
26
29
|
const cacheBlockResolution = Vec2.of(600, 600);
|
27
30
|
this.cache = new RenderingCache({
|
28
31
|
createRenderer: () => {
|
@@ -104,6 +107,22 @@ export default class Display {
|
|
104
107
|
dryInkCtx.drawImage(wetInkCanvas, 0, 0);
|
105
108
|
};
|
106
109
|
}
|
110
|
+
initializeTextRendering() {
|
111
|
+
const textRendererOutputContainer = document.createElement('div');
|
112
|
+
textRendererOutputContainer.classList.add('textRendererOutputContainer');
|
113
|
+
const rerenderButton = document.createElement('button');
|
114
|
+
rerenderButton.classList.add('rerenderButton');
|
115
|
+
rerenderButton.innerText = this.editor.localization.rerenderAsText;
|
116
|
+
const rerenderOutput = document.createElement('div');
|
117
|
+
rerenderOutput.ariaLive = 'polite';
|
118
|
+
rerenderButton.onclick = () => {
|
119
|
+
this.textRenderer.clear();
|
120
|
+
this.editor.image.render(this.textRenderer, this.editor.viewport);
|
121
|
+
rerenderOutput.innerText = this.textRenderer.getDescription();
|
122
|
+
};
|
123
|
+
textRendererOutputContainer.replaceChildren(rerenderButton, rerenderOutput);
|
124
|
+
this.editor.createHTMLOverlay(textRendererOutputContainer);
|
125
|
+
}
|
107
126
|
// Clears the drawing surfaces and otherwise prepares for a rerender.
|
108
127
|
startRerender() {
|
109
128
|
var _a;
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { TextStyle } from '../../components/Text';
|
2
|
+
import Mat33 from '../../geometry/Mat33';
|
3
|
+
import Rect2 from '../../geometry/Rect2';
|
4
|
+
import Vec3 from '../../geometry/Vec3';
|
5
|
+
import Viewport from '../../Viewport';
|
6
|
+
import { TextRendererLocalization } from '../localization';
|
7
|
+
import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
|
8
|
+
export default class TextOnlyRenderer extends AbstractRenderer {
|
9
|
+
private localizationTable;
|
10
|
+
private descriptionBuilder;
|
11
|
+
constructor(viewport: Viewport, localizationTable: TextRendererLocalization);
|
12
|
+
displaySize(): Vec3;
|
13
|
+
clear(): void;
|
14
|
+
getDescription(): string;
|
15
|
+
protected beginPath(_startPoint: Vec3): void;
|
16
|
+
protected endPath(_style: RenderingStyle): void;
|
17
|
+
protected lineTo(_point: Vec3): void;
|
18
|
+
protected moveTo(_point: Vec3): void;
|
19
|
+
protected traceCubicBezierCurve(_p1: Vec3, _p2: Vec3, _p3: Vec3): void;
|
20
|
+
protected traceQuadraticBezierCurve(_controlPoint: Vec3, _endPoint: Vec3): void;
|
21
|
+
drawText(text: string, _transform: Mat33, _style: TextStyle): void;
|
22
|
+
isTooSmallToRender(rect: Rect2): boolean;
|
23
|
+
drawPoints(..._points: Vec3[]): void;
|
24
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { Vec2 } from '../../geometry/Vec2';
|
2
|
+
import AbstractRenderer from './AbstractRenderer';
|
3
|
+
// Outputs a description of what was rendered.
|
4
|
+
export default class TextOnlyRenderer extends AbstractRenderer {
|
5
|
+
constructor(viewport, localizationTable) {
|
6
|
+
super(viewport);
|
7
|
+
this.localizationTable = localizationTable;
|
8
|
+
this.descriptionBuilder = [];
|
9
|
+
}
|
10
|
+
displaySize() {
|
11
|
+
// We don't have a graphical display, export a reasonable size.
|
12
|
+
return Vec2.of(500, 500);
|
13
|
+
}
|
14
|
+
clear() {
|
15
|
+
this.descriptionBuilder = [];
|
16
|
+
}
|
17
|
+
getDescription() {
|
18
|
+
return this.descriptionBuilder.join('\n');
|
19
|
+
}
|
20
|
+
beginPath(_startPoint) {
|
21
|
+
}
|
22
|
+
endPath(_style) {
|
23
|
+
}
|
24
|
+
lineTo(_point) {
|
25
|
+
}
|
26
|
+
moveTo(_point) {
|
27
|
+
}
|
28
|
+
traceCubicBezierCurve(_p1, _p2, _p3) {
|
29
|
+
}
|
30
|
+
traceQuadraticBezierCurve(_controlPoint, _endPoint) {
|
31
|
+
}
|
32
|
+
drawText(text, _transform, _style) {
|
33
|
+
this.descriptionBuilder.push(this.localizationTable.textNode(text));
|
34
|
+
}
|
35
|
+
isTooSmallToRender(rect) {
|
36
|
+
return rect.maxDimension < 10 / this.getSizeOfCanvasPixelOnScreen();
|
37
|
+
}
|
38
|
+
drawPoints(..._points) {
|
39
|
+
}
|
40
|
+
}
|
@@ -338,25 +338,51 @@ class TextToolWidget extends ToolbarWidget {
|
|
338
338
|
return makeTextIcon(textStyle);
|
339
339
|
}
|
340
340
|
fillDropdown(dropdown) {
|
341
|
+
const fontRow = document.createElement('div');
|
341
342
|
const colorRow = document.createElement('div');
|
343
|
+
const fontInput = document.createElement('select');
|
344
|
+
const fontLabel = document.createElement('label');
|
342
345
|
const colorInput = document.createElement('input');
|
343
346
|
const colorLabel = document.createElement('label');
|
347
|
+
const fontsInInput = new Set();
|
348
|
+
const addFontToInput = (fontName) => {
|
349
|
+
const option = document.createElement('option');
|
350
|
+
option.value = fontName;
|
351
|
+
option.textContent = fontName;
|
352
|
+
fontInput.appendChild(option);
|
353
|
+
fontsInInput.add(fontName);
|
354
|
+
};
|
355
|
+
fontLabel.innerText = this.localizationTable.fontLabel;
|
344
356
|
colorLabel.innerText = this.localizationTable.colorLabel;
|
345
357
|
colorInput.classList.add('coloris_input');
|
346
358
|
colorInput.type = 'button';
|
347
359
|
colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
|
348
360
|
colorLabel.setAttribute('for', colorInput.id);
|
361
|
+
addFontToInput('monospace');
|
362
|
+
addFontToInput('serif');
|
363
|
+
addFontToInput('sans-serif');
|
364
|
+
fontInput.id = `${toolbarCSSPrefix}-text-font-input-${TextToolWidget.idCounter++}`;
|
365
|
+
fontLabel.setAttribute('for', fontInput.id);
|
366
|
+
fontInput.onchange = () => {
|
367
|
+
this.tool.setFontFamily(fontInput.value);
|
368
|
+
};
|
349
369
|
colorInput.oninput = () => {
|
350
370
|
this.tool.setColor(Color4.fromString(colorInput.value));
|
351
371
|
};
|
352
372
|
colorRow.appendChild(colorLabel);
|
353
373
|
colorRow.appendChild(colorInput);
|
374
|
+
fontRow.appendChild(fontLabel);
|
375
|
+
fontRow.appendChild(fontInput);
|
354
376
|
this.updateDropdownInputs = () => {
|
355
377
|
const style = this.tool.getTextStyle();
|
356
378
|
colorInput.value = style.renderingStyle.fill.toHexString();
|
379
|
+
if (!fontsInInput.has(style.fontFamily)) {
|
380
|
+
addFontToInput(style.fontFamily);
|
381
|
+
}
|
382
|
+
fontInput.value = style.fontFamily;
|
357
383
|
};
|
358
384
|
this.updateDropdownInputs();
|
359
|
-
dropdown.
|
385
|
+
dropdown.replaceChildren(colorRow, fontRow);
|
360
386
|
return true;
|
361
387
|
}
|
362
388
|
}
|
@@ -125,6 +125,7 @@ export const makeTextIcon = (textStyle) => {
|
|
125
125
|
textNode.setAttribute('x', '50');
|
126
126
|
textNode.setAttribute('y', '75');
|
127
127
|
textNode.style.fontSize = '65px';
|
128
|
+
textNode.style.filter = 'drop-shadow(0px 0px 10px var(--primary-shadow-color))';
|
128
129
|
icon.appendChild(textNode);
|
129
130
|
return icon;
|
130
131
|
};
|
@@ -14,6 +14,7 @@ export default class TextTool extends BaseTool {
|
|
14
14
|
private textInputElem;
|
15
15
|
private textTargetPosition;
|
16
16
|
private textMeasuringCtx;
|
17
|
+
private textRotation;
|
17
18
|
constructor(editor: Editor, description: string, localizationTable: ToolLocalization);
|
18
19
|
private getTextAscent;
|
19
20
|
private flushInput;
|
@@ -21,6 +22,7 @@ export default class TextTool extends BaseTool {
|
|
21
22
|
private startTextInput;
|
22
23
|
setEnabled(enabled: boolean): void;
|
23
24
|
onPointerDown({ current, allPointers }: PointerEvt): boolean;
|
25
|
+
onGestureCancel(): void;
|
24
26
|
private dispatchUpdateEvent;
|
25
27
|
setFontFamily(fontFamily: string): void;
|
26
28
|
setColor(color: Color4): void;
|
@@ -58,7 +58,7 @@ export default class TextTool extends BaseTool {
|
|
58
58
|
if (content === '') {
|
59
59
|
return;
|
60
60
|
}
|
61
|
-
const textTransform = Mat33.translation(this.textTargetPosition).rightMul(Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas()));
|
61
|
+
const textTransform = Mat33.translation(this.textTargetPosition).rightMul(Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())).rightMul(Mat33.zRotation(this.textRotation));
|
62
62
|
const textComponent = new Text([content], textTransform, this.textStyle);
|
63
63
|
const action = new EditorImage.AddElementCommand(textComponent);
|
64
64
|
this.editor.dispatch(action);
|
@@ -70,7 +70,8 @@ export default class TextTool extends BaseTool {
|
|
70
70
|
(_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.remove();
|
71
71
|
return;
|
72
72
|
}
|
73
|
-
const
|
73
|
+
const viewport = this.editor.viewport;
|
74
|
+
const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
|
74
75
|
this.textInputElem.type = 'text';
|
75
76
|
this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
|
76
77
|
this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
|
@@ -80,14 +81,19 @@ export default class TextTool extends BaseTool {
|
|
80
81
|
this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
|
81
82
|
this.textInputElem.style.position = 'relative';
|
82
83
|
this.textInputElem.style.left = `${textScreenPos.x}px`;
|
84
|
+
this.textInputElem.style.top = `${textScreenPos.y}px`;
|
85
|
+
this.textInputElem.style.margin = '0';
|
86
|
+
const rotation = this.textRotation + viewport.getRotationAngle();
|
83
87
|
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
|
84
|
-
this.textInputElem.style.
|
88
|
+
this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
|
89
|
+
this.textInputElem.style.transformOrigin = 'top left';
|
85
90
|
}
|
86
91
|
startTextInput(textCanvasPos, initialText) {
|
87
92
|
this.flushInput();
|
88
93
|
this.textInputElem = document.createElement('input');
|
89
94
|
this.textInputElem.value = initialText;
|
90
95
|
this.textTargetPosition = textCanvasPos;
|
96
|
+
this.textRotation = -this.editor.viewport.getRotationAngle();
|
91
97
|
this.updateTextInput();
|
92
98
|
this.textInputElem.oninput = () => {
|
93
99
|
var _a;
|
@@ -96,15 +102,25 @@ export default class TextTool extends BaseTool {
|
|
96
102
|
}
|
97
103
|
};
|
98
104
|
this.textInputElem.onblur = () => {
|
99
|
-
|
105
|
+
// Don't remove the input within the context of a blur event handler.
|
106
|
+
// Doing so causes errors.
|
107
|
+
setTimeout(() => this.flushInput(), 0);
|
100
108
|
};
|
101
109
|
this.textInputElem.onkeyup = (evt) => {
|
110
|
+
var _a;
|
102
111
|
if (evt.key === 'Enter') {
|
103
112
|
this.flushInput();
|
113
|
+
this.editor.focus();
|
114
|
+
}
|
115
|
+
else if (evt.key === 'Escape') {
|
116
|
+
// Cancel input.
|
117
|
+
(_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.remove();
|
118
|
+
this.textInputElem = null;
|
119
|
+
this.editor.focus();
|
104
120
|
}
|
105
121
|
};
|
106
122
|
this.textEditOverlay.replaceChildren(this.textInputElem);
|
107
|
-
setTimeout(() => this.textInputElem.focus(),
|
123
|
+
setTimeout(() => { var _a; return (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.focus(); }, 0);
|
108
124
|
}
|
109
125
|
setEnabled(enabled) {
|
110
126
|
super.setEnabled(enabled);
|
@@ -123,6 +139,10 @@ export default class TextTool extends BaseTool {
|
|
123
139
|
}
|
124
140
|
return false;
|
125
141
|
}
|
142
|
+
onGestureCancel() {
|
143
|
+
this.flushInput();
|
144
|
+
this.editor.focus();
|
145
|
+
}
|
126
146
|
dispatchUpdateEvent() {
|
127
147
|
this.updateTextInput();
|
128
148
|
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
@@ -27,9 +27,16 @@
|
|
27
27
|
wheelEventsEnabled: false,
|
28
28
|
});
|
29
29
|
editor1.addToolbar();
|
30
|
+
editor1.loadFromSVG('<svg><text>Wheel events disabled.</text></svg>');
|
30
31
|
|
31
|
-
const editor2 = new jsdraw.Editor(document.body
|
32
|
+
const editor2 = new jsdraw.Editor(document.body, {
|
33
|
+
wheelEventsEnabled: 'only-if-focused',
|
34
|
+
});
|
32
35
|
editor2.addToolbar();
|
36
|
+
editor2.loadFromSVG('<svg><text>Wheel events enabled, only if focused.</text></svg>');
|
37
|
+
|
38
|
+
const editor3 = new jsdraw.Editor(document.body);
|
39
|
+
editor3.addToolbar();
|
33
40
|
</script>
|
34
41
|
</body>
|
35
42
|
</html>
|
package/package.json
CHANGED
package/src/Editor.css
CHANGED
@@ -8,6 +8,7 @@
|
|
8
8
|
--secondary-background-color: #faf;
|
9
9
|
--primary-foreground-color: black;
|
10
10
|
--secondary-foreground-color: black;
|
11
|
+
--primary-shadow-color: rgba(0, 0, 0, 0.5);
|
11
12
|
}
|
12
13
|
|
13
14
|
@media (prefers-color-scheme: dark) {
|
@@ -17,6 +18,7 @@
|
|
17
18
|
--secondary-background-color: #607;
|
18
19
|
--primary-foreground-color: white;
|
19
20
|
--secondary-foreground-color: white;
|
21
|
+
--primary-shadow-color: rgba(250, 250, 250, 0.5);
|
20
22
|
}
|
21
23
|
}
|
22
24
|
|
@@ -66,3 +68,13 @@
|
|
66
68
|
overflow: hidden;
|
67
69
|
pointer-events: none;
|
68
70
|
}
|
71
|
+
|
72
|
+
.imageEditorContainer .textRendererOutputContainer {
|
73
|
+
width: 1px;
|
74
|
+
height: 1px;
|
75
|
+
overflow: hidden;
|
76
|
+
}
|
77
|
+
|
78
|
+
.imageEditorContainer .textRendererOutputContainer:focus-within {
|
79
|
+
overflow: visible;
|
80
|
+
}
|
package/src/Editor.ts
CHANGED
@@ -29,7 +29,7 @@ export interface EditorSettings {
|
|
29
29
|
// True if touchpad/mousewheel scrolling should scroll the editor instead of the document.
|
30
30
|
// This does not include pinch-zoom events.
|
31
31
|
// Defaults to true.
|
32
|
-
wheelEventsEnabled: boolean;
|
32
|
+
wheelEventsEnabled: boolean|'only-if-focused';
|
33
33
|
}
|
34
34
|
|
35
35
|
export class Editor {
|
@@ -245,16 +245,26 @@ export class Editor {
|
|
245
245
|
ctrlKey: evt.ctrlKey,
|
246
246
|
})) {
|
247
247
|
evt.preventDefault();
|
248
|
+
} else if (evt.key === 'Escape') {
|
249
|
+
this.renderingRegion.blur();
|
248
250
|
}
|
249
251
|
});
|
250
252
|
|
251
253
|
this.container.addEventListener('wheel', evt => {
|
252
254
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
253
255
|
|
254
|
-
// Process wheel events if the ctrl key is down -- we do want to handle
|
256
|
+
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
255
257
|
// pinch-zooming.
|
256
|
-
if (!
|
257
|
-
|
258
|
+
if (!evt.ctrlKey) {
|
259
|
+
if (!this.settings.wheelEventsEnabled) {
|
260
|
+
return;
|
261
|
+
} else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
262
|
+
const focusedChild = this.container.querySelector(':focus');
|
263
|
+
|
264
|
+
if (!focusedChild) {
|
265
|
+
return;
|
266
|
+
}
|
267
|
+
}
|
258
268
|
}
|
259
269
|
|
260
270
|
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
@@ -267,7 +277,7 @@ export class Editor {
|
|
267
277
|
delta = Vec3.of(0, 0, evt.deltaY);
|
268
278
|
}
|
269
279
|
|
270
|
-
const pos = Vec2.of(evt.
|
280
|
+
const pos = Vec2.of(evt.offsetX, evt.offsetY);
|
271
281
|
if (this.toolController.dispatchInputEvent({
|
272
282
|
kind: InputEvtType.WheelEvt,
|
273
283
|
delta,
|
@@ -399,6 +409,11 @@ export class Editor {
|
|
399
409
|
this.display.getWetInkRenderer().clear();
|
400
410
|
}
|
401
411
|
|
412
|
+
// Focuses the region used for text input
|
413
|
+
public focus() {
|
414
|
+
this.renderingRegion.focus();
|
415
|
+
}
|
416
|
+
|
402
417
|
public createHTMLOverlay(overlay: HTMLElement) {
|
403
418
|
overlay.classList.add('overlay');
|
404
419
|
this.container.appendChild(overlay);
|
package/src/SVGLoader.ts
CHANGED
@@ -198,14 +198,19 @@ export default class SVGLoader implements ImageLoader {
|
|
198
198
|
}
|
199
199
|
const style: TextStyle = {
|
200
200
|
size: fontSize,
|
201
|
-
fontFamily: computedStyles.fontFamily || 'sans',
|
201
|
+
fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
|
202
202
|
renderingStyle: {
|
203
203
|
fill: Color4.fromString(computedStyles.fill)
|
204
204
|
},
|
205
205
|
};
|
206
206
|
|
207
|
+
let transformProperty = computedStyles.transform;
|
208
|
+
if (transformProperty === '' || transformProperty === 'none') {
|
209
|
+
transformProperty = elem.style.transform || 'none';
|
210
|
+
}
|
211
|
+
|
207
212
|
// Compute transform matrix
|
208
|
-
let transform = Mat33.fromCSSMatrix(
|
213
|
+
let transform = Mat33.fromCSSMatrix(transformProperty);
|
209
214
|
const supportedAttrs = [];
|
210
215
|
const elemX = elem.getAttribute('x');
|
211
216
|
const elemY = elem.getAttribute('y');
|
package/src/Viewport.ts
CHANGED
@@ -101,7 +101,7 @@ export class Viewport {
|
|
101
101
|
|
102
102
|
// Updates the transformation directly. Using ViewportTransform is preferred.
|
103
103
|
// [newTransform] should map from canvas coordinates to screen coordinates.
|
104
|
-
public resetTransform(newTransform: Mat33) {
|
104
|
+
public resetTransform(newTransform: Mat33 = Mat33.identity) {
|
105
105
|
this.transform = newTransform;
|
106
106
|
this.inverseTransform = newTransform.inverse();
|
107
107
|
this.notifier.dispatch(EditorEventType.ViewportChanged, {
|
@@ -181,19 +181,19 @@ export class Viewport {
|
|
181
181
|
throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
|
182
182
|
}
|
183
183
|
|
184
|
-
// Try to move the selection within the center
|
184
|
+
// Try to move the selection within the center 4/5ths of the viewport.
|
185
185
|
const recomputeTargetRect = () => {
|
186
186
|
// transform transforms objects on the canvas. As such, we need to invert it
|
187
187
|
// to transform the viewport.
|
188
188
|
const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
|
189
|
-
return visibleRect.transformedBoundingBox(Mat33.scaling2D(
|
189
|
+
return visibleRect.transformedBoundingBox(Mat33.scaling2D(4/5, visibleRect.center));
|
190
190
|
};
|
191
191
|
|
192
192
|
let targetRect = recomputeTargetRect();
|
193
193
|
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
|
194
194
|
|
195
|
-
// Ensure that toMakeVisible is at least 1/
|
196
|
-
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension <
|
195
|
+
// Ensure that toMakeVisible is at least 1/3rd of the visible region.
|
196
|
+
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 1/3;
|
197
197
|
|
198
198
|
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
|
199
199
|
// If larger than the target, ensure that the longest axis is visible.
|