js-draw 0.4.1 → 0.6.0

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 +19 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +9 -6
  4. package/dist/src/Editor.js +9 -3
  5. package/dist/src/EditorImage.d.ts +3 -0
  6. package/dist/src/EditorImage.js +7 -0
  7. package/dist/src/SVGLoader.js +5 -6
  8. package/dist/src/components/AbstractComponent.d.ts +1 -0
  9. package/dist/src/components/AbstractComponent.js +4 -0
  10. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -0
  11. package/dist/src/components/SVGGlobalAttributesObject.js +3 -0
  12. package/dist/src/components/Text.d.ts +3 -5
  13. package/dist/src/components/Text.js +19 -10
  14. package/dist/src/components/UnknownSVGObject.d.ts +1 -0
  15. package/dist/src/components/UnknownSVGObject.js +3 -0
  16. package/dist/src/components/builders/FreehandLineBuilder.js +3 -3
  17. package/dist/src/rendering/renderers/SVGRenderer.js +1 -1
  18. package/dist/src/testing/beforeEachFile.js +4 -0
  19. package/dist/src/toolbar/HTMLToolbar.js +2 -3
  20. package/dist/src/toolbar/IconProvider.d.ts +24 -0
  21. package/dist/src/toolbar/IconProvider.js +415 -0
  22. package/dist/src/toolbar/lib.d.ts +1 -1
  23. package/dist/src/toolbar/lib.js +1 -2
  24. package/dist/src/toolbar/localization.d.ts +0 -1
  25. package/dist/src/toolbar/localization.js +0 -1
  26. package/dist/src/toolbar/makeColorInput.js +1 -2
  27. package/dist/src/toolbar/widgets/BaseWidget.d.ts +2 -0
  28. package/dist/src/toolbar/widgets/BaseWidget.js +16 -2
  29. package/dist/src/toolbar/widgets/EraserToolWidget.js +1 -2
  30. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +5 -3
  31. package/dist/src/toolbar/widgets/HandToolWidget.js +35 -12
  32. package/dist/src/toolbar/widgets/PenToolWidget.d.ts +2 -0
  33. package/dist/src/toolbar/widgets/PenToolWidget.js +16 -3
  34. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +3 -0
  35. package/dist/src/toolbar/widgets/SelectionToolWidget.js +20 -7
  36. package/dist/src/toolbar/widgets/TextToolWidget.js +1 -2
  37. package/dist/src/tools/PanZoom.d.ts +1 -1
  38. package/dist/src/tools/PanZoom.js +4 -1
  39. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -0
  40. package/dist/src/tools/SelectionTool/SelectionTool.js +66 -3
  41. package/dist/src/tools/ToolController.js +3 -0
  42. package/dist/src/tools/ToolbarShortcutHandler.d.ts +12 -0
  43. package/dist/src/tools/ToolbarShortcutHandler.js +23 -0
  44. package/dist/src/tools/lib.d.ts +1 -0
  45. package/dist/src/tools/lib.js +1 -0
  46. package/dist/src/tools/localization.d.ts +1 -0
  47. package/dist/src/tools/localization.js +1 -0
  48. package/dist/src/types.d.ts +4 -2
  49. package/package.json +1 -1
  50. package/src/Editor.ts +17 -7
  51. package/src/EditorImage.ts +9 -0
  52. package/src/SVGLoader.test.ts +37 -0
  53. package/src/SVGLoader.ts +5 -6
  54. package/src/components/AbstractComponent.ts +5 -0
  55. package/src/components/SVGGlobalAttributesObject.ts +4 -0
  56. package/src/components/Text.test.ts +1 -16
  57. package/src/components/Text.ts +21 -11
  58. package/src/components/UnknownSVGObject.ts +4 -0
  59. package/src/components/builders/FreehandLineBuilder.ts +3 -3
  60. package/src/rendering/renderers/SVGRenderer.ts +1 -1
  61. package/src/testing/beforeEachFile.ts +6 -1
  62. package/src/toolbar/HTMLToolbar.ts +2 -3
  63. package/src/toolbar/IconProvider.ts +476 -0
  64. package/src/toolbar/lib.ts +1 -1
  65. package/src/toolbar/localization.ts +0 -2
  66. package/src/toolbar/makeColorInput.ts +1 -2
  67. package/src/toolbar/widgets/BaseWidget.ts +20 -3
  68. package/src/toolbar/widgets/EraserToolWidget.ts +1 -2
  69. package/src/toolbar/widgets/HandToolWidget.ts +42 -20
  70. package/src/toolbar/widgets/PenToolWidget.ts +20 -4
  71. package/src/toolbar/widgets/SelectionToolWidget.ts +24 -8
  72. package/src/toolbar/widgets/TextToolWidget.ts +1 -2
  73. package/src/tools/PanZoom.ts +4 -1
  74. package/src/tools/SelectionTool/SelectionTool.css +2 -1
  75. package/src/tools/SelectionTool/SelectionTool.test.ts +40 -0
  76. package/src/tools/SelectionTool/SelectionTool.ts +73 -4
  77. package/src/tools/ToolController.ts +3 -0
  78. package/src/tools/ToolbarShortcutHandler.ts +34 -0
  79. package/src/tools/UndoRedoShortcut.test.ts +3 -0
  80. package/src/tools/lib.ts +1 -0
  81. package/src/tools/localization.ts +4 -0
  82. package/src/types.ts +13 -8
  83. package/typedoc.json +5 -1
  84. package/dist/src/toolbar/icons.d.ts +0 -20
  85. package/dist/src/toolbar/icons.js +0 -385
  86. package/src/toolbar/icons.ts +0 -443
@@ -14,8 +14,6 @@ export interface TextStyle {
14
14
  renderingStyle: RenderingStyle;
15
15
  }
16
16
 
17
- type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2;
18
-
19
17
  const componentTypeId = 'text';
20
18
  export default class Text extends AbstractComponent {
21
19
  protected contentBBox: Rect2;
@@ -24,10 +22,6 @@ export default class Text extends AbstractComponent {
24
22
  protected readonly textObjects: Array<string|Text>,
25
23
  private transform: Mat33,
26
24
  private readonly style: TextStyle,
27
-
28
- // If not given, an HtmlCanvasElement is used to determine text boundaries.
29
- // @internal
30
- private readonly getTextDimens: GetTextDimensCallback = Text.getTextDimens,
31
25
  ) {
32
26
  super(componentTypeId);
33
27
  this.recomputeBBox();
@@ -47,9 +41,25 @@ export default class Text extends AbstractComponent {
47
41
  ctx.textAlign = 'left';
48
42
  }
49
43
 
50
- private static textMeasuringCtx: CanvasRenderingContext2D;
44
+ private static textMeasuringCtx: CanvasRenderingContext2D|null = null;
45
+
46
+ // Roughly estimate the bounding box of `text`. Use if no CanvasRenderingContext2D is available.
47
+ private static estimateTextDimens(text: string, style: TextStyle): Rect2 {
48
+ const widthEst = text.length * style.size;
49
+ const heightEst = style.size;
50
+
51
+ // Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should
52
+ // be above (0, 0).
53
+ return new Rect2(0, -heightEst * 2/3, widthEst, heightEst);
54
+ }
55
+
56
+ // Returns the bounding box of `text`. This is approximate if no Canvas is available.
51
57
  private static getTextDimens(text: string, style: TextStyle): Rect2 {
52
- Text.textMeasuringCtx ??= document.createElement('canvas').getContext('2d')!;
58
+ Text.textMeasuringCtx ??= document.createElement('canvas').getContext('2d') ?? null;
59
+ if (!Text.textMeasuringCtx) {
60
+ return this.estimateTextDimens(text, style);
61
+ }
62
+
53
63
  const ctx = Text.textMeasuringCtx;
54
64
  Text.applyTextStyles(ctx, style);
55
65
 
@@ -63,7 +73,7 @@ export default class Text extends AbstractComponent {
63
73
 
64
74
  private computeBBoxOfPart(part: string|Text) {
65
75
  if (typeof part === 'string') {
66
- const textBBox = this.getTextDimens(part, this.style);
76
+ const textBBox = Text.getTextDimens(part, this.style);
67
77
  return textBBox.transformedBoundingBox(this.transform);
68
78
  } else {
69
79
  const bbox = part.contentBBox.transformedBoundingBox(this.transform);
@@ -178,7 +188,7 @@ export default class Text extends AbstractComponent {
178
188
  };
179
189
  }
180
190
 
181
- public static deserializeFromString(json: any, getTextDimens: GetTextDimensCallback = Text.getTextDimens): Text {
191
+ public static deserializeFromString(json: any): Text {
182
192
  const style: TextStyle = {
183
193
  renderingStyle: styleFromJSON(json.style.renderingStyle),
184
194
  size: json.style.size,
@@ -203,7 +213,7 @@ export default class Text extends AbstractComponent {
203
213
  const transformData = json.transform as Mat33Array;
204
214
  const transform = new Mat33(...transformData);
205
215
 
206
- return new Text(textObjects, transform, style, getTextDimens);
216
+ return new Text(textObjects, transform, style);
207
217
  }
208
218
  }
209
219
 
@@ -37,6 +37,10 @@ export default class UnknownSVGObject extends AbstractComponent {
37
37
  protected applyTransformation(_affineTransfm: Mat33): void {
38
38
  }
39
39
 
40
+ public isSelectable() {
41
+ return false;
42
+ }
43
+
40
44
  protected createClone(): AbstractComponent {
41
45
  return new UnknownSVGObject(this.svgObject.cloneNode(true) as SVGElement);
42
46
  }
@@ -415,8 +415,8 @@ export default class FreehandLineBuilder implements ComponentBuilder {
415
415
 
416
416
  let enteringVec = this.lastExitingVec;
417
417
  if (!enteringVec) {
418
- let sampleIdx = Math.ceil(this.buffer.length / 3);
419
- if (sampleIdx === 0) {
418
+ let sampleIdx = Math.ceil(this.buffer.length / 2);
419
+ if (sampleIdx === 0 || sampleIdx >= this.buffer.length) {
420
420
  sampleIdx = this.buffer.length - 1;
421
421
  }
422
422
 
@@ -426,7 +426,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
426
426
  let exitingVec = this.computeExitingVec();
427
427
 
428
428
  // Find the intersection between the entering vector and the exiting vector
429
- const maxRelativeLength = 3;
429
+ const maxRelativeLength = 2;
430
430
  const segmentStart = this.buffer[0];
431
431
  const segmentEnd = newPoint.pos;
432
432
  const startEndDist = segmentEnd.minus(segmentStart).magnitude();
@@ -88,7 +88,7 @@ export default class SVGRenderer extends AbstractRenderer {
88
88
 
89
89
  public drawPath(pathSpec: RenderablePathSpec) {
90
90
  const style = pathSpec.style;
91
- const path = Path.fromRenderable(pathSpec);
91
+ const path = Path.fromRenderable(pathSpec).transformedBy(this.getCanvasToScreenTransform());
92
92
 
93
93
  // Try to extend the previous path, if possible
94
94
  if (!style.fill.eq(this.lastPathStyle?.fill) || this.lastPathString.length === 0) {
@@ -1,3 +1,8 @@
1
1
  import loadExpectExtensions from './loadExpectExtensions';
2
2
  loadExpectExtensions();
3
- jest.useFakeTimers();
3
+ jest.useFakeTimers();
4
+
5
+ // jsdom doesn't support HTMLCanvasElement#getContext — it logs an error
6
+ // to the console. Make it return null so we can handle a non-existent Canvas
7
+ // at runtime (e.g. use something else, if available).
8
+ HTMLCanvasElement.prototype.getContext = () => null;
@@ -5,7 +5,6 @@ import { coloris, init as colorisInit } from '@melloware/coloris';
5
5
  import Color4 from '../Color4';
6
6
  import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
7
7
  import { ActionButtonIcon } from './types';
8
- import { makeRedoIcon, makeUndoIcon } from './icons';
9
8
  import SelectionTool from '../tools/SelectionTool/SelectionTool';
10
9
  import PanZoomTool from '../tools/PanZoom';
11
10
  import TextTool from '../tools/TextTool';
@@ -156,13 +155,13 @@ export default class HTMLToolbar {
156
155
 
157
156
  const undoButton = this.addActionButton({
158
157
  label: this.localizationTable.undo,
159
- icon: makeUndoIcon()
158
+ icon: this.editor.icons.makeUndoIcon()
160
159
  }, () => {
161
160
  this.editor.history.undo();
162
161
  }, undoRedoGroup);
163
162
  const redoButton = this.addActionButton({
164
163
  label: this.localizationTable.redo,
165
- icon: makeRedoIcon(),
164
+ icon: this.editor.icons.makeRedoIcon(),
166
165
  }, () => {
167
166
  this.editor.history.redo();
168
167
  }, undoRedoGroup);
@@ -0,0 +1,476 @@
1
+ import Color4 from '../Color4';
2
+ import { ComponentBuilderFactory } from '../components/builders/types';
3
+ import { TextStyle } from '../components/Text';
4
+ import EventDispatcher from '../EventDispatcher';
5
+ import { Vec2 } from '../math/Vec2';
6
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
7
+ import Pen from '../tools/Pen';
8
+ import { StrokeDataPoint } from '../types';
9
+ import Viewport from '../Viewport';
10
+
11
+
12
+
13
+ const svgNamespace = 'http://www.w3.org/2000/svg';
14
+ const iconColorFill = `
15
+ style='fill: var(--icon-color);'
16
+ `;
17
+ const iconColorStrokeFill = `
18
+ style='fill: var(--icon-color); stroke: var(--icon-color);'
19
+ `;
20
+ const checkerboardPatternDef = `
21
+ <pattern
22
+ id='checkerboard'
23
+ viewBox='0,0,10,10'
24
+ width='20%'
25
+ height='20%'
26
+ patternUnits='userSpaceOnUse'
27
+ >
28
+ <rect x=0 y=0 width=10 height=10 fill='white'/>
29
+ <rect x=0 y=0 width=5 height=5 fill='gray'/>
30
+ <rect x=5 y=5 width=5 height=5 fill='gray'/>
31
+ </pattern>
32
+ `;
33
+ const checkerboardPatternRef = 'url(#checkerboard)';
34
+
35
+ // Provides icons that can be used in the toolbar, etc.
36
+ // Extend this class and override methods to customize icons.
37
+ export default class IconProvider {
38
+
39
+ public makeUndoIcon() {
40
+ return this.makeRedoIcon(true);
41
+ }
42
+
43
+ // @param mirror - reflect across the x-axis @internal
44
+ // @returns a redo icon.
45
+ public makeRedoIcon(mirror: boolean = false) {
46
+ const icon = document.createElementNS(svgNamespace, 'svg');
47
+ icon.innerHTML = `
48
+ <style>
49
+ .toolbar-svg-undo-redo-icon {
50
+ stroke: var(--icon-color);
51
+ stroke-width: 12;
52
+ stroke-linejoin: round;
53
+ stroke-linecap: round;
54
+ fill: none;
55
+
56
+ transform-origin: center;
57
+ }
58
+ </style>
59
+ <path
60
+ d='M20,20 A15,15 0 0 1 70,80 L80,90 L60,70 L65,90 L87,90 L65,80'
61
+ class='toolbar-svg-undo-redo-icon'
62
+ style='${mirror ? 'transform: scale(-1, 1);' : ''}'/>
63
+ `;
64
+ icon.setAttribute('viewBox', '0 0 100 100');
65
+ return icon;
66
+ }
67
+
68
+ public makeDropdownIcon() {
69
+ const icon = document.createElementNS(svgNamespace, 'svg');
70
+ icon.innerHTML = `
71
+ <g>
72
+ <path
73
+ d='M5,10 L50,90 L95,10 Z'
74
+ ${iconColorFill}
75
+ />
76
+ </g>
77
+ `;
78
+ icon.setAttribute('viewBox', '0 0 100 100');
79
+ return icon;
80
+ }
81
+
82
+ public makeEraserIcon() {
83
+ const icon = document.createElementNS(svgNamespace, 'svg');
84
+
85
+ // Draw an eraser-like shape
86
+ icon.innerHTML = `
87
+ <g>
88
+ <rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
89
+ <rect
90
+ x=10 y=10 width=80 height=50
91
+ ${iconColorFill}
92
+ />
93
+ </g>
94
+ `;
95
+ icon.setAttribute('viewBox', '0 0 100 100');
96
+ return icon;
97
+ }
98
+
99
+ public makeSelectionIcon() {
100
+ const icon = document.createElementNS(svgNamespace, 'svg');
101
+
102
+ // Draw a cursor-like shape
103
+ icon.innerHTML = `
104
+ <g>
105
+ <rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
106
+ <rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
107
+ </g>
108
+ `;
109
+ icon.setAttribute('viewBox', '0 0 100 100');
110
+
111
+ return icon;
112
+ }
113
+
114
+ protected makeIconFromPath(
115
+ pathData: string,
116
+ fill: string = 'var(--icon-color)',
117
+ strokeColor: string = 'none',
118
+ strokeWidth: string = '0px',
119
+ ) {
120
+ const icon = document.createElementNS(svgNamespace, 'svg');
121
+ const path = document.createElementNS(svgNamespace, 'path');
122
+ path.setAttribute('d', pathData);
123
+ path.style.fill = fill;
124
+ path.style.stroke = strokeColor;
125
+ path.style.strokeWidth = strokeWidth;
126
+ icon.appendChild(path);
127
+ icon.setAttribute('viewBox', '0 0 100 100');
128
+
129
+ return icon;
130
+ }
131
+
132
+ public makeHandToolIcon() {
133
+ const fill = 'none';
134
+ const strokeColor = 'var(--icon-color)';
135
+ const strokeWidth = '3';
136
+
137
+ // Draw a cursor-like shape (like some of the other icons, made with Inkscape)
138
+ return this.makeIconFromPath(`
139
+ m 10,60
140
+ 5,30
141
+ H 90
142
+ V 30
143
+ C 90,20 75,20 75,30
144
+ V 60
145
+ 20
146
+ C 75,10 60,10 60,20
147
+ V 60
148
+ 15
149
+ C 60,5 45,5 45,15
150
+ V 60
151
+ 25
152
+ C 45,15 30,15 30,25
153
+ V 60
154
+ 75
155
+ L 25,60
156
+ C 20,45 10,50 10,60
157
+ Z
158
+ `, fill, strokeColor, strokeWidth);
159
+ }
160
+
161
+ public makeTouchPanningIcon() {
162
+ const fill = 'none';
163
+ const strokeColor = 'var(--icon-color)';
164
+ const strokeWidth = '3';
165
+
166
+ return this.makeIconFromPath(`
167
+ M 5,5.5
168
+ V 17.2
169
+ L 16.25,5.46
170
+ Z
171
+
172
+ m 33.75,0
173
+ L 50,17
174
+ V 5.5
175
+ Z
176
+
177
+ M 5,40.7
178
+ v 11.7
179
+ h 11.25
180
+ z
181
+
182
+ M 26,19
183
+ C 19.8,19.4 17.65,30.4 21.9,34.8
184
+ L 50,70
185
+ H 27.5
186
+ c -11.25,0 -11.25,17.6 0,17.6
187
+ H 61.25
188
+ C 94.9,87.8 95,87.6 95,40.7 78.125,23 67,29 55.6,46.5
189
+ L 33.1,23
190
+ C 30.3125,20.128192 27.9,19 25.830078,19.119756
191
+ Z
192
+ `, fill, strokeColor, strokeWidth);
193
+ }
194
+
195
+ public makeAllDevicePanningIcon() {
196
+ const fill = 'none';
197
+ const strokeColor = 'var(--icon-color)';
198
+ const strokeWidth = '3';
199
+ return this.makeIconFromPath(`
200
+ M 5 5
201
+ L 5 17.5
202
+ 17.5 5
203
+ 5 5
204
+ z
205
+
206
+ M 42.5 5
207
+ L 55 17.5
208
+ 55 5
209
+ 42.5 5
210
+ z
211
+
212
+ M 70 10
213
+ L 70 21
214
+ 61 15
215
+ 55.5 23
216
+ 66 30
217
+ 56 37
218
+ 61 45
219
+ 70 39
220
+ 70 50
221
+ 80 50
222
+ 80 39
223
+ 89 45
224
+ 95 36
225
+ 84 30
226
+ 95 23
227
+ 89 15
228
+ 80 21
229
+ 80 10
230
+ 70 10
231
+ z
232
+
233
+ M 27.5 26.25
234
+ L 27.5 91.25
235
+ L 43.75 83.125
236
+ L 52 99
237
+ L 68 91
238
+ L 60 75
239
+ L 76.25 66.875
240
+ L 27.5 26.25
241
+ z
242
+
243
+ M 5 42.5
244
+ L 5 55
245
+ L 17.5 55
246
+ L 5 42.5
247
+ z
248
+ `, fill, strokeColor, strokeWidth);
249
+ }
250
+
251
+ public makeZoomIcon = () => {
252
+ const icon = document.createElementNS(svgNamespace, 'svg');
253
+ icon.setAttribute('viewBox', '0 0 100 100');
254
+
255
+ const addTextNode = (text: string, x: number, y: number) => {
256
+ const textNode = document.createElementNS(svgNamespace, 'text');
257
+ textNode.appendChild(document.createTextNode(text));
258
+ textNode.setAttribute('x', x.toString());
259
+ textNode.setAttribute('y', y.toString());
260
+ textNode.style.textAlign = 'center';
261
+ textNode.style.textAnchor = 'middle';
262
+ textNode.style.fontSize = '55px';
263
+ textNode.style.fill = 'var(--icon-color)';
264
+ textNode.style.fontFamily = 'monospace';
265
+
266
+ icon.appendChild(textNode);
267
+ };
268
+
269
+ addTextNode('+', 40, 45);
270
+ addTextNode('-', 70, 75);
271
+
272
+ return icon;
273
+ };
274
+
275
+ public makeTextIcon(textStyle: TextStyle) {
276
+ const icon = document.createElementNS(svgNamespace, 'svg');
277
+ icon.setAttribute('viewBox', '0 0 100 100');
278
+
279
+ const textNode = document.createElementNS(svgNamespace, 'text');
280
+ textNode.appendChild(document.createTextNode('T'));
281
+
282
+ textNode.style.fontFamily = textStyle.fontFamily;
283
+ textNode.style.fontWeight = textStyle.fontWeight ?? '';
284
+ textNode.style.fontVariant = textStyle.fontVariant ?? '';
285
+ textNode.style.fill = textStyle.renderingStyle.fill.toHexString();
286
+
287
+ textNode.style.textAnchor = 'middle';
288
+ textNode.setAttribute('x', '50');
289
+ textNode.setAttribute('y', '75');
290
+ textNode.style.fontSize = '65px';
291
+ textNode.style.filter = 'drop-shadow(0px 0px 10px var(--primary-shadow-color))';
292
+
293
+ icon.appendChild(textNode);
294
+
295
+ return icon;
296
+ }
297
+
298
+ public makePenIcon(tipThickness: number, color: string|Color4) {
299
+ if (color instanceof Color4) {
300
+ color = color.toHexString();
301
+ }
302
+
303
+ const icon = document.createElementNS(svgNamespace, 'svg');
304
+ icon.setAttribute('viewBox', '0 0 100 100');
305
+
306
+ const halfThickness = tipThickness / 2;
307
+
308
+ // Draw a pen-like shape
309
+ const primaryStrokeTipPath = `M14,63 L${50 - halfThickness},95 L${50 + halfThickness},90 L88,60 Z`;
310
+ const backgroundStrokeTipPath = `M14,63 L${50 - halfThickness},85 L${50 + halfThickness},83 L88,60 Z`;
311
+ icon.innerHTML = `
312
+ <defs>
313
+ ${checkerboardPatternDef}
314
+ </defs>
315
+ <g>
316
+ <!-- Pen grip -->
317
+ <path
318
+ d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
319
+ ${iconColorStrokeFill}
320
+ />
321
+ </g>
322
+ <g>
323
+ <!-- Checkerboard background for slightly transparent pens -->
324
+ <path d='${backgroundStrokeTipPath}' fill='${checkerboardPatternRef}'/>
325
+
326
+ <!-- Actual pen tip -->
327
+ <path
328
+ d='${primaryStrokeTipPath}'
329
+ fill='${color}'
330
+ stroke='${color}'
331
+ />
332
+ </g>
333
+ `;
334
+ return icon;
335
+ }
336
+
337
+ public makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory) {
338
+ const toolThickness = pen.getThickness();
339
+
340
+ const nowTime = (new Date()).getTime();
341
+ const startPoint: StrokeDataPoint = {
342
+ pos: Vec2.of(10, 10),
343
+ width: toolThickness / 5,
344
+ color: pen.getColor(),
345
+ time: nowTime - 100,
346
+ };
347
+ const endPoint: StrokeDataPoint = {
348
+ pos: Vec2.of(90, 90),
349
+ width: toolThickness / 5,
350
+ color: pen.getColor(),
351
+ time: nowTime,
352
+ };
353
+
354
+ const viewport = new Viewport(new EventDispatcher());
355
+ const builder = factory(startPoint, viewport);
356
+ builder.addPoint(endPoint);
357
+
358
+ const icon = document.createElementNS(svgNamespace, 'svg');
359
+ icon.setAttribute('viewBox', '0 0 100 100');
360
+ viewport.updateScreenSize(Vec2.of(100, 100));
361
+
362
+ const renderer = new SVGRenderer(icon, viewport);
363
+ builder.preview(renderer);
364
+
365
+ return icon;
366
+ }
367
+
368
+ public makePipetteIcon(color?: Color4) {
369
+ const icon = document.createElementNS(svgNamespace, 'svg');
370
+ const pipette = document.createElementNS(svgNamespace, 'path');
371
+
372
+ pipette.setAttribute('d', `
373
+ M 47,6
374
+ C 35,5 25,15 35,30
375
+ c -9.2,1.3 -15,0 -15,3
376
+ 0,2 5,5 15,7
377
+ V 81
378
+ L 40,90
379
+ h 6
380
+ L 40,80
381
+ V 40
382
+ h 15
383
+ v 40
384
+ l -6,10
385
+ h 6
386
+ l 5,-9.2
387
+ V 40
388
+ C 70,38 75,35 75,33
389
+ 75,30 69.2,31.2 60,30
390
+ 65,15 65,5 47,6
391
+ Z
392
+ `);
393
+ pipette.style.fill = 'var(--icon-color)';
394
+
395
+ if (color) {
396
+ const defs = document.createElementNS(svgNamespace, 'defs');
397
+ defs.innerHTML = checkerboardPatternDef;
398
+ icon.appendChild(defs);
399
+
400
+ const fluidBackground = document.createElementNS(svgNamespace, 'path');
401
+ const fluid = document.createElementNS(svgNamespace, 'path');
402
+
403
+ const fluidPathData = `
404
+ m 40,50 c 5,5 10,0 15,-5 V 80 L 50,90 H 45 L 40,80 Z
405
+ `;
406
+
407
+ fluid.setAttribute('d', fluidPathData);
408
+ fluidBackground.setAttribute('d', fluidPathData);
409
+
410
+ fluid.style.fill = color.toHexString();
411
+ fluidBackground.style.fill = checkerboardPatternRef;
412
+
413
+ icon.appendChild(fluidBackground);
414
+ icon.appendChild(fluid);
415
+ }
416
+ icon.appendChild(pipette);
417
+
418
+ icon.setAttribute('viewBox', '0 0 100 100');
419
+ return icon;
420
+ }
421
+
422
+ public makeResizeViewportIcon() {
423
+ return this.makeIconFromPath(`
424
+ M 75 5 75 10 90 10 90 25 95 25 95 5 75 5 z
425
+ M 15 15 15 30 20 30 20 20 30 20 30 15 15 15 z
426
+ M 84 15 82 17 81 16 81 20 85 20 84 19 86 17 84 15 z
427
+ M 26 24 24 26 26 28 25 29 29 29 29 25 28 26 26 24 z
428
+ M 25 71 26 72 24 74 26 76 28 74 29 75 29 71 25 71 z
429
+ M 15 75 15 85 25 85 25 80 20 80 20 75 15 75 z
430
+ M 90 75 90 90 75 90 75 95 95 95 95 75 90 75 z
431
+ M 81 81 81 85 82 84 84 86 86 84 84 82 85 81 81 81 z
432
+ `);
433
+ }
434
+
435
+ public makeDuplicateSelectionIcon() {
436
+ return this.makeIconFromPath(`
437
+ M 45,10 45,55 90,55 90,10 45,10 z
438
+ M 10,25 10,90 70,90 70,60 40,60 40,25 10,25 z
439
+ `);
440
+ }
441
+
442
+ public makeDeleteSelectionIcon() {
443
+ const strokeWidth = '5px';
444
+ const strokeColor = 'var(--icon-color)';
445
+ const fillColor = 'none';
446
+
447
+ return this.makeIconFromPath(`
448
+ M 10,10 90,90
449
+ M 10,90 90,10
450
+ `, fillColor, strokeColor, strokeWidth);
451
+ }
452
+
453
+ public makeSaveIcon() {
454
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
455
+ svg.innerHTML = `
456
+ <style>
457
+ .toolbar-save-icon {
458
+ stroke: var(--icon-color);
459
+ stroke-width: 10;
460
+ stroke-linejoin: round;
461
+ stroke-linecap: round;
462
+ fill: none;
463
+ }
464
+ </style>
465
+ <path
466
+ d='
467
+ M 15,55 30,70 85,20
468
+ '
469
+ class='toolbar-save-icon'
470
+ />
471
+ `;
472
+ svg.setAttribute('viewBox', '0 0 100 100');
473
+ return svg;
474
+ }
475
+
476
+ }
@@ -1,4 +1,4 @@
1
1
 
2
2
  export * from './widgets/lib';
3
- export * as icons from './icons';
4
3
  export * from './makeColorInput';
4
+ export { default as IconProvider } from './IconProvider';
@@ -2,7 +2,6 @@
2
2
 
3
3
  export interface ToolbarLocalization {
4
4
  fontLabel: string;
5
- anyDevicePanning: string;
6
5
  touchPanning: string;
7
6
  outlinedRectanglePen: string;
8
7
  filledRectanglePen: string;
@@ -54,7 +53,6 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
54
53
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
55
54
 
56
55
  touchPanning: 'Touchscreen panning',
57
- anyDevicePanning: 'Any device panning',
58
56
 
59
57
  freehandPen: 'Freehand',
60
58
  arrowPen: 'Arrow',
@@ -2,7 +2,6 @@ import Color4 from '../Color4';
2
2
  import Editor from '../Editor';
3
3
  import PipetteTool from '../tools/PipetteTool';
4
4
  import { EditorEventType } from '../types';
5
- import { makePipetteIcon } from './icons';
6
5
 
7
6
  type OnColorChangeListener = (color: Color4)=>void;
8
7
 
@@ -72,7 +71,7 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
72
71
  pipetteButton.setAttribute('alt', pipetteButton.title);
73
72
 
74
73
  const updatePipetteIcon = (color?: Color4) => {
75
- pipetteButton.replaceChildren(makePipetteIcon(color));
74
+ pipetteButton.replaceChildren(editor.icons.makePipetteIcon(color));
76
75
  };
77
76
  updatePipetteIcon();
78
77