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.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +21 -12
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +2 -1
  5. package/dist/src/Editor.js +19 -4
  6. package/dist/src/SVGLoader.js +6 -2
  7. package/dist/src/Viewport.d.ts +1 -1
  8. package/dist/src/Viewport.js +5 -5
  9. package/dist/src/components/Text.js +3 -1
  10. package/dist/src/localization.d.ts +2 -1
  11. package/dist/src/localization.js +2 -1
  12. package/dist/src/rendering/Display.d.ts +2 -0
  13. package/dist/src/rendering/Display.js +19 -0
  14. package/dist/src/rendering/localization.d.ts +5 -0
  15. package/dist/src/rendering/localization.js +4 -0
  16. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +24 -0
  17. package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
  18. package/dist/src/toolbar/HTMLToolbar.js +27 -1
  19. package/dist/src/toolbar/icons.js +1 -0
  20. package/dist/src/toolbar/localization.d.ts +1 -0
  21. package/dist/src/toolbar/localization.js +1 -0
  22. package/dist/src/tools/TextTool.d.ts +2 -0
  23. package/dist/src/tools/TextTool.js +25 -5
  24. package/dist-test/test-dist-bundle.html +8 -1
  25. package/package.json +1 -1
  26. package/src/Editor.css +12 -0
  27. package/src/Editor.ts +20 -5
  28. package/src/SVGLoader.ts +7 -2
  29. package/src/Viewport.ts +5 -5
  30. package/src/components/Text.ts +5 -1
  31. package/src/localization.ts +3 -1
  32. package/src/rendering/Display.ts +26 -0
  33. package/src/rendering/localization.ts +10 -0
  34. package/src/rendering/renderers/TextOnlyRenderer.ts +51 -0
  35. package/src/toolbar/HTMLToolbar.ts +34 -2
  36. package/src/toolbar/icons.ts +1 -0
  37. package/src/toolbar/localization.ts +2 -0
  38. package/src/toolbar/toolbar.css +6 -3
  39. package/src/tools/TextTool.ts +27 -4
@@ -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
  };
@@ -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 (!this.settings.wheelEventsEnabled && !evt.ctrlKey) {
192
- return;
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.clientX, evt.clientY);
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);
@@ -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(computedStyles.transform);
175
+ let transform = Mat33.fromCSSMatrix(transformProperty);
172
176
  const supportedAttrs = [];
173
177
  const elemX = elem.getAttribute('x');
174
178
  const elemY = elem.getAttribute('y');
@@ -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: Mat33): void;
29
+ resetTransform(newTransform?: Mat33): void;
30
30
  get screenToCanvasTransform(): Mat33;
31
31
  get canvasToScreenTransform(): Mat33;
32
32
  getResolution(): Vec2;
@@ -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 2/3rds of the viewport.
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(2 / 3, visibleRect.center));
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/8th of the visible region.
106
- const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
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
- `"${style.fontFamily.replace(/["]/g, '\\"')}"`,
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;
@@ -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,5 @@
1
+ export interface TextRendererLocalization {
2
+ textNode(content: string): string;
3
+ rerenderAsText: string;
4
+ }
5
+ export declare const defaultTextRendererLocalization: TextRendererLocalization;
@@ -0,0 +1,4 @@
1
+ export const defaultTextRendererLocalization = {
2
+ textNode: (content) => `Text: ${content}`,
3
+ rerenderAsText: 'Re-render as text',
4
+ };
@@ -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.appendChild(colorRow);
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
  };
@@ -1,4 +1,5 @@
1
1
  export interface ToolbarLocalization {
2
+ fontLabel: string;
2
3
  anyDevicePanning: string;
3
4
  touchPanning: string;
4
5
  outlinedRectanglePen: string;
@@ -5,6 +5,7 @@ export const defaultToolbarLocalization = {
5
5
  handTool: 'Pan',
6
6
  thicknessLabel: 'Thickness: ',
7
7
  colorLabel: 'Color: ',
8
+ fontLabel: 'Font: ',
8
9
  resizeImageToSelection: 'Resize image to selection',
9
10
  deleteSelection: 'Delete selection',
10
11
  undo: 'Undo',
@@ -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 textScreenPos = this.editor.viewport.canvasToScreen(this.textTargetPosition);
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.top = `${textScreenPos.y - ascent}px`;
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
- this.flushInput();
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(), 100);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "dist/src/Editor.js",
6
6
  "types": "dist/src/Editor.d.ts",
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 (!this.settings.wheelEventsEnabled && !evt.ctrlKey) {
257
- return;
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.clientX, evt.clientY);
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(computedStyles.transform);
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 2/3rds of the viewport.
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(2 / 3, visibleRect.center));
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/8th of the visible region.
196
- const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
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.