js-draw 0.1.3 → 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.
@@ -14,7 +14,7 @@ import { EditorLocalization } from './localization';
14
14
  export interface EditorSettings {
15
15
  renderingMode: RenderingMode;
16
16
  localization: Partial<EditorLocalization>;
17
- wheelEventsEnabled: boolean;
17
+ wheelEventsEnabled: boolean | 'only-if-focused';
18
18
  }
19
19
  export declare class Editor {
20
20
  private container;
@@ -48,6 +48,7 @@ export declare class Editor {
48
48
  rerender(showImageBounds?: boolean): void;
49
49
  drawWetInk(...path: RenderablePathSpec[]): void;
50
50
  clearWetInk(): void;
51
+ focus(): void;
51
52
  createHTMLOverlay(overlay: HTMLElement): {
52
53
  remove: () => void;
53
54
  };
@@ -183,13 +183,24 @@ export class Editor {
183
183
  })) {
184
184
  evt.preventDefault();
185
185
  }
186
+ else if (evt.key === 'Escape') {
187
+ this.renderingRegion.blur();
188
+ }
186
189
  });
187
190
  this.container.addEventListener('wheel', evt => {
188
191
  let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
189
- // Process wheel events if the ctrl key is down -- we do want to handle
192
+ // Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
190
193
  // pinch-zooming.
191
- if (!this.settings.wheelEventsEnabled && !evt.ctrlKey) {
192
- return;
194
+ if (!evt.ctrlKey) {
195
+ if (!this.settings.wheelEventsEnabled) {
196
+ return;
197
+ }
198
+ else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
199
+ const focusedChild = this.container.querySelector(':focus');
200
+ if (!focusedChild) {
201
+ return;
202
+ }
203
+ }
193
204
  }
194
205
  if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
195
206
  delta = delta.times(15);
@@ -297,6 +308,10 @@ export class Editor {
297
308
  clearWetInk() {
298
309
  this.display.getWetInkRenderer().clear();
299
310
  }
311
+ // Focuses the region used for text input
312
+ focus() {
313
+ this.renderingRegion.focus();
314
+ }
300
315
  createHTMLOverlay(overlay) {
301
316
  overlay.classList.add('overlay');
302
317
  this.container.appendChild(overlay);
@@ -162,7 +162,7 @@ export default class SVGLoader {
162
162
  }
163
163
  const style = {
164
164
  size: fontSize,
165
- fontFamily: computedStyles.fontFamily || 'sans',
165
+ fontFamily: computedStyles.fontFamily || 'sans-serif',
166
166
  renderingStyle: {
167
167
  fill: Color4.fromString(computedStyles.fill)
168
168
  },
@@ -11,10 +11,12 @@ export default class Text extends AbstractComponent {
11
11
  }
12
12
  static applyTextStyles(ctx, style) {
13
13
  var _a, _b;
14
+ // Quote the font family if necessary.
15
+ const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
14
16
  ctx.font = [
15
17
  ((_a = style.size) !== null && _a !== void 0 ? _a : 12) + 'px',
16
18
  (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '',
17
- `"${style.fontFamily.replace(/["]/g, '\\"')}"`,
19
+ `${fontFamily}`,
18
20
  style.fontWeight
19
21
  ].join(' ');
20
22
  ctx.textAlign = 'left';
@@ -338,25 +338,51 @@ class TextToolWidget extends ToolbarWidget {
338
338
  return makeTextIcon(textStyle);
339
339
  }
340
340
  fillDropdown(dropdown) {
341
+ const fontRow = document.createElement('div');
341
342
  const colorRow = document.createElement('div');
343
+ const fontInput = document.createElement('select');
344
+ const fontLabel = document.createElement('label');
342
345
  const colorInput = document.createElement('input');
343
346
  const colorLabel = document.createElement('label');
347
+ const fontsInInput = new Set();
348
+ const addFontToInput = (fontName) => {
349
+ const option = document.createElement('option');
350
+ option.value = fontName;
351
+ option.textContent = fontName;
352
+ fontInput.appendChild(option);
353
+ fontsInInput.add(fontName);
354
+ };
355
+ fontLabel.innerText = this.localizationTable.fontLabel;
344
356
  colorLabel.innerText = this.localizationTable.colorLabel;
345
357
  colorInput.classList.add('coloris_input');
346
358
  colorInput.type = 'button';
347
359
  colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
348
360
  colorLabel.setAttribute('for', colorInput.id);
361
+ addFontToInput('monospace');
362
+ addFontToInput('serif');
363
+ addFontToInput('sans-serif');
364
+ fontInput.id = `${toolbarCSSPrefix}-text-font-input-${TextToolWidget.idCounter++}`;
365
+ fontLabel.setAttribute('for', fontInput.id);
366
+ fontInput.onchange = () => {
367
+ this.tool.setFontFamily(fontInput.value);
368
+ };
349
369
  colorInput.oninput = () => {
350
370
  this.tool.setColor(Color4.fromString(colorInput.value));
351
371
  };
352
372
  colorRow.appendChild(colorLabel);
353
373
  colorRow.appendChild(colorInput);
374
+ fontRow.appendChild(fontLabel);
375
+ fontRow.appendChild(fontInput);
354
376
  this.updateDropdownInputs = () => {
355
377
  const style = this.tool.getTextStyle();
356
378
  colorInput.value = style.renderingStyle.fill.toHexString();
379
+ if (!fontsInInput.has(style.fontFamily)) {
380
+ addFontToInput(style.fontFamily);
381
+ }
382
+ fontInput.value = style.fontFamily;
357
383
  };
358
384
  this.updateDropdownInputs();
359
- dropdown.appendChild(colorRow);
385
+ dropdown.replaceChildren(colorRow, fontRow);
360
386
  return true;
361
387
  }
362
388
  }
@@ -125,6 +125,7 @@ export const makeTextIcon = (textStyle) => {
125
125
  textNode.setAttribute('x', '50');
126
126
  textNode.setAttribute('y', '75');
127
127
  textNode.style.fontSize = '65px';
128
+ textNode.style.filter = 'drop-shadow(0px 0px 10px var(--primary-shadow-color))';
128
129
  icon.appendChild(textNode);
129
130
  return icon;
130
131
  };
@@ -1,4 +1,5 @@
1
1
  export interface ToolbarLocalization {
2
+ fontLabel: string;
2
3
  anyDevicePanning: string;
3
4
  touchPanning: string;
4
5
  outlinedRectanglePen: string;
@@ -5,6 +5,7 @@ export const defaultToolbarLocalization = {
5
5
  handTool: 'Pan',
6
6
  thicknessLabel: 'Thickness: ',
7
7
  colorLabel: 'Color: ',
8
+ fontLabel: 'Font: ',
8
9
  resizeImageToSelection: 'Resize image to selection',
9
10
  deleteSelection: 'Delete selection',
10
11
  undo: 'Undo',
@@ -21,6 +21,7 @@ export default class TextTool extends BaseTool {
21
21
  private startTextInput;
22
22
  setEnabled(enabled: boolean): void;
23
23
  onPointerDown({ current, allPointers }: PointerEvt): boolean;
24
+ onGestureCancel(): void;
24
25
  private dispatchUpdateEvent;
25
26
  setFontFamily(fontFamily: string): void;
26
27
  setColor(color: Color4): void;
@@ -70,6 +70,7 @@ export default class TextTool extends BaseTool {
70
70
  (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.remove();
71
71
  return;
72
72
  }
73
+ const viewport = this.editor.viewport;
73
74
  const textScreenPos = this.editor.viewport.canvasToScreen(this.textTargetPosition);
74
75
  this.textInputElem.type = 'text';
75
76
  this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
@@ -80,8 +81,12 @@ export default class TextTool extends BaseTool {
80
81
  this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
81
82
  this.textInputElem.style.position = 'relative';
82
83
  this.textInputElem.style.left = `${textScreenPos.x}px`;
84
+ this.textInputElem.style.top = `${textScreenPos.y}px`;
85
+ this.textInputElem.style.margin = '0';
86
+ const rotation = viewport.getRotationAngle();
83
87
  const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
84
- this.textInputElem.style.top = `${textScreenPos.y - ascent}px`;
88
+ this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
89
+ this.textInputElem.style.transformOrigin = 'top left';
85
90
  }
86
91
  startTextInput(textCanvasPos, initialText) {
87
92
  this.flushInput();
@@ -96,15 +101,25 @@ export default class TextTool extends BaseTool {
96
101
  }
97
102
  };
98
103
  this.textInputElem.onblur = () => {
99
- this.flushInput();
104
+ // Don't remove the input within the context of a blur event handler.
105
+ // Doing so causes errors.
106
+ setTimeout(() => this.flushInput(), 0);
100
107
  };
101
108
  this.textInputElem.onkeyup = (evt) => {
109
+ var _a;
102
110
  if (evt.key === 'Enter') {
103
111
  this.flushInput();
112
+ this.editor.focus();
113
+ }
114
+ else if (evt.key === 'Escape') {
115
+ // Cancel input.
116
+ (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.remove();
117
+ this.textInputElem = null;
118
+ this.editor.focus();
104
119
  }
105
120
  };
106
121
  this.textEditOverlay.replaceChildren(this.textInputElem);
107
- setTimeout(() => this.textInputElem.focus(), 100);
122
+ setTimeout(() => { var _a; return (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.focus(); }, 0);
108
123
  }
109
124
  setEnabled(enabled) {
110
125
  super.setEnabled(enabled);
@@ -123,6 +138,10 @@ export default class TextTool extends BaseTool {
123
138
  }
124
139
  return false;
125
140
  }
141
+ onGestureCancel() {
142
+ this.flushInput();
143
+ this.editor.focus();
144
+ }
126
145
  dispatchUpdateEvent() {
127
146
  this.updateTextInput();
128
147
  this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
@@ -27,9 +27,16 @@
27
27
  wheelEventsEnabled: false,
28
28
  });
29
29
  editor1.addToolbar();
30
+ editor1.loadFromSVG('<svg><text>Wheel events disabled.</text></svg>');
30
31
 
31
- const editor2 = new jsdraw.Editor(document.body);
32
+ const editor2 = new jsdraw.Editor(document.body, {
33
+ wheelEventsEnabled: 'only-if-focused',
34
+ });
32
35
  editor2.addToolbar();
36
+ editor2.loadFromSVG('<svg><text>Wheel events enabled, only if focused.</text></svg>');
37
+
38
+ const editor3 = new jsdraw.Editor(document.body);
39
+ editor3.addToolbar();
33
40
  </script>
34
41
  </body>
35
42
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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
 
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) {
@@ -399,6 +409,11 @@ export class Editor {
399
409
  this.display.getWetInkRenderer().clear();
400
410
  }
401
411
 
412
+ // Focuses the region used for text input
413
+ public focus() {
414
+ this.renderingRegion.focus();
415
+ }
416
+
402
417
  public createHTMLOverlay(overlay: HTMLElement) {
403
418
  overlay.classList.add('overlay');
404
419
  this.container.appendChild(overlay);
package/src/SVGLoader.ts CHANGED
@@ -198,7 +198,7 @@ export default class SVGLoader implements ImageLoader {
198
198
  }
199
199
  const style: TextStyle = {
200
200
  size: fontSize,
201
- fontFamily: computedStyles.fontFamily || 'sans',
201
+ fontFamily: computedStyles.fontFamily || 'sans-serif',
202
202
  renderingStyle: {
203
203
  fill: Color4.fromString(computedStyles.fill)
204
204
  },
@@ -23,12 +23,16 @@ export default class Text extends AbstractComponent {
23
23
  }
24
24
 
25
25
  public static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle) {
26
+ // Quote the font family if necessary.
27
+ const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
28
+
26
29
  ctx.font = [
27
30
  (style.size ?? 12) + 'px',
28
31
  style.fontWeight ?? '',
29
- `"${style.fontFamily.replace(/["]/g, '\\"')}"`,
32
+ `${fontFamily}`,
30
33
  style.fontWeight
31
34
  ].join(' ');
35
+
32
36
  ctx.textAlign = 'left';
33
37
  }
34
38
 
@@ -428,10 +428,25 @@ class TextToolWidget extends ToolbarWidget {
428
428
 
429
429
  private static idCounter: number = 0;
430
430
  protected fillDropdown(dropdown: HTMLElement): boolean {
431
+ const fontRow = document.createElement('div');
431
432
  const colorRow = document.createElement('div');
433
+
434
+ const fontInput = document.createElement('select');
435
+ const fontLabel = document.createElement('label');
436
+
432
437
  const colorInput = document.createElement('input');
433
438
  const colorLabel = document.createElement('label');
434
439
 
440
+ const fontsInInput = new Set();
441
+ const addFontToInput = (fontName: string) => {
442
+ const option = document.createElement('option');
443
+ option.value = fontName;
444
+ option.textContent = fontName;
445
+ fontInput.appendChild(option);
446
+ fontsInInput.add(fontName);
447
+ };
448
+
449
+ fontLabel.innerText = this.localizationTable.fontLabel;
435
450
  colorLabel.innerText = this.localizationTable.colorLabel;
436
451
 
437
452
  colorInput.classList.add('coloris_input');
@@ -439,6 +454,16 @@ class TextToolWidget extends ToolbarWidget {
439
454
  colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
440
455
  colorLabel.setAttribute('for', colorInput.id);
441
456
 
457
+ addFontToInput('monospace');
458
+ addFontToInput('serif');
459
+ addFontToInput('sans-serif');
460
+ fontInput.id = `${toolbarCSSPrefix}-text-font-input-${TextToolWidget.idCounter++}`;
461
+ fontLabel.setAttribute('for', fontInput.id);
462
+
463
+ fontInput.onchange = () => {
464
+ this.tool.setFontFamily(fontInput.value);
465
+ };
466
+
442
467
  colorInput.oninput = () => {
443
468
  this.tool.setColor(Color4.fromString(colorInput.value));
444
469
  };
@@ -446,14 +471,21 @@ class TextToolWidget extends ToolbarWidget {
446
471
  colorRow.appendChild(colorLabel);
447
472
  colorRow.appendChild(colorInput);
448
473
 
474
+ fontRow.appendChild(fontLabel);
475
+ fontRow.appendChild(fontInput);
476
+
449
477
  this.updateDropdownInputs = () => {
450
478
  const style = this.tool.getTextStyle();
451
479
  colorInput.value = style.renderingStyle.fill.toHexString();
480
+
481
+ if (!fontsInInput.has(style.fontFamily)) {
482
+ addFontToInput(style.fontFamily);
483
+ }
484
+ fontInput.value = style.fontFamily;
452
485
  };
453
486
  this.updateDropdownInputs();
454
487
 
455
- dropdown.appendChild(colorRow);
456
-
488
+ dropdown.replaceChildren(colorRow, fontRow);
457
489
  return true;
458
490
  }
459
491
  }
@@ -143,6 +143,7 @@ export const makeTextIcon = (textStyle: TextStyle) => {
143
143
  textNode.setAttribute('x', '50');
144
144
  textNode.setAttribute('y', '75');
145
145
  textNode.style.fontSize = '65px';
146
+ textNode.style.filter = 'drop-shadow(0px 0px 10px var(--primary-shadow-color))';
146
147
 
147
148
  icon.appendChild(textNode);
148
149
 
@@ -1,6 +1,7 @@
1
1
 
2
2
 
3
3
  export interface ToolbarLocalization {
4
+ fontLabel: string;
4
5
  anyDevicePanning: string;
5
6
  touchPanning: string;
6
7
  outlinedRectanglePen: string;
@@ -32,6 +33,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
32
33
  handTool: 'Pan',
33
34
  thicknessLabel: 'Thickness: ',
34
35
  colorLabel: 'Color: ',
36
+ fontLabel: 'Font: ',
35
37
  resizeImageToSelection: 'Resize image to selection',
36
38
  deleteSelection: 'Delete selection',
37
39
  undo: 'Undo',
@@ -12,6 +12,9 @@
12
12
  flex-direction: row;
13
13
  justify-content: center;
14
14
 
15
+ /* Display above selection dialogs, etc. */
16
+ z-index: 1000;
17
+
15
18
  font-family: system-ui, -apple-system, sans-serif;
16
19
  }
17
20
 
@@ -41,13 +44,13 @@
41
44
  background-color: var(--primary-background-color);
42
45
  color: var(--primary-foreground-color);
43
46
  border: none;
44
- box-shadow: 0px 0px 2px var(--primary-foreground-color);
47
+ box-shadow: 0px 0px 2px var(--primary-shadow-color);
45
48
 
46
49
  transition: background-color 0.25s ease, box-shadow 0.25s ease, opacity 0.3s ease;
47
50
  }
48
51
 
49
52
  .toolbar-button:hover, .toolbar-root button:not(:disabled):hover {
50
- box-shadow: 0px 2px 4px var(--primary-foreground-color);
53
+ box-shadow: 0px 2px 4px var(--primary-shadow-color);
51
54
  }
52
55
 
53
56
  .toolbar-root button:disabled {
@@ -90,7 +93,7 @@
90
93
  /* Prevent overlap/being displayed under the undo/redo buttons */
91
94
  z-index: 2;
92
95
  background-color: var(--primary-background-color);
93
- box-shadow: 0px 3px 3px var(--primary-foreground-color);
96
+ box-shadow: 0px 3px 3px var(--primary-shadow-color);
94
97
  }
95
98
 
96
99
  .toolbar-buttonGroup {
@@ -92,6 +92,7 @@ export default class TextTool extends BaseTool {
92
92
  return;
93
93
  }
94
94
 
95
+ const viewport = this.editor.viewport;
95
96
  const textScreenPos = this.editor.viewport.canvasToScreen(this.textTargetPosition);
96
97
  this.textInputElem.type = 'text';
97
98
  this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
@@ -103,8 +104,13 @@ export default class TextTool extends BaseTool {
103
104
 
104
105
  this.textInputElem.style.position = 'relative';
105
106
  this.textInputElem.style.left = `${textScreenPos.x}px`;
107
+ this.textInputElem.style.top = `${textScreenPos.y}px`;
108
+ this.textInputElem.style.margin = '0';
109
+
110
+ const rotation = viewport.getRotationAngle();
106
111
  const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
107
- this.textInputElem.style.top = `${textScreenPos.y - ascent}px`;
112
+ this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
113
+ this.textInputElem.style.transformOrigin = 'top left';
108
114
  }
109
115
 
110
116
  private startTextInput(textCanvasPos: Vec2, initialText: string) {
@@ -121,16 +127,24 @@ export default class TextTool extends BaseTool {
121
127
  }
122
128
  };
123
129
  this.textInputElem.onblur = () => {
124
- this.flushInput();
130
+ // Don't remove the input within the context of a blur event handler.
131
+ // Doing so causes errors.
132
+ setTimeout(() => this.flushInput(), 0);
125
133
  };
126
134
  this.textInputElem.onkeyup = (evt) => {
127
135
  if (evt.key === 'Enter') {
128
136
  this.flushInput();
137
+ this.editor.focus();
138
+ } else if (evt.key === 'Escape') {
139
+ // Cancel input.
140
+ this.textInputElem?.remove();
141
+ this.textInputElem = null;
142
+ this.editor.focus();
129
143
  }
130
144
  };
131
145
 
132
146
  this.textEditOverlay.replaceChildren(this.textInputElem);
133
- setTimeout(() => this.textInputElem!.focus(), 100);
147
+ setTimeout(() => this.textInputElem?.focus(), 0);
134
148
  }
135
149
 
136
150
  public setEnabled(enabled: boolean) {
@@ -156,6 +170,11 @@ export default class TextTool extends BaseTool {
156
170
  return false;
157
171
  }
158
172
 
173
+ public onGestureCancel(): void {
174
+ this.flushInput();
175
+ this.editor.focus();
176
+ }
177
+
159
178
  private dispatchUpdateEvent() {
160
179
  this.updateTextInput();
161
180
  this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {