js-draw 0.1.2 → 0.1.5

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 (79) hide show
  1. package/CHANGELOG.md +14 -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 +20 -6
  6. package/dist/src/SVGLoader.d.ts +8 -0
  7. package/dist/src/SVGLoader.js +105 -6
  8. package/dist/src/Viewport.d.ts +1 -1
  9. package/dist/src/Viewport.js +5 -5
  10. package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
  11. package/dist/src/components/Text.d.ts +30 -0
  12. package/dist/src/components/Text.js +111 -0
  13. package/dist/src/components/localization.d.ts +1 -0
  14. package/dist/src/components/localization.js +1 -0
  15. package/dist/src/geometry/Mat33.d.ts +1 -0
  16. package/dist/src/geometry/Mat33.js +30 -0
  17. package/dist/src/geometry/Path.js +8 -1
  18. package/dist/src/geometry/Rect2.d.ts +2 -0
  19. package/dist/src/geometry/Rect2.js +6 -0
  20. package/dist/src/localization.d.ts +2 -1
  21. package/dist/src/localization.js +2 -1
  22. package/dist/src/rendering/Display.d.ts +2 -0
  23. package/dist/src/rendering/Display.js +19 -0
  24. package/dist/src/rendering/localization.d.ts +5 -0
  25. package/dist/src/rendering/localization.js +4 -0
  26. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +5 -0
  27. package/dist/src/rendering/renderers/AbstractRenderer.js +12 -0
  28. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
  29. package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
  30. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
  31. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  32. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -0
  33. package/dist/src/rendering/renderers/SVGRenderer.js +30 -1
  34. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +24 -0
  35. package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
  36. package/dist/src/testing/loadExpectExtensions.js +1 -4
  37. package/dist/src/toolbar/HTMLToolbar.js +78 -1
  38. package/dist/src/toolbar/icons.d.ts +2 -0
  39. package/dist/src/toolbar/icons.js +18 -0
  40. package/dist/src/toolbar/localization.d.ts +1 -0
  41. package/dist/src/toolbar/localization.js +1 -0
  42. package/dist/src/tools/SelectionTool.js +1 -1
  43. package/dist/src/tools/TextTool.d.ts +31 -0
  44. package/dist/src/tools/TextTool.js +174 -0
  45. package/dist/src/tools/ToolController.d.ts +2 -1
  46. package/dist/src/tools/ToolController.js +4 -1
  47. package/dist/src/tools/localization.d.ts +3 -1
  48. package/dist/src/tools/localization.js +3 -1
  49. package/dist-test/test-dist-bundle.html +8 -1
  50. package/package.json +1 -1
  51. package/src/Editor.css +12 -0
  52. package/src/Editor.ts +22 -7
  53. package/src/SVGLoader.ts +124 -6
  54. package/src/Viewport.ts +5 -5
  55. package/src/components/SVGGlobalAttributesObject.ts +0 -1
  56. package/src/components/Text.ts +140 -0
  57. package/src/components/localization.ts +2 -0
  58. package/src/geometry/Mat33.test.ts +44 -0
  59. package/src/geometry/Mat33.ts +41 -0
  60. package/src/geometry/Path.toString.test.ts +7 -3
  61. package/src/geometry/Path.ts +11 -1
  62. package/src/geometry/Rect2.ts +8 -0
  63. package/src/localization.ts +3 -1
  64. package/src/rendering/Display.ts +26 -0
  65. package/src/rendering/localization.ts +10 -0
  66. package/src/rendering/renderers/AbstractRenderer.ts +16 -0
  67. package/src/rendering/renderers/CanvasRenderer.ts +34 -10
  68. package/src/rendering/renderers/DummyRenderer.ts +8 -0
  69. package/src/rendering/renderers/SVGRenderer.ts +36 -1
  70. package/src/rendering/renderers/TextOnlyRenderer.ts +51 -0
  71. package/src/testing/loadExpectExtensions.ts +1 -4
  72. package/src/toolbar/HTMLToolbar.ts +96 -1
  73. package/src/toolbar/icons.ts +24 -0
  74. package/src/toolbar/localization.ts +2 -0
  75. package/src/toolbar/toolbar.css +6 -3
  76. package/src/tools/SelectionTool.ts +1 -1
  77. package/src/tools/TextTool.ts +229 -0
  78. package/src/tools/ToolController.ts +4 -0
  79. package/src/tools/localization.ts +7 -2
@@ -0,0 +1,174 @@
1
+ import Color4 from '../Color4';
2
+ import Text from '../components/Text';
3
+ import EditorImage from '../EditorImage';
4
+ import Mat33 from '../geometry/Mat33';
5
+ import { PointerDevice } from '../Pointer';
6
+ import { EditorEventType } from '../types';
7
+ import BaseTool from './BaseTool';
8
+ import { ToolType } from './ToolController';
9
+ const overlayCssClass = 'textEditorOverlay';
10
+ export default class TextTool extends BaseTool {
11
+ constructor(editor, description, localizationTable) {
12
+ super(editor.notifier, description);
13
+ this.editor = editor;
14
+ this.localizationTable = localizationTable;
15
+ this.kind = ToolType.Text;
16
+ this.textInputElem = null;
17
+ this.textTargetPosition = null;
18
+ this.textMeasuringCtx = null;
19
+ this.textStyle = {
20
+ size: 32,
21
+ fontFamily: 'sans-serif',
22
+ renderingStyle: {
23
+ fill: Color4.purple,
24
+ },
25
+ };
26
+ this.textEditOverlay = document.createElement('div');
27
+ this.textEditOverlay.classList.add(overlayCssClass);
28
+ this.editor.addStyleSheet(`
29
+ .${overlayCssClass} {
30
+ height: 0;
31
+ overflow: visible;
32
+ }
33
+
34
+ .${overlayCssClass} input {
35
+ background-color: rgba(0, 0, 0, 0);
36
+ border: none;
37
+ padding: 0;
38
+ }
39
+ `);
40
+ this.editor.createHTMLOverlay(this.textEditOverlay);
41
+ this.editor.notifier.on(EditorEventType.ViewportChanged, () => this.updateTextInput());
42
+ }
43
+ getTextAscent(text, style) {
44
+ var _a;
45
+ (_a = this.textMeasuringCtx) !== null && _a !== void 0 ? _a : (this.textMeasuringCtx = document.createElement('canvas').getContext('2d'));
46
+ if (this.textMeasuringCtx) {
47
+ Text.applyTextStyles(this.textMeasuringCtx, style);
48
+ return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;
49
+ }
50
+ // Estimate
51
+ return style.size * 2 / 3;
52
+ }
53
+ flushInput() {
54
+ if (this.textInputElem && this.textTargetPosition) {
55
+ const content = this.textInputElem.value;
56
+ this.textInputElem.remove();
57
+ this.textInputElem = null;
58
+ if (content === '') {
59
+ return;
60
+ }
61
+ const textTransform = Mat33.translation(this.textTargetPosition).rightMul(Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())).rightMul(Mat33.zRotation(this.textRotation));
62
+ const textComponent = new Text([content], textTransform, this.textStyle);
63
+ const action = new EditorImage.AddElementCommand(textComponent);
64
+ this.editor.dispatch(action);
65
+ }
66
+ }
67
+ updateTextInput() {
68
+ var _a, _b, _c;
69
+ if (!this.textInputElem || !this.textTargetPosition) {
70
+ (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.remove();
71
+ return;
72
+ }
73
+ const viewport = this.editor.viewport;
74
+ const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
75
+ this.textInputElem.type = 'text';
76
+ this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
77
+ this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
78
+ this.textInputElem.style.fontVariant = (_b = this.textStyle.fontVariant) !== null && _b !== void 0 ? _b : '';
79
+ this.textInputElem.style.fontWeight = (_c = this.textStyle.fontWeight) !== null && _c !== void 0 ? _c : '';
80
+ this.textInputElem.style.fontSize = `${this.textStyle.size}px`;
81
+ this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
82
+ this.textInputElem.style.position = 'relative';
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();
87
+ const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
88
+ this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
89
+ this.textInputElem.style.transformOrigin = 'top left';
90
+ }
91
+ startTextInput(textCanvasPos, initialText) {
92
+ this.flushInput();
93
+ this.textInputElem = document.createElement('input');
94
+ this.textInputElem.value = initialText;
95
+ this.textTargetPosition = textCanvasPos;
96
+ this.textRotation = -this.editor.viewport.getRotationAngle();
97
+ this.updateTextInput();
98
+ this.textInputElem.oninput = () => {
99
+ var _a;
100
+ if (this.textInputElem) {
101
+ this.textInputElem.size = ((_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.value.length) || 10;
102
+ }
103
+ };
104
+ this.textInputElem.onblur = () => {
105
+ // Don't remove the input within the context of a blur event handler.
106
+ // Doing so causes errors.
107
+ setTimeout(() => this.flushInput(), 0);
108
+ };
109
+ this.textInputElem.onkeyup = (evt) => {
110
+ var _a;
111
+ if (evt.key === 'Enter') {
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();
120
+ }
121
+ };
122
+ this.textEditOverlay.replaceChildren(this.textInputElem);
123
+ setTimeout(() => { var _a; return (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.focus(); }, 0);
124
+ }
125
+ setEnabled(enabled) {
126
+ super.setEnabled(enabled);
127
+ if (!enabled) {
128
+ this.flushInput();
129
+ }
130
+ this.textEditOverlay.style.display = enabled ? 'block' : 'none';
131
+ }
132
+ onPointerDown({ current, allPointers }) {
133
+ if (current.device === PointerDevice.Eraser) {
134
+ return false;
135
+ }
136
+ if (allPointers.length === 1) {
137
+ this.startTextInput(current.canvasPos, '');
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+ onGestureCancel() {
143
+ this.flushInput();
144
+ this.editor.focus();
145
+ }
146
+ dispatchUpdateEvent() {
147
+ this.updateTextInput();
148
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
149
+ kind: EditorEventType.ToolUpdated,
150
+ tool: this,
151
+ });
152
+ }
153
+ setFontFamily(fontFamily) {
154
+ if (fontFamily !== this.textStyle.fontFamily) {
155
+ this.textStyle = Object.assign(Object.assign({}, this.textStyle), { fontFamily: fontFamily });
156
+ this.dispatchUpdateEvent();
157
+ }
158
+ }
159
+ setColor(color) {
160
+ if (!color.eq(this.textStyle.renderingStyle.fill)) {
161
+ this.textStyle = Object.assign(Object.assign({}, this.textStyle), { renderingStyle: Object.assign(Object.assign({}, this.textStyle.renderingStyle), { fill: color }) });
162
+ this.dispatchUpdateEvent();
163
+ }
164
+ }
165
+ setFontSize(size) {
166
+ if (size !== this.textStyle.size) {
167
+ this.textStyle = Object.assign(Object.assign({}, this.textStyle), { size });
168
+ this.dispatchUpdateEvent();
169
+ }
170
+ }
171
+ getTextStyle() {
172
+ return this.textStyle;
173
+ }
174
+ }
@@ -7,7 +7,8 @@ export declare enum ToolType {
7
7
  Selection = 1,
8
8
  Eraser = 2,
9
9
  PanZoom = 3,
10
- UndoRedoShortcut = 4
10
+ Text = 4,
11
+ UndoRedoShortcut = 5
11
12
  }
12
13
  export default class ToolController {
13
14
  private tools;
@@ -6,13 +6,15 @@ import Eraser from './Eraser';
6
6
  import SelectionTool from './SelectionTool';
7
7
  import Color4 from '../Color4';
8
8
  import UndoRedoShortcut from './UndoRedoShortcut';
9
+ import TextTool from './TextTool';
9
10
  export var ToolType;
10
11
  (function (ToolType) {
11
12
  ToolType[ToolType["Pen"] = 0] = "Pen";
12
13
  ToolType[ToolType["Selection"] = 1] = "Selection";
13
14
  ToolType[ToolType["Eraser"] = 2] = "Eraser";
14
15
  ToolType[ToolType["PanZoom"] = 3] = "PanZoom";
15
- ToolType[ToolType["UndoRedoShortcut"] = 4] = "UndoRedoShortcut";
16
+ ToolType[ToolType["Text"] = 4] = "Text";
17
+ ToolType[ToolType["UndoRedoShortcut"] = 5] = "UndoRedoShortcut";
16
18
  })(ToolType || (ToolType = {}));
17
19
  export default class ToolController {
18
20
  constructor(editor, localization) {
@@ -27,6 +29,7 @@ export default class ToolController {
27
29
  new Pen(editor, localization.penTool(2), { color: Color4.clay, thickness: 8 }),
28
30
  // Highlighter-like pen with width=64
29
31
  new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
32
+ new TextTool(editor, localization.textTool, localization),
30
33
  ];
31
34
  this.tools = [
32
35
  panZoomTool,
@@ -1,11 +1,13 @@
1
1
  export interface ToolLocalization {
2
- RightClickDragPanTool: string;
2
+ rightClickDragPanTool: string;
3
3
  penTool: (penId: number) => string;
4
4
  selectionTool: string;
5
5
  eraserTool: string;
6
6
  touchPanTool: string;
7
7
  twoFingerPanZoomTool: string;
8
8
  undoRedoTool: string;
9
+ textTool: string;
10
+ enterTextToInsert: string;
9
11
  toolEnabledAnnouncement: (toolName: string) => string;
10
12
  toolDisabledAnnouncement: (toolName: string) => string;
11
13
  }
@@ -5,7 +5,9 @@ export const defaultToolLocalization = {
5
5
  touchPanTool: 'Touch Panning',
6
6
  twoFingerPanZoomTool: 'Panning and Zooming',
7
7
  undoRedoTool: 'Undo/Redo',
8
- RightClickDragPanTool: 'Right-click drag',
8
+ rightClickDragPanTool: 'Right-click drag',
9
+ textTool: 'Text',
10
+ enterTextToInsert: 'Text to insert',
9
11
  toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
10
12
  toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
11
13
  };
@@ -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.2",
3
+ "version": "0.1.5",
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) {
@@ -374,9 +384,11 @@ export class Editor {
374
384
  // Draw a rectangle around the region that will be visible on save
375
385
  const renderer = this.display.getDryInkRenderer();
376
386
 
387
+ this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
388
+
377
389
  if (showImageBounds) {
378
390
  const exportRectFill = { fill: Color4.fromHex('#44444455') };
379
- const exportRectStrokeWidth = 12;
391
+ const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
380
392
  renderer.drawRect(
381
393
  this.importExportViewport.visibleRect,
382
394
  exportRectStrokeWidth,
@@ -384,8 +396,6 @@ export class Editor {
384
396
  );
385
397
  }
386
398
 
387
- //this.image.render(renderer, this.viewport);
388
- this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
389
399
  this.rerenderQueued = false;
390
400
  }
391
401
 
@@ -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
@@ -2,9 +2,12 @@ import Color4 from './Color4';
2
2
  import AbstractComponent from './components/AbstractComponent';
3
3
  import Stroke from './components/Stroke';
4
4
  import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
5
+ import Text, { TextStyle } from './components/Text';
5
6
  import UnknownSVGObject from './components/UnknownSVGObject';
7
+ import Mat33 from './geometry/Mat33';
6
8
  import Path from './geometry/Path';
7
9
  import Rect2 from './geometry/Rect2';
10
+ import { Vec2 } from './geometry/Vec2';
8
11
  import { RenderablePathSpec, RenderingStyle } from './rendering/renderers/AbstractRenderer';
9
12
  import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
10
13
 
@@ -15,10 +18,14 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
15
18
 
16
19
  // Key to retrieve unrecognised attributes from an AbstractComponent
17
20
  export const svgAttributesDataKey = 'svgAttrs';
21
+ export const svgStyleAttributesDataKey = 'svgStyleAttrs';
18
22
 
19
23
  // [key, value]
20
24
  export type SVGLoaderUnknownAttribute = [ string, string ];
21
25
 
26
+ // [key, value, priority]
27
+ export type SVGLoaderUnknownStyleAttribute = { key: string, value: string, priority?: string };
28
+
22
29
  export default class SVGLoader implements ImageLoader {
23
30
  private onAddComponent: ComponentAddedListener|null = null;
24
31
  private onProgress: OnProgressListener|null = null;
@@ -97,10 +104,11 @@ export default class SVGLoader implements ImageLoader {
97
104
  private attachUnrecognisedAttrs(
98
105
  elem: AbstractComponent,
99
106
  node: SVGElement,
100
- supportedAttrs: Set<string>
107
+ supportedAttrs: Set<string>,
108
+ supportedStyleAttrs?: Set<string>
101
109
  ) {
102
110
  for (const attr of node.getAttributeNames()) {
103
- if (supportedAttrs.has(attr)) {
111
+ if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
104
112
  continue;
105
113
  }
106
114
 
@@ -108,6 +116,27 @@ export default class SVGLoader implements ImageLoader {
108
116
  [ attr, node.getAttribute(attr) ] as SVGLoaderUnknownAttribute,
109
117
  );
110
118
  }
119
+
120
+ if (supportedStyleAttrs) {
121
+ for (const attr of node.style) {
122
+ if (attr === '' || !attr) {
123
+ continue;
124
+ }
125
+
126
+ if (supportedStyleAttrs.has(attr)) {
127
+ continue;
128
+ }
129
+
130
+ // TODO: Do we need special logic for !important properties?
131
+ elem.attachLoadSaveData(svgStyleAttributesDataKey,
132
+ {
133
+ key: attr,
134
+ value: node.style.getPropertyValue(attr),
135
+ priority: node.style.getPropertyPriority(attr)
136
+ } as SVGLoaderUnknownStyleAttribute
137
+ );
138
+ }
139
+ }
111
140
  }
112
141
 
113
142
  // Adds a stroke with a single path
@@ -115,9 +144,14 @@ export default class SVGLoader implements ImageLoader {
115
144
  let elem: AbstractComponent;
116
145
  try {
117
146
  const strokeData = this.strokeDataFromElem(node);
147
+
118
148
  elem = new Stroke(strokeData);
149
+
150
+ const supportedStyleAttrs = [ 'stroke', 'fill', 'stroke-width' ];
119
151
  this.attachUnrecognisedAttrs(
120
- elem, node, new Set([ 'stroke', 'fill', 'stroke-width', 'd' ]),
152
+ elem, node,
153
+ new Set([ ...supportedStyleAttrs, 'd' ]),
154
+ new Set(supportedStyleAttrs)
121
155
  );
122
156
  } catch (e) {
123
157
  console.error(
@@ -131,6 +165,80 @@ export default class SVGLoader implements ImageLoader {
131
165
  this.onAddComponent?.(elem);
132
166
  }
133
167
 
168
+ private makeText(elem: SVGTextElement|SVGTSpanElement): Text {
169
+ const contentList: Array<Text|string> = [];
170
+ for (const child of elem.childNodes) {
171
+ if (child.nodeType === Node.TEXT_NODE) {
172
+ contentList.push(child.nodeValue ?? '');
173
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
174
+ const subElem = child as SVGElement;
175
+ if (subElem.tagName.toLowerCase() === 'tspan') {
176
+ contentList.push(this.makeText(subElem as SVGTSpanElement));
177
+ } else {
178
+ throw new Error(`Unrecognized text child element: ${subElem}`);
179
+ }
180
+ } else {
181
+ throw new Error(`Unrecognized text child node: ${child}.`);
182
+ }
183
+ }
184
+
185
+ // Compute styles.
186
+ const computedStyles = window.getComputedStyle(elem);
187
+ const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
188
+
189
+ const supportedStyleAttrs = [
190
+ 'fontFamily',
191
+ 'fill',
192
+ 'transform'
193
+ ];
194
+ let fontSize = 12;
195
+ if (fontSizeMatch) {
196
+ supportedStyleAttrs.push('fontSize');
197
+ fontSize = parseFloat(fontSizeMatch[1]);
198
+ }
199
+ const style: TextStyle = {
200
+ size: fontSize,
201
+ fontFamily: computedStyles.fontFamily || 'sans-serif',
202
+ renderingStyle: {
203
+ fill: Color4.fromString(computedStyles.fill)
204
+ },
205
+ };
206
+
207
+ // Compute transform matrix
208
+ let transform = Mat33.fromCSSMatrix(computedStyles.transform);
209
+ const supportedAttrs = [];
210
+ const elemX = elem.getAttribute('x');
211
+ const elemY = elem.getAttribute('y');
212
+ if (elemX && elemY) {
213
+ const x = parseFloat(elemX);
214
+ const y = parseFloat(elemY);
215
+ if (!isNaN(x) && !isNaN(y)) {
216
+ supportedAttrs.push('x', 'y');
217
+ transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
218
+ }
219
+ }
220
+
221
+ const result = new Text(contentList, transform, style);
222
+ this.attachUnrecognisedAttrs(
223
+ result,
224
+ elem,
225
+ new Set(supportedAttrs),
226
+ new Set(supportedStyleAttrs)
227
+ );
228
+
229
+ return result;
230
+ }
231
+
232
+ private addText(elem: SVGTextElement|SVGTSpanElement) {
233
+ try {
234
+ const textElem = this.makeText(elem);
235
+ this.onAddComponent?.(textElem);
236
+ } catch (e) {
237
+ console.error('Invalid text object in node', elem, '. Adding as an unknown object. Error:', e);
238
+ this.addUnknownNode(elem);
239
+ }
240
+ }
241
+
134
242
  private addUnknownNode(node: SVGElement) {
135
243
  const component = new UnknownSVGObject(node);
136
244
  this.onAddComponent?.(component);
@@ -142,13 +250,14 @@ export default class SVGLoader implements ImageLoader {
142
250
  return;
143
251
  }
144
252
 
145
- const components = viewBoxAttr.split(/[ \t,]/);
253
+ const components = viewBoxAttr.split(/[ \t\n,]+/);
146
254
  const x = parseFloat(components[0]);
147
255
  const y = parseFloat(components[1]);
148
256
  const width = parseFloat(components[2]);
149
257
  const height = parseFloat(components[3]);
150
258
 
151
259
  if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
260
+ console.warn(`node ${node} has an unparsable viewbox. Viewbox: ${viewBoxAttr}. Match: ${components}.`);
152
261
  return;
153
262
  }
154
263
 
@@ -162,6 +271,7 @@ export default class SVGLoader implements ImageLoader {
162
271
 
163
272
  private async visit(node: Element) {
164
273
  this.totalToProcess += node.childElementCount;
274
+ let visitChildren = true;
165
275
 
166
276
  switch (node.tagName.toLowerCase()) {
167
277
  case 'g':
@@ -170,6 +280,10 @@ export default class SVGLoader implements ImageLoader {
170
280
  case 'path':
171
281
  this.addPath(node as SVGPathElement);
172
282
  break;
283
+ case 'text':
284
+ this.addText(node as SVGTextElement);
285
+ visitChildren = false;
286
+ break;
173
287
  case 'svg':
174
288
  this.updateViewBox(node as SVGSVGElement);
175
289
  this.updateSVGAttrs(node as SVGSVGElement);
@@ -184,8 +298,10 @@ export default class SVGLoader implements ImageLoader {
184
298
  return;
185
299
  }
186
300
 
187
- for (const child of node.children) {
188
- await this.visit(child);
301
+ if (visitChildren) {
302
+ for (const child of node.children) {
303
+ await this.visit(child);
304
+ }
189
305
  }
190
306
 
191
307
  this.processedCount ++;
@@ -265,8 +381,10 @@ export default class SVGLoader implements ImageLoader {
265
381
  'http://www.w3.org/2000/svg', 'svg'
266
382
  );
267
383
  svgElem.innerHTML = text;
384
+ sandboxDoc.body.appendChild(svgElem);
268
385
 
269
386
  return new SVGLoader(svgElem, () => {
387
+ svgElem.remove();
270
388
  sandbox.remove();
271
389
  });
272
390
  }
package/src/Viewport.ts CHANGED
@@ -170,7 +170,7 @@ export class Viewport {
170
170
  // Returns a Command that transforms the view such that [rect] is visible, and perhaps
171
171
  // centered in the viewport.
172
172
  // Returns null if no transformation is necessary
173
- public zoomTo(toMakeVisible: Rect2): Command {
173
+ public zoomTo(toMakeVisible: Rect2, allowZoomIn: boolean = true, allowZoomOut: boolean = true): Command {
174
174
  let transform = Mat33.identity;
175
175
 
176
176
  if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
@@ -181,21 +181,21 @@ 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 3/4ths 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(3 / 4, 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
195
  // Ensure that toMakeVisible is at least 1/8th of the visible region.
196
- const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
196
+ const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.25;
197
197
 
198
- if (largerThanTarget || muchSmallerThanTarget) {
198
+ if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
199
199
  // If larger than the target, ensure that the longest axis is visible.
200
200
  // If smaller, shrink the visible rectangle as much as possible
201
201
  const multiplier = (largerThanTarget ? Math.max : Math.min)(
@@ -20,7 +20,6 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
20
20
  return;
21
21
  }
22
22
 
23
- console.log('Rendering to SVG.', this.attrs);
24
23
  for (const [ attr, value ] of this.attrs) {
25
24
  canvas.setRootSVGAttribute(attr, value);
26
25
  }