js-draw 0.1.1 → 0.1.4

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 (86) 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 +24 -6
  6. package/dist/src/EditorImage.js +3 -0
  7. package/dist/src/Pointer.d.ts +3 -2
  8. package/dist/src/Pointer.js +12 -3
  9. package/dist/src/SVGLoader.d.ts +11 -0
  10. package/dist/src/SVGLoader.js +113 -4
  11. package/dist/src/Viewport.d.ts +1 -1
  12. package/dist/src/Viewport.js +12 -2
  13. package/dist/src/components/AbstractComponent.d.ts +6 -0
  14. package/dist/src/components/AbstractComponent.js +11 -0
  15. package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
  16. package/dist/src/components/Stroke.js +1 -1
  17. package/dist/src/components/Text.d.ts +30 -0
  18. package/dist/src/components/Text.js +111 -0
  19. package/dist/src/components/localization.d.ts +1 -0
  20. package/dist/src/components/localization.js +1 -0
  21. package/dist/src/geometry/Mat33.d.ts +1 -0
  22. package/dist/src/geometry/Mat33.js +30 -0
  23. package/dist/src/geometry/Path.js +105 -67
  24. package/dist/src/geometry/Rect2.d.ts +2 -0
  25. package/dist/src/geometry/Rect2.js +6 -0
  26. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
  27. package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
  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 +6 -2
  33. package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
  34. package/dist/src/testing/loadExpectExtensions.js +1 -4
  35. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  36. package/dist/src/toolbar/HTMLToolbar.js +242 -154
  37. package/dist/src/toolbar/icons.d.ts +12 -0
  38. package/dist/src/toolbar/icons.js +198 -0
  39. package/dist/src/toolbar/localization.d.ts +5 -1
  40. package/dist/src/toolbar/localization.js +5 -1
  41. package/dist/src/toolbar/types.d.ts +4 -0
  42. package/dist/src/tools/PanZoom.d.ts +9 -6
  43. package/dist/src/tools/PanZoom.js +30 -21
  44. package/dist/src/tools/Pen.js +8 -3
  45. package/dist/src/tools/SelectionTool.js +1 -1
  46. package/dist/src/tools/TextTool.d.ts +30 -0
  47. package/dist/src/tools/TextTool.js +173 -0
  48. package/dist/src/tools/ToolController.d.ts +5 -5
  49. package/dist/src/tools/ToolController.js +10 -9
  50. package/dist/src/tools/localization.d.ts +3 -0
  51. package/dist/src/tools/localization.js +3 -0
  52. package/dist-test/test-dist-bundle.html +8 -1
  53. package/package.json +1 -1
  54. package/src/Editor.css +2 -0
  55. package/src/Editor.ts +26 -7
  56. package/src/EditorImage.ts +4 -0
  57. package/src/Pointer.ts +13 -4
  58. package/src/SVGLoader.ts +146 -5
  59. package/src/Viewport.ts +15 -3
  60. package/src/components/AbstractComponent.ts +16 -1
  61. package/src/components/SVGGlobalAttributesObject.ts +0 -1
  62. package/src/components/Stroke.ts +1 -1
  63. package/src/components/Text.ts +140 -0
  64. package/src/components/localization.ts +2 -0
  65. package/src/geometry/Mat33.test.ts +44 -0
  66. package/src/geometry/Mat33.ts +41 -0
  67. package/src/geometry/Path.fromString.test.ts +94 -4
  68. package/src/geometry/Path.toString.test.ts +7 -3
  69. package/src/geometry/Path.ts +110 -68
  70. package/src/geometry/Rect2.ts +8 -0
  71. package/src/rendering/renderers/AbstractRenderer.ts +18 -1
  72. package/src/rendering/renderers/CanvasRenderer.ts +34 -10
  73. package/src/rendering/renderers/DummyRenderer.ts +8 -0
  74. package/src/rendering/renderers/SVGRenderer.ts +57 -10
  75. package/src/testing/loadExpectExtensions.ts +1 -4
  76. package/src/toolbar/HTMLToolbar.ts +294 -170
  77. package/src/toolbar/icons.ts +227 -0
  78. package/src/toolbar/localization.ts +11 -2
  79. package/src/toolbar/toolbar.css +27 -11
  80. package/src/toolbar/types.ts +5 -0
  81. package/src/tools/PanZoom.ts +37 -27
  82. package/src/tools/Pen.ts +7 -3
  83. package/src/tools/SelectionTool.ts +1 -1
  84. package/src/tools/TextTool.ts +225 -0
  85. package/src/tools/ToolController.ts +7 -5
  86. package/src/tools/localization.ts +7 -0
@@ -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
  };
@@ -115,6 +115,10 @@ export class Editor {
115
115
  // May be required to prevent text selection on iOS/Safari:
116
116
  // See https://stackoverflow.com/a/70992717/17055750
117
117
  this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
118
+ this.renderingRegion.addEventListener('contextmenu', evt => {
119
+ // Don't show a context menu
120
+ evt.preventDefault();
121
+ });
118
122
  this.renderingRegion.addEventListener('pointerdown', evt => {
119
123
  const pointer = Pointer.ofEvent(evt, true, this.viewport);
120
124
  pointers[pointer.id] = pointer;
@@ -179,13 +183,24 @@ export class Editor {
179
183
  })) {
180
184
  evt.preventDefault();
181
185
  }
186
+ else if (evt.key === 'Escape') {
187
+ this.renderingRegion.blur();
188
+ }
182
189
  });
183
190
  this.container.addEventListener('wheel', evt => {
184
191
  let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
185
- // 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
186
193
  // pinch-zooming.
187
- if (!this.settings.wheelEventsEnabled && !evt.ctrlKey) {
188
- 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
+ }
189
204
  }
190
205
  if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
191
206
  delta = delta.times(15);
@@ -277,13 +292,12 @@ export class Editor {
277
292
  this.display.startRerender();
278
293
  // Draw a rectangle around the region that will be visible on save
279
294
  const renderer = this.display.getDryInkRenderer();
295
+ this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
280
296
  if (showImageBounds) {
281
297
  const exportRectFill = { fill: Color4.fromHex('#44444455') };
282
- const exportRectStrokeWidth = 12;
298
+ const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
283
299
  renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
284
300
  }
285
- //this.image.render(renderer, this.viewport);
286
- this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
287
301
  this.rerenderQueued = false;
288
302
  }
289
303
  drawWetInk(...path) {
@@ -294,6 +308,10 @@ export class Editor {
294
308
  clearWetInk() {
295
309
  this.display.getWetInkRenderer().clear();
296
310
  }
311
+ // Focuses the region used for text input
312
+ focus() {
313
+ this.renderingRegion.focus();
314
+ }
297
315
  createHTMLOverlay(overlay) {
298
316
  overlay.classList.add('overlay');
299
317
  this.container.appendChild(overlay);
@@ -62,6 +62,9 @@ EditorImage.AddElementCommand = (_a = class {
62
62
  _applyByFlattening.set(this, false);
63
63
  __classPrivateFieldSet(this, _element, element, "f");
64
64
  __classPrivateFieldSet(this, _applyByFlattening, applyByFlattening, "f");
65
+ if (isNaN(__classPrivateFieldGet(this, _element, "f").getBBox().area)) {
66
+ throw new Error('Elements in the image cannot have NaN bounding boxes');
67
+ }
65
68
  }
66
69
  apply(editor) {
67
70
  editor.image.addElement(__classPrivateFieldGet(this, _element, "f"));
@@ -4,8 +4,9 @@ export declare enum PointerDevice {
4
4
  Pen = 0,
5
5
  Eraser = 1,
6
6
  Touch = 2,
7
- Mouse = 3,
8
- Other = 4
7
+ PrimaryButtonMouse = 3,
8
+ RightButtonMouse = 4,
9
+ Other = 5
9
10
  }
10
11
  export default class Pointer {
11
12
  readonly screenPos: Point2;
@@ -4,8 +4,9 @@ export var PointerDevice;
4
4
  PointerDevice[PointerDevice["Pen"] = 0] = "Pen";
5
5
  PointerDevice[PointerDevice["Eraser"] = 1] = "Eraser";
6
6
  PointerDevice[PointerDevice["Touch"] = 2] = "Touch";
7
- PointerDevice[PointerDevice["Mouse"] = 3] = "Mouse";
8
- PointerDevice[PointerDevice["Other"] = 4] = "Other";
7
+ PointerDevice[PointerDevice["PrimaryButtonMouse"] = 3] = "PrimaryButtonMouse";
8
+ PointerDevice[PointerDevice["RightButtonMouse"] = 4] = "RightButtonMouse";
9
+ PointerDevice[PointerDevice["Other"] = 5] = "Other";
9
10
  })(PointerDevice || (PointerDevice = {}));
10
11
  // Provides a snapshot containing information about a pointer. A Pointer
11
12
  // object is immutable --- it will not be updated when the pointer's information changes.
@@ -34,7 +35,7 @@ export default class Pointer {
34
35
  var _a, _b;
35
36
  const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
36
37
  const pointerTypeToDevice = {
37
- 'mouse': PointerDevice.Mouse,
38
+ 'mouse': PointerDevice.PrimaryButtonMouse,
38
39
  'pen': PointerDevice.Pen,
39
40
  'touch': PointerDevice.Touch,
40
41
  };
@@ -45,6 +46,14 @@ export default class Pointer {
45
46
  }
46
47
  const timeStamp = (new Date()).getTime();
47
48
  const canvasPos = viewport.roundPoint(viewport.screenToCanvas(screenPos));
49
+ if (device === PointerDevice.PrimaryButtonMouse) {
50
+ if (evt.buttons & 0x2) {
51
+ device = PointerDevice.RightButtonMouse;
52
+ }
53
+ else if (!(evt.buttons & 0x1)) {
54
+ device = PointerDevice.Other;
55
+ }
56
+ }
48
57
  return new Pointer(screenPos, canvasPos, (_b = evt.pressure) !== null && _b !== void 0 ? _b : null, evt.isPrimary, isDown, device, evt.pointerId, timeStamp);
49
58
  }
50
59
  // Create a new Pointer from a point on the canvas.
@@ -1,6 +1,14 @@
1
1
  import Rect2 from './geometry/Rect2';
2
2
  import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
3
3
  export declare const defaultSVGViewRect: Rect2;
4
+ export declare const svgAttributesDataKey = "svgAttrs";
5
+ export declare const svgStyleAttributesDataKey = "svgStyleAttrs";
6
+ export declare type SVGLoaderUnknownAttribute = [string, string];
7
+ export declare type SVGLoaderUnknownStyleAttribute = {
8
+ key: string;
9
+ value: string;
10
+ priority?: string;
11
+ };
4
12
  export default class SVGLoader implements ImageLoader {
5
13
  private source;
6
14
  private onFinish?;
@@ -13,7 +21,10 @@ export default class SVGLoader implements ImageLoader {
13
21
  private constructor();
14
22
  private getStyle;
15
23
  private strokeDataFromElem;
24
+ private attachUnrecognisedAttrs;
16
25
  private addPath;
26
+ private makeText;
27
+ private addText;
17
28
  private addUnknownNode;
18
29
  private updateViewBox;
19
30
  private updateSVGAttrs;
@@ -10,11 +10,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import Color4 from './Color4';
11
11
  import Stroke from './components/Stroke';
12
12
  import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
13
+ import Text from './components/Text';
13
14
  import UnknownSVGObject from './components/UnknownSVGObject';
15
+ import Mat33 from './geometry/Mat33';
14
16
  import Path from './geometry/Path';
15
17
  import Rect2 from './geometry/Rect2';
18
+ import { Vec2 } from './geometry/Vec2';
16
19
  // Size of a loaded image if no size is specified.
17
20
  export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
21
+ // Key to retrieve unrecognised attributes from an AbstractComponent
22
+ export const svgAttributesDataKey = 'svgAttrs';
23
+ export const svgStyleAttributesDataKey = 'svgStyleAttrs';
18
24
  export default class SVGLoader {
19
25
  constructor(source, onFinish) {
20
26
  this.source = source;
@@ -81,6 +87,30 @@ export default class SVGLoader {
81
87
  }
82
88
  return result;
83
89
  }
90
+ attachUnrecognisedAttrs(elem, node, supportedAttrs, supportedStyleAttrs) {
91
+ for (const attr of node.getAttributeNames()) {
92
+ if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
93
+ continue;
94
+ }
95
+ elem.attachLoadSaveData(svgAttributesDataKey, [attr, node.getAttribute(attr)]);
96
+ }
97
+ if (supportedStyleAttrs) {
98
+ for (const attr of node.style) {
99
+ if (attr === '' || !attr) {
100
+ continue;
101
+ }
102
+ if (supportedStyleAttrs.has(attr)) {
103
+ continue;
104
+ }
105
+ // TODO: Do we need special logic for !important properties?
106
+ elem.attachLoadSaveData(svgStyleAttributesDataKey, {
107
+ key: attr,
108
+ value: node.style.getPropertyValue(attr),
109
+ priority: node.style.getPropertyPriority(attr)
110
+ });
111
+ }
112
+ }
113
+ }
84
114
  // Adds a stroke with a single path
85
115
  addPath(node) {
86
116
  var _a;
@@ -88,6 +118,8 @@ export default class SVGLoader {
88
118
  try {
89
119
  const strokeData = this.strokeDataFromElem(node);
90
120
  elem = new Stroke(strokeData);
121
+ const supportedStyleAttrs = ['stroke', 'fill', 'stroke-width'];
122
+ this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStyleAttrs, 'd']), new Set(supportedStyleAttrs));
91
123
  }
92
124
  catch (e) {
93
125
  console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
@@ -95,6 +127,74 @@ export default class SVGLoader {
95
127
  }
96
128
  (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
97
129
  }
130
+ makeText(elem) {
131
+ var _a;
132
+ const contentList = [];
133
+ for (const child of elem.childNodes) {
134
+ if (child.nodeType === Node.TEXT_NODE) {
135
+ contentList.push((_a = child.nodeValue) !== null && _a !== void 0 ? _a : '');
136
+ }
137
+ else if (child.nodeType === Node.ELEMENT_NODE) {
138
+ const subElem = child;
139
+ if (subElem.tagName.toLowerCase() === 'tspan') {
140
+ contentList.push(this.makeText(subElem));
141
+ }
142
+ else {
143
+ throw new Error(`Unrecognized text child element: ${subElem}`);
144
+ }
145
+ }
146
+ else {
147
+ throw new Error(`Unrecognized text child node: ${child}.`);
148
+ }
149
+ }
150
+ // Compute styles.
151
+ const computedStyles = window.getComputedStyle(elem);
152
+ const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
153
+ const supportedStyleAttrs = [
154
+ 'fontFamily',
155
+ 'fill',
156
+ 'transform'
157
+ ];
158
+ let fontSize = 12;
159
+ if (fontSizeMatch) {
160
+ supportedStyleAttrs.push('fontSize');
161
+ fontSize = parseFloat(fontSizeMatch[1]);
162
+ }
163
+ const style = {
164
+ size: fontSize,
165
+ fontFamily: computedStyles.fontFamily || 'sans-serif',
166
+ renderingStyle: {
167
+ fill: Color4.fromString(computedStyles.fill)
168
+ },
169
+ };
170
+ // Compute transform matrix
171
+ let transform = Mat33.fromCSSMatrix(computedStyles.transform);
172
+ const supportedAttrs = [];
173
+ const elemX = elem.getAttribute('x');
174
+ const elemY = elem.getAttribute('y');
175
+ if (elemX && elemY) {
176
+ const x = parseFloat(elemX);
177
+ const y = parseFloat(elemY);
178
+ if (!isNaN(x) && !isNaN(y)) {
179
+ supportedAttrs.push('x', 'y');
180
+ transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
181
+ }
182
+ }
183
+ const result = new Text(contentList, transform, style);
184
+ this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
185
+ return result;
186
+ }
187
+ addText(elem) {
188
+ var _a;
189
+ try {
190
+ const textElem = this.makeText(elem);
191
+ (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
192
+ }
193
+ catch (e) {
194
+ console.error('Invalid text object in node', elem, '. Adding as an unknown object. Error:', e);
195
+ this.addUnknownNode(elem);
196
+ }
197
+ }
98
198
  addUnknownNode(node) {
99
199
  var _a;
100
200
  const component = new UnknownSVGObject(node);
@@ -106,12 +206,13 @@ export default class SVGLoader {
106
206
  if (this.rootViewBox || !viewBoxAttr) {
107
207
  return;
108
208
  }
109
- const components = viewBoxAttr.split(/[ \t,]/);
209
+ const components = viewBoxAttr.split(/[ \t\n,]+/);
110
210
  const x = parseFloat(components[0]);
111
211
  const y = parseFloat(components[1]);
112
212
  const width = parseFloat(components[2]);
113
213
  const height = parseFloat(components[3]);
114
214
  if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
215
+ console.warn(`node ${node} has an unparsable viewbox. Viewbox: ${viewBoxAttr}. Match: ${components}.`);
115
216
  return;
116
217
  }
117
218
  this.rootViewBox = new Rect2(x, y, width, height);
@@ -125,6 +226,7 @@ export default class SVGLoader {
125
226
  var _a;
126
227
  return __awaiter(this, void 0, void 0, function* () {
127
228
  this.totalToProcess += node.childElementCount;
229
+ let visitChildren = true;
128
230
  switch (node.tagName.toLowerCase()) {
129
231
  case 'g':
130
232
  // Continue -- visit the node's children.
@@ -132,6 +234,10 @@ export default class SVGLoader {
132
234
  case 'path':
133
235
  this.addPath(node);
134
236
  break;
237
+ case 'text':
238
+ this.addText(node);
239
+ visitChildren = false;
240
+ break;
135
241
  case 'svg':
136
242
  this.updateViewBox(node);
137
243
  this.updateSVGAttrs(node);
@@ -144,8 +250,10 @@ export default class SVGLoader {
144
250
  this.addUnknownNode(node);
145
251
  return;
146
252
  }
147
- for (const child of node.children) {
148
- yield this.visit(child);
253
+ if (visitChildren) {
254
+ for (const child of node.children) {
255
+ yield this.visit(child);
256
+ }
149
257
  }
150
258
  this.processedCount++;
151
259
  yield ((_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, this.processedCount, this.totalToProcess));
@@ -189,7 +297,6 @@ export default class SVGLoader {
189
297
  sandbox.remove();
190
298
  throw new Error('SVG loading iframe is not sandboxed.');
191
299
  }
192
- // Try running JavaScript within the iframe
193
300
  const sandboxDoc = (_b = (_a = sandbox.contentWindow) === null || _a === void 0 ? void 0 : _a.document) !== null && _b !== void 0 ? _b : sandbox.contentDocument;
194
301
  if (sandboxDoc == null)
195
302
  throw new Error('Unable to open a sandboxed iframe!');
@@ -213,7 +320,9 @@ export default class SVGLoader {
213
320
  sandboxDoc.close();
214
321
  const svgElem = sandboxDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
215
322
  svgElem.innerHTML = text;
323
+ sandboxDoc.body.appendChild(svgElem);
216
324
  return new SVGLoader(svgElem, () => {
325
+ svgElem.remove();
217
326
  sandbox.remove();
218
327
  });
219
328
  }
@@ -35,7 +35,7 @@ export declare class Viewport {
35
35
  getRotationAngle(): number;
36
36
  static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
37
37
  roundPoint(point: Point2): Point2;
38
- zoomTo(toMakeVisible: Rect2): Command;
38
+ zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;
39
39
  }
40
40
  export declare namespace Viewport {
41
41
  type ViewportTransform = typeof Viewport.ViewportTransform.prototype;
@@ -85,8 +85,14 @@ export class Viewport {
85
85
  // Returns a Command that transforms the view such that [rect] is visible, and perhaps
86
86
  // centered in the viewport.
87
87
  // Returns null if no transformation is necessary
88
- zoomTo(toMakeVisible) {
88
+ zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
89
89
  let transform = Mat33.identity;
90
+ if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
91
+ throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
92
+ }
93
+ if (isNaN(toMakeVisible.size.magnitude())) {
94
+ throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
95
+ }
90
96
  // Try to move the selection within the center 2/3rds of the viewport.
91
97
  const recomputeTargetRect = () => {
92
98
  // transform transforms objects on the canvas. As such, we need to invert it
@@ -98,7 +104,7 @@ export class Viewport {
98
104
  const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
99
105
  // Ensure that toMakeVisible is at least 1/8th of the visible region.
100
106
  const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
101
- if (largerThanTarget || muchSmallerThanTarget) {
107
+ if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
102
108
  // If larger than the target, ensure that the longest axis is visible.
103
109
  // If smaller, shrink the visible rectangle as much as possible
104
110
  const multiplier = (largerThanTarget ? Math.max : Math.min)(toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h);
@@ -115,6 +121,10 @@ export class Viewport {
115
121
  const viewportContentTransform = visibleRectTransform.inverse();
116
122
  transform = transform.rightMul(viewportContentTransform);
117
123
  }
124
+ if (!transform.invertable()) {
125
+ console.warn('Unable to zoom to ', toMakeVisible, '! Computed transform', transform, 'is singular.');
126
+ transform = Mat33.identity;
127
+ }
118
128
  return new Viewport.ViewportTransform(transform);
119
129
  }
120
130
  }
@@ -4,12 +4,17 @@ import Mat33 from '../geometry/Mat33';
4
4
  import Rect2 from '../geometry/Rect2';
5
5
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
6
6
  import { ImageComponentLocalization } from './localization';
7
+ declare type LoadSaveData = unknown;
8
+ export declare type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
7
9
  export default abstract class AbstractComponent {
8
10
  protected lastChangedTime: number;
9
11
  protected abstract contentBBox: Rect2;
10
12
  private zIndex;
11
13
  private static zIndexCounter;
12
14
  protected constructor();
15
+ private loadSaveData;
16
+ attachLoadSaveData(key: string, data: LoadSaveData): void;
17
+ getLoadSaveData(): LoadSaveDataTable;
13
18
  getZIndex(): number;
14
19
  getBBox(): Rect2;
15
20
  abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
@@ -18,3 +23,4 @@ export default abstract class AbstractComponent {
18
23
  transformBy(affineTransfm: Mat33): Command;
19
24
  abstract description(localizationTable: ImageComponentLocalization): string;
20
25
  }
26
+ export {};
@@ -1,9 +1,20 @@
1
1
  import EditorImage from '../EditorImage';
2
2
  export default class AbstractComponent {
3
3
  constructor() {
4
+ // Get and manage data attached by a loader.
5
+ this.loadSaveData = {};
4
6
  this.lastChangedTime = (new Date()).getTime();
5
7
  this.zIndex = AbstractComponent.zIndexCounter++;
6
8
  }
9
+ attachLoadSaveData(key, data) {
10
+ if (!this.loadSaveData[key]) {
11
+ this.loadSaveData[key] = [];
12
+ }
13
+ this.loadSaveData[key].push(data);
14
+ }
15
+ getLoadSaveData() {
16
+ return this.loadSaveData;
17
+ }
7
18
  getZIndex() {
8
19
  return this.zIndex;
9
20
  }
@@ -13,7 +13,6 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
13
13
  // Don't draw unrenderable objects if we can't
14
14
  return;
15
15
  }
16
- console.log('Rendering to SVG.', this.attrs);
17
16
  for (const [attr, value] of this.attrs) {
18
17
  canvas.setRootSVGAttribute(attr, value);
19
18
  }
@@ -41,7 +41,7 @@ export default class Stroke extends AbstractComponent {
41
41
  canvas.drawPath(part);
42
42
  }
43
43
  }
44
- canvas.endObject();
44
+ canvas.endObject(this.getLoadSaveData());
45
45
  }
46
46
  // Grows the bounding box for a given stroke part based on that part's style.
47
47
  bboxForPart(origBBox, style) {
@@ -0,0 +1,30 @@
1
+ import LineSegment2 from '../geometry/LineSegment2';
2
+ import Mat33 from '../geometry/Mat33';
3
+ import Rect2 from '../geometry/Rect2';
4
+ import AbstractRenderer, { RenderingStyle } from '../rendering/renderers/AbstractRenderer';
5
+ import AbstractComponent from './AbstractComponent';
6
+ import { ImageComponentLocalization } from './localization';
7
+ export interface TextStyle {
8
+ size: number;
9
+ fontFamily: string;
10
+ fontWeight?: string;
11
+ fontVariant?: string;
12
+ renderingStyle: RenderingStyle;
13
+ }
14
+ export default class Text extends AbstractComponent {
15
+ protected textObjects: Array<string | Text>;
16
+ private transform;
17
+ private style;
18
+ protected contentBBox: Rect2;
19
+ constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle);
20
+ static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
21
+ private static textMeasuringCtx;
22
+ private static getTextDimens;
23
+ private computeBBoxOfPart;
24
+ private recomputeBBox;
25
+ render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
26
+ intersects(lineSegment: LineSegment2): boolean;
27
+ protected applyTransformation(affineTransfm: Mat33): void;
28
+ private getText;
29
+ description(localizationTable: ImageComponentLocalization): string;
30
+ }
@@ -0,0 +1,111 @@
1
+ import LineSegment2 from '../geometry/LineSegment2';
2
+ import Rect2 from '../geometry/Rect2';
3
+ import AbstractComponent from './AbstractComponent';
4
+ export default class Text extends AbstractComponent {
5
+ constructor(textObjects, transform, style) {
6
+ super();
7
+ this.textObjects = textObjects;
8
+ this.transform = transform;
9
+ this.style = style;
10
+ this.recomputeBBox();
11
+ }
12
+ static applyTextStyles(ctx, style) {
13
+ var _a, _b;
14
+ // Quote the font family if necessary.
15
+ const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
16
+ ctx.font = [
17
+ ((_a = style.size) !== null && _a !== void 0 ? _a : 12) + 'px',
18
+ (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '',
19
+ `${fontFamily}`,
20
+ style.fontWeight
21
+ ].join(' ');
22
+ ctx.textAlign = 'left';
23
+ }
24
+ static getTextDimens(text, style) {
25
+ var _a;
26
+ (_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = document.createElement('canvas').getContext('2d'));
27
+ const ctx = Text.textMeasuringCtx;
28
+ Text.applyTextStyles(ctx, style);
29
+ const measure = ctx.measureText(text);
30
+ // Text is drawn with (0,0) at the bottom left of the baseline.
31
+ const textY = -measure.actualBoundingBoxAscent;
32
+ const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
33
+ return new Rect2(0, textY, measure.width, textHeight);
34
+ }
35
+ computeBBoxOfPart(part) {
36
+ if (typeof part === 'string') {
37
+ const textBBox = Text.getTextDimens(part, this.style);
38
+ return textBBox.transformedBoundingBox(this.transform);
39
+ }
40
+ else {
41
+ const bbox = part.contentBBox.transformedBoundingBox(this.transform);
42
+ return bbox;
43
+ }
44
+ }
45
+ recomputeBBox() {
46
+ let bbox = null;
47
+ for (const textObject of this.textObjects) {
48
+ const currentBBox = this.computeBBoxOfPart(textObject);
49
+ bbox !== null && bbox !== void 0 ? bbox : (bbox = currentBBox);
50
+ bbox = bbox.union(currentBBox);
51
+ }
52
+ this.contentBBox = bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
53
+ }
54
+ render(canvas, _visibleRect) {
55
+ const cursor = this.transform;
56
+ canvas.startObject(this.contentBBox);
57
+ for (const textObject of this.textObjects) {
58
+ if (typeof textObject === 'string') {
59
+ canvas.drawText(textObject, cursor, this.style);
60
+ }
61
+ else {
62
+ canvas.pushTransform(cursor);
63
+ textObject.render(canvas);
64
+ canvas.popTransform();
65
+ }
66
+ }
67
+ canvas.endObject(this.getLoadSaveData());
68
+ }
69
+ intersects(lineSegment) {
70
+ // Convert canvas space to internal space.
71
+ const invTransform = this.transform.inverse();
72
+ const p1InThisSpace = invTransform.transformVec2(lineSegment.p1);
73
+ const p2InThisSpace = invTransform.transformVec2(lineSegment.p2);
74
+ lineSegment = new LineSegment2(p1InThisSpace, p2InThisSpace);
75
+ for (const subObject of this.textObjects) {
76
+ if (typeof subObject === 'string') {
77
+ const textBBox = Text.getTextDimens(subObject, this.style);
78
+ // TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and
79
+ // use pixel-testing to check for intersection with its contour.
80
+ if (textBBox.getEdges().some(edge => lineSegment.intersection(edge) !== null)) {
81
+ return true;
82
+ }
83
+ }
84
+ else {
85
+ if (subObject.intersects(lineSegment)) {
86
+ return true;
87
+ }
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+ applyTransformation(affineTransfm) {
93
+ this.transform = affineTransfm.rightMul(this.transform);
94
+ this.recomputeBBox();
95
+ }
96
+ getText() {
97
+ const result = [];
98
+ for (const textObject of this.textObjects) {
99
+ if (typeof textObject === 'string') {
100
+ result.push(textObject);
101
+ }
102
+ else {
103
+ result.push(textObject.getText());
104
+ }
105
+ }
106
+ return result.join(' ');
107
+ }
108
+ description(localizationTable) {
109
+ return localizationTable.text(this.getText());
110
+ }
111
+ }
@@ -1,4 +1,5 @@
1
1
  export interface ImageComponentLocalization {
2
+ text: (text: string) => string;
2
3
  stroke: string;
3
4
  svgObject: string;
4
5
  }
@@ -1,4 +1,5 @@
1
1
  export const defaultComponentLocalization = {
2
2
  stroke: 'Stroke',
3
3
  svgObject: 'SVG Object',
4
+ text: (text) => `Text object: ${text}`,
4
5
  };
@@ -28,4 +28,5 @@ export default class Mat33 {
28
28
  static translation(amount: Vec2): Mat33;
29
29
  static zRotation(radians: number, center?: Point2): Mat33;
30
30
  static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
31
+ static fromCSSMatrix(cssString: string): Mat33;
31
32
  }