js-draw 0.1.0 → 0.1.3

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 (102) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.js +6 -3
  5. package/dist/src/EditorImage.d.ts +1 -1
  6. package/dist/src/EditorImage.js +6 -3
  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 +109 -0
  19. package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
  20. package/dist/src/components/localization.d.ts +1 -0
  21. package/dist/src/components/localization.js +1 -0
  22. package/dist/src/geometry/Mat33.d.ts +1 -0
  23. package/dist/src/geometry/Mat33.js +30 -0
  24. package/dist/src/geometry/Path.js +105 -67
  25. package/dist/src/geometry/Rect2.d.ts +2 -0
  26. package/dist/src/geometry/Rect2.js +25 -8
  27. package/dist/src/rendering/Display.js +4 -3
  28. package/dist/src/rendering/caching/CacheRecord.js +2 -1
  29. package/dist/src/rendering/caching/CacheRecordManager.js +2 -10
  30. package/dist/src/rendering/caching/RenderingCache.js +10 -4
  31. package/dist/src/rendering/caching/RenderingCacheNode.js +10 -3
  32. package/dist/src/rendering/caching/testUtils.js +1 -1
  33. package/dist/src/rendering/caching/types.d.ts +1 -0
  34. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
  35. package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
  36. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
  37. package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
  38. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
  39. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  40. package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
  41. package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
  42. package/dist/src/testing/loadExpectExtensions.js +1 -4
  43. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  44. package/dist/src/toolbar/HTMLToolbar.js +216 -154
  45. package/dist/src/toolbar/icons.d.ts +12 -0
  46. package/dist/src/toolbar/icons.js +197 -0
  47. package/dist/src/toolbar/localization.d.ts +4 -1
  48. package/dist/src/toolbar/localization.js +4 -1
  49. package/dist/src/toolbar/types.d.ts +4 -0
  50. package/dist/src/tools/PanZoom.d.ts +9 -6
  51. package/dist/src/tools/PanZoom.js +30 -21
  52. package/dist/src/tools/Pen.js +8 -3
  53. package/dist/src/tools/SelectionTool.js +1 -1
  54. package/dist/src/tools/TextTool.d.ts +29 -0
  55. package/dist/src/tools/TextTool.js +154 -0
  56. package/dist/src/tools/ToolController.d.ts +5 -5
  57. package/dist/src/tools/ToolController.js +10 -9
  58. package/dist/src/tools/localization.d.ts +3 -0
  59. package/dist/src/tools/localization.js +3 -0
  60. package/package.json +1 -1
  61. package/src/Editor.ts +7 -3
  62. package/src/EditorImage.ts +7 -3
  63. package/src/Pointer.ts +13 -4
  64. package/src/SVGLoader.ts +146 -5
  65. package/src/Viewport.ts +15 -3
  66. package/src/components/AbstractComponent.ts +16 -1
  67. package/src/components/SVGGlobalAttributesObject.ts +0 -1
  68. package/src/components/Stroke.ts +1 -1
  69. package/src/components/Text.ts +136 -0
  70. package/src/components/builders/FreehandLineBuilder.ts +1 -1
  71. package/src/components/localization.ts +2 -0
  72. package/src/geometry/Mat33.test.ts +44 -0
  73. package/src/geometry/Mat33.ts +41 -0
  74. package/src/geometry/Path.fromString.test.ts +94 -4
  75. package/src/geometry/Path.toString.test.ts +7 -3
  76. package/src/geometry/Path.ts +110 -68
  77. package/src/geometry/Rect2.test.ts +9 -0
  78. package/src/geometry/Rect2.ts +33 -8
  79. package/src/rendering/Display.ts +4 -3
  80. package/src/rendering/caching/CacheRecord.ts +2 -1
  81. package/src/rendering/caching/CacheRecordManager.ts +2 -12
  82. package/src/rendering/caching/RenderingCache.test.ts +1 -1
  83. package/src/rendering/caching/RenderingCache.ts +11 -4
  84. package/src/rendering/caching/RenderingCacheNode.ts +16 -3
  85. package/src/rendering/caching/testUtils.ts +1 -0
  86. package/src/rendering/caching/types.ts +4 -0
  87. package/src/rendering/renderers/AbstractRenderer.ts +18 -1
  88. package/src/rendering/renderers/CanvasRenderer.ts +34 -10
  89. package/src/rendering/renderers/DummyRenderer.ts +8 -0
  90. package/src/rendering/renderers/SVGRenderer.ts +57 -10
  91. package/src/testing/loadExpectExtensions.ts +1 -4
  92. package/src/toolbar/HTMLToolbar.ts +262 -170
  93. package/src/toolbar/icons.ts +226 -0
  94. package/src/toolbar/localization.ts +9 -2
  95. package/src/toolbar/toolbar.css +21 -8
  96. package/src/toolbar/types.ts +5 -0
  97. package/src/tools/PanZoom.ts +37 -27
  98. package/src/tools/Pen.ts +7 -3
  99. package/src/tools/SelectionTool.ts +1 -1
  100. package/src/tools/TextTool.ts +206 -0
  101. package/src/tools/ToolController.ts +7 -5
  102. package/src/tools/localization.ts +7 -0
@@ -172,7 +172,12 @@ export default class RenderingCacheNode {
172
172
  const newItems = [];
173
173
  // Divide [items] until nodes are leaves or smaller than this
174
174
  for (const item of items) {
175
- if (item.getBBox().maxDimension >= this.region.maxDimension) {
175
+ const bbox = item.getBBox();
176
+ if (!bbox.intersects(this.region)) {
177
+ continue;
178
+ }
179
+
180
+ if (bbox.maxDimension >= this.region.maxDimension) {
176
181
  newItems.push(...item.getChildrenOrSelfIntersectingRegion(this.region));
177
182
  } else {
178
183
  newItems.push(item);
@@ -186,6 +191,10 @@ export default class RenderingCacheNode {
186
191
  return;
187
192
  }
188
193
 
194
+ if (debugMode) {
195
+ screenRenderer.drawRect(this.region, 0.5 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
196
+ }
197
+
189
198
  // Could we render direclty from [this] or do we need to recurse?
190
199
  const couldRender = this.renderingWouldBeHighEnoughResolution(viewport);
191
200
  if (!couldRender) {
@@ -198,7 +207,11 @@ export default class RenderingCacheNode {
198
207
  // Determine whether we already have rendered the items
199
208
  const leaves = [];
200
209
  for (const item of items) {
201
- leaves.push(...item.getLeavesIntersectingRegion(this.region));
210
+ leaves.push(
211
+ ...item.getLeavesIntersectingRegion(
212
+ this.region, rect => rect.w / this.region.w < 2 / this.cacheState.props.blockResolution.x,
213
+ )
214
+ );
202
215
  }
203
216
  sortLeavesByZIndex(leaves);
204
217
  const leavesByIds = this.computeSortedByLeafIds(leaves);
@@ -266,7 +279,7 @@ export default class RenderingCacheNode {
266
279
  }
267
280
 
268
281
  if (debugMode) {
269
- screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
282
+ screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.clay });
270
283
  }
271
284
  }
272
285
  }
@@ -24,6 +24,7 @@ export const createCache = (onRenderAlloc?: RenderAllocCallback, cacheOptions?:
24
24
  cacheSize: 500 * 10 * 4,
25
25
  maxScale: 2,
26
26
  minComponentsPerCache: 0,
27
+ minComponentsToUseCache: 0,
27
28
  ...cacheOptions
28
29
  });
29
30
 
@@ -21,6 +21,10 @@ export interface CacheProps {
21
21
 
22
22
  // Minimum component count to cache, rather than just re-render each time.
23
23
  minComponentsPerCache: number;
24
+
25
+ // Minimum number of strokes/etc. to use the cache to render, isntead of
26
+ // rendering directly.
27
+ minComponentsToUseCache: number;
24
28
  }
25
29
 
26
30
  // CacheRecordManager relies on a partial copy of the shared state. Thus,
@@ -1,4 +1,6 @@
1
1
  import Color4 from '../../Color4';
2
+ import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
+ import { TextStyle } from '../../components/Text';
2
4
  import Mat33 from '../../geometry/Mat33';
3
5
  import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
4
6
  import Rect2 from '../../geometry/Rect2';
@@ -28,6 +30,7 @@ const stylesEqual = (a: RenderingStyle, b: RenderingStyle) => {
28
30
  export default abstract class AbstractRenderer {
29
31
  // If null, this' transformation is linked to the Viewport
30
32
  private selfTransform: Mat33|null = null;
33
+ private transformStack: Array<Mat33|null> = [];
31
34
 
32
35
  protected constructor(private viewport: Viewport) { }
33
36
 
@@ -50,6 +53,7 @@ export default abstract class AbstractRenderer {
50
53
  protected abstract traceQuadraticBezierCurve(
51
54
  controlPoint: Point2, endPoint: Point2,
52
55
  ): void;
56
+ public abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
53
57
 
54
58
  // Returns true iff the given rectangle is so small, rendering anything within
55
59
  // it has no effect on the image.
@@ -128,7 +132,7 @@ export default abstract class AbstractRenderer {
128
132
  this.objectLevel ++;
129
133
  }
130
134
 
131
- public endObject() {
135
+ public endObject(_loaderData?: LoadSaveDataTable) {
132
136
  // Render the paths all at once
133
137
  this.flushPath();
134
138
  this.currentPaths = null;
@@ -165,6 +169,19 @@ export default abstract class AbstractRenderer {
165
169
  this.selfTransform = transform;
166
170
  }
167
171
 
172
+ public pushTransform(transform: Mat33) {
173
+ this.transformStack.push(this.selfTransform);
174
+ this.setTransform(this.getCanvasToScreenTransform().rightMul(transform));
175
+ }
176
+
177
+ public popTransform() {
178
+ if (this.transformStack.length === 0) {
179
+ throw new Error('Unable to pop more transforms than have been pushed!');
180
+ }
181
+
182
+ this.setTransform(this.transformStack.pop() ?? null);
183
+ }
184
+
168
185
  // Get the matrix that transforms a vector on the canvas to a vector on this'
169
186
  // rendering target.
170
187
  public getCanvasToScreenTransform(): Mat33 {
@@ -1,4 +1,5 @@
1
1
  import Color4 from '../../Color4';
2
+ import Text, { TextStyle } from '../../components/Text';
2
3
  import Mat33 from '../../geometry/Mat33';
3
4
  import Rect2 from '../../geometry/Rect2';
4
5
  import { Point2, Vec2 } from '../../geometry/Vec2';
@@ -26,16 +27,7 @@ export default class CanvasRenderer extends AbstractRenderer {
26
27
  this.setDraftMode(false);
27
28
  }
28
29
 
29
- public canRenderFromWithoutDataLoss(other: AbstractRenderer) {
30
- return other instanceof CanvasRenderer;
31
- }
32
-
33
- public renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void {
34
- if (!(other instanceof CanvasRenderer)) {
35
- throw new Error(`${other} cannot be rendered onto ${this}`);
36
- }
37
- transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
38
- this.ctx.save();
30
+ private transformBy(transformBy: Mat33) {
39
31
  // From MDN, transform(a,b,c,d,e,f)
40
32
  // takes input such that
41
33
  // ⎡ a c e ⎤
@@ -46,6 +38,19 @@ export default class CanvasRenderer extends AbstractRenderer {
46
38
  transformBy.a2, transformBy.b2, // c, d
47
39
  transformBy.a3, transformBy.b3, // e, f
48
40
  );
41
+ }
42
+
43
+ public canRenderFromWithoutDataLoss(other: AbstractRenderer) {
44
+ return other instanceof CanvasRenderer;
45
+ }
46
+
47
+ public renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void {
48
+ if (!(other instanceof CanvasRenderer)) {
49
+ throw new Error(`${other} cannot be rendered onto ${this}`);
50
+ }
51
+ transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
52
+ this.ctx.save();
53
+ this.transformBy(transformBy);
49
54
  this.ctx.drawImage(other.ctx.canvas, 0, 0);
50
55
  this.ctx.restore();
51
56
  }
@@ -143,6 +148,25 @@ export default class CanvasRenderer extends AbstractRenderer {
143
148
  super.drawPath(path);
144
149
  }
145
150
 
151
+ public drawText(text: string, transform: Mat33, style: TextStyle): void {
152
+ this.ctx.save();
153
+ transform = this.getCanvasToScreenTransform().rightMul(transform);
154
+ this.transformBy(transform);
155
+ Text.applyTextStyles(this.ctx, style);
156
+
157
+ if (style.renderingStyle.fill.a !== 0) {
158
+ this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
159
+ this.ctx.fillText(text, 0, 0);
160
+ }
161
+ if (style.renderingStyle.stroke) {
162
+ this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
163
+ this.ctx.lineWidth = style.renderingStyle.stroke.width;
164
+ this.ctx.strokeText(text, 0, 0);
165
+ }
166
+
167
+ this.ctx.restore();
168
+ }
169
+
146
170
  private clipLevels: number[] = [];
147
171
  public startObject(boundingBox: Rect2, clip: boolean) {
148
172
  if (this.isTooSmallToRender(boundingBox)) {
@@ -1,5 +1,6 @@
1
1
  // Renderer that outputs nothing. Useful for automated tests.
2
2
 
3
+ import { TextStyle } from '../../components/Text';
3
4
  import Mat33 from '../../geometry/Mat33';
4
5
  import Rect2 from '../../geometry/Rect2';
5
6
  import { Point2, Vec2 } from '../../geometry/Vec2';
@@ -14,6 +15,7 @@ export default class DummyRenderer extends AbstractRenderer {
14
15
  public lastFillStyle: RenderingStyle|null = null;
15
16
  public lastPoint: Point2|null = null;
16
17
  public objectNestingLevel: number = 0;
18
+ public lastText: string|null = null;
17
19
 
18
20
  // List of points drawn since the last clear.
19
21
  public pointBuffer: Point2[] = [];
@@ -40,6 +42,7 @@ export default class DummyRenderer extends AbstractRenderer {
40
42
  this.clearedCount ++;
41
43
  this.renderedPathCount = 0;
42
44
  this.pointBuffer = [];
45
+ this.lastText = null;
43
46
 
44
47
  // Ensure all objects finished rendering
45
48
  if (this.objectNestingLevel > 0) {
@@ -88,6 +91,11 @@ export default class DummyRenderer extends AbstractRenderer {
88
91
  // As such, it is unlikely to be the target of automated tests.
89
92
  }
90
93
 
94
+
95
+ public drawText(text: string, _transform: Mat33, _style: TextStyle): void {
96
+ this.lastText = text;
97
+ }
98
+
91
99
  public startObject(boundingBox: Rect2, _clip: boolean) {
92
100
  super.startObject(boundingBox);
93
101
 
@@ -1,7 +1,11 @@
1
1
 
2
+ import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
+ import { TextStyle } from '../../components/Text';
4
+ import Mat33 from '../../geometry/Mat33';
2
5
  import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
3
6
  import Rect2 from '../../geometry/Rect2';
4
7
  import { Point2, Vec2 } from '../../geometry/Vec2';
8
+ import { svgAttributesDataKey, SVGLoaderUnknownAttribute, SVGLoaderUnknownStyleAttribute, svgStyleAttributesDataKey } from '../../SVGLoader';
5
9
  import Viewport from '../../Viewport';
6
10
  import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
7
11
 
@@ -13,8 +17,8 @@ export default class SVGRenderer extends AbstractRenderer {
13
17
  private lastPathStyle: RenderingStyle|null;
14
18
  private lastPath: PathCommand[]|null;
15
19
  private lastPathStart: Point2|null;
20
+ private objectElems: SVGElement[]|null = null;
16
21
 
17
- private mainGroup: SVGGElement;
18
22
  private overwrittenAttrs: Record<string, string|null> = {};
19
23
 
20
24
  public constructor(private elem: SVGSVGElement, viewport: Viewport) {
@@ -41,8 +45,6 @@ export default class SVGRenderer extends AbstractRenderer {
41
45
  }
42
46
 
43
47
  public clear() {
44
- this.mainGroup = document.createElementNS(svgNameSpace, 'g');
45
-
46
48
  // Restore all alltributes
47
49
  for (const attrName in this.overwrittenAttrs) {
48
50
  const value = this.overwrittenAttrs[attrName];
@@ -54,9 +56,6 @@ export default class SVGRenderer extends AbstractRenderer {
54
56
  }
55
57
  }
56
58
  this.overwrittenAttrs = {};
57
-
58
- // Remove all children
59
- this.elem.replaceChildren(this.mainGroup);
60
59
  }
61
60
 
62
61
  protected beginPath(startPoint: Point2) {
@@ -106,7 +105,34 @@ export default class SVGRenderer extends AbstractRenderer {
106
105
  pathElem.setAttribute('stroke-width', style.stroke.width.toString());
107
106
  }
108
107
 
109
- this.mainGroup.appendChild(pathElem);
108
+ this.elem.appendChild(pathElem);
109
+ this.objectElems?.push(pathElem);
110
+ }
111
+
112
+ public drawText(text: string, transform: Mat33, style: TextStyle): void {
113
+ transform = this.getCanvasToScreenTransform().rightMul(transform);
114
+
115
+ const textElem = document.createElementNS(svgNameSpace, 'text');
116
+ textElem.appendChild(document.createTextNode(text));
117
+ textElem.style.transform = `matrix(
118
+ ${transform.a1}, ${transform.b1},
119
+ ${transform.a2}, ${transform.b2},
120
+ ${transform.a3}, ${transform.b3}
121
+ )`;
122
+ textElem.style.fontFamily = style.fontFamily;
123
+ textElem.style.fontVariant = style.fontVariant ?? '';
124
+ textElem.style.fontWeight = style.fontWeight ?? '';
125
+ textElem.style.fontSize = style.size + 'px';
126
+ textElem.style.fill = style.renderingStyle.fill.toHexString();
127
+
128
+ if (style.renderingStyle.stroke) {
129
+ const strokeStyle = style.renderingStyle.stroke;
130
+ textElem.style.stroke = strokeStyle.color.toHexString();
131
+ textElem.style.strokeWidth = strokeStyle.width + 'px';
132
+ }
133
+
134
+ this.elem.appendChild(textElem);
135
+ this.objectElems?.push(textElem);
110
136
  }
111
137
 
112
138
  public startObject(boundingBox: Rect2) {
@@ -116,13 +142,34 @@ export default class SVGRenderer extends AbstractRenderer {
116
142
  this.lastPath = null;
117
143
  this.lastPathStart = null;
118
144
  this.lastPathStyle = null;
145
+ this.objectElems = [];
119
146
  }
120
147
 
121
- public endObject() {
122
- super.endObject();
148
+ public endObject(loaderData?: LoadSaveDataTable) {
149
+ super.endObject(loaderData);
123
150
 
124
151
  // Don't extend paths across objects
125
152
  this.addPathToSVG();
153
+
154
+ if (loaderData) {
155
+ // Restore any attributes unsupported by the app.
156
+ for (const elem of this.objectElems ?? []) {
157
+ const attrs = loaderData[svgAttributesDataKey] as SVGLoaderUnknownAttribute[]|undefined;
158
+ const styleAttrs = loaderData[svgStyleAttributesDataKey] as SVGLoaderUnknownStyleAttribute[]|undefined;
159
+
160
+ if (attrs) {
161
+ for (const [ attr, value ] of attrs) {
162
+ elem.setAttribute(attr, value);
163
+ }
164
+ }
165
+
166
+ if (styleAttrs) {
167
+ for (const attr of styleAttrs) {
168
+ elem.style.setProperty(attr.key, attr.value, attr.priority);
169
+ }
170
+ }
171
+ }
172
+ }
126
173
  }
127
174
 
128
175
  protected lineTo(point: Point2) {
@@ -175,7 +222,7 @@ export default class SVGRenderer extends AbstractRenderer {
175
222
  elem.setAttribute('cx', `${point.x}`);
176
223
  elem.setAttribute('cy', `${point.y}`);
177
224
  elem.setAttribute('r', '15');
178
- this.mainGroup.appendChild(elem);
225
+ this.elem.appendChild(elem);
179
226
  });
180
227
  }
181
228
 
@@ -15,10 +15,7 @@ export const loadExpectExtensions = () => {
15
15
  return {
16
16
  pass,
17
17
  message: () => {
18
- if (pass) {
19
- return `Expected ${expected} not to .eq ${actual}. Options(${eqArgs})`;
20
- }
21
- return `Expected ${expected} to .eq ${actual}. Options(${eqArgs})`;
18
+ return `Expected ${pass ? '!' : ''}(${actual}).eq(${expected}). Options(${eqArgs})`;
22
19
  },
23
20
  };
24
21
  },