js-draw 0.1.3 → 0.1.6

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 (39) 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 +19 -4
  6. package/dist/src/SVGLoader.js +6 -2
  7. package/dist/src/Viewport.d.ts +1 -1
  8. package/dist/src/Viewport.js +5 -5
  9. package/dist/src/components/Text.js +3 -1
  10. package/dist/src/localization.d.ts +2 -1
  11. package/dist/src/localization.js +2 -1
  12. package/dist/src/rendering/Display.d.ts +2 -0
  13. package/dist/src/rendering/Display.js +19 -0
  14. package/dist/src/rendering/localization.d.ts +5 -0
  15. package/dist/src/rendering/localization.js +4 -0
  16. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +24 -0
  17. package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
  18. package/dist/src/toolbar/HTMLToolbar.js +27 -1
  19. package/dist/src/toolbar/icons.js +1 -0
  20. package/dist/src/toolbar/localization.d.ts +1 -0
  21. package/dist/src/toolbar/localization.js +1 -0
  22. package/dist/src/tools/TextTool.d.ts +2 -0
  23. package/dist/src/tools/TextTool.js +25 -5
  24. package/dist-test/test-dist-bundle.html +8 -1
  25. package/package.json +1 -1
  26. package/src/Editor.css +12 -0
  27. package/src/Editor.ts +20 -5
  28. package/src/SVGLoader.ts +7 -2
  29. package/src/Viewport.ts +5 -5
  30. package/src/components/Text.ts +5 -1
  31. package/src/localization.ts +3 -1
  32. package/src/rendering/Display.ts +26 -0
  33. package/src/rendering/localization.ts +10 -0
  34. package/src/rendering/renderers/TextOnlyRenderer.ts +51 -0
  35. package/src/toolbar/HTMLToolbar.ts +34 -2
  36. package/src/toolbar/icons.ts +1 -0
  37. package/src/toolbar/localization.ts +2 -0
  38. package/src/toolbar/toolbar.css +6 -3
  39. package/src/tools/TextTool.ts +27 -4
@@ -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
 
@@ -1,10 +1,11 @@
1
1
  import { CommandLocalization, defaultCommandLocalization } from './commands/localization';
2
2
  import { defaultComponentLocalization, ImageComponentLocalization } from './components/localization';
3
+ import { defaultTextRendererLocalization, TextRendererLocalization } from './rendering/localization';
3
4
  import { defaultToolbarLocalization, ToolbarLocalization } from './toolbar/localization';
4
5
  import { defaultToolLocalization, ToolLocalization } from './tools/localization';
5
6
 
6
7
 
7
- export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
8
+ export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization, TextRendererLocalization {
8
9
  undoAnnouncement: (actionDescription: string)=> string;
9
10
  redoAnnouncement: (actionDescription: string)=> string;
10
11
  doneLoading: string;
@@ -17,6 +18,7 @@ export const defaultEditorLocalization: EditorLocalization = {
17
18
  ...defaultToolLocalization,
18
19
  ...defaultCommandLocalization,
19
20
  ...defaultComponentLocalization,
21
+ ...defaultTextRendererLocalization,
20
22
  loading: (percentage: number) => `Loading ${percentage}%...`,
21
23
  imageEditor: 'Image Editor',
22
24
  doneLoading: 'Done loading',
@@ -5,6 +5,7 @@ import { EditorEventType } from '../types';
5
5
  import DummyRenderer from './renderers/DummyRenderer';
6
6
  import { Vec2 } from '../geometry/Vec2';
7
7
  import RenderingCache from './caching/RenderingCache';
8
+ import TextOnlyRenderer from './renderers/TextOnlyRenderer';
8
9
 
9
10
  export enum RenderingMode {
10
11
  DummyRenderer,
@@ -15,6 +16,7 @@ export enum RenderingMode {
15
16
  export default class Display {
16
17
  private dryInkRenderer: AbstractRenderer;
17
18
  private wetInkRenderer: AbstractRenderer;
19
+ private textRenderer: TextOnlyRenderer;
18
20
  private cache: RenderingCache;
19
21
  private resizeSurfacesCallback?: ()=> void;
20
22
  private flattenCallback?: ()=> void;
@@ -31,6 +33,9 @@ export default class Display {
31
33
  throw new Error(`Unknown rendering mode, ${mode}!`);
32
34
  }
33
35
 
36
+ this.textRenderer = new TextOnlyRenderer(editor.viewport, editor.localization);
37
+ this.initializeTextRendering();
38
+
34
39
  const cacheBlockResolution = Vec2.of(600, 600);
35
40
  this.cache = new RenderingCache({
36
41
  createRenderer: () => {
@@ -129,6 +134,27 @@ export default class Display {
129
134
  };
130
135
  }
131
136
 
137
+ private initializeTextRendering() {
138
+ const textRendererOutputContainer = document.createElement('div');
139
+ textRendererOutputContainer.classList.add('textRendererOutputContainer');
140
+
141
+ const rerenderButton = document.createElement('button');
142
+ rerenderButton.classList.add('rerenderButton');
143
+ rerenderButton.innerText = this.editor.localization.rerenderAsText;
144
+
145
+ const rerenderOutput = document.createElement('div');
146
+ rerenderOutput.ariaLive = 'polite';
147
+
148
+ rerenderButton.onclick = () => {
149
+ this.textRenderer.clear();
150
+ this.editor.image.render(this.textRenderer, this.editor.viewport);
151
+ rerenderOutput.innerText = this.textRenderer.getDescription();
152
+ };
153
+
154
+ textRendererOutputContainer.replaceChildren(rerenderButton, rerenderOutput);
155
+ this.editor.createHTMLOverlay(textRendererOutputContainer);
156
+ }
157
+
132
158
  // Clears the drawing surfaces and otherwise prepares for a rerender.
133
159
  public startRerender(): AbstractRenderer {
134
160
  this.resizeSurfacesCallback?.();
@@ -0,0 +1,10 @@
1
+
2
+ export interface TextRendererLocalization {
3
+ textNode(content: string): string;
4
+ rerenderAsText: string;
5
+ }
6
+
7
+ export const defaultTextRendererLocalization: TextRendererLocalization = {
8
+ textNode: (content: string) => `Text: ${content}`,
9
+ rerenderAsText: 'Re-render as text',
10
+ };
@@ -0,0 +1,51 @@
1
+ import { TextStyle } from '../../components/Text';
2
+ import Mat33 from '../../geometry/Mat33';
3
+ import Rect2 from '../../geometry/Rect2';
4
+ import { Vec2 } from '../../geometry/Vec2';
5
+ import Vec3 from '../../geometry/Vec3';
6
+ import Viewport from '../../Viewport';
7
+ import { TextRendererLocalization } from '../localization';
8
+ import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
9
+
10
+ // Outputs a description of what was rendered.
11
+
12
+ export default class TextOnlyRenderer extends AbstractRenderer {
13
+ private descriptionBuilder: string[] = [];
14
+ public constructor(viewport: Viewport, private localizationTable: TextRendererLocalization) {
15
+ super(viewport);
16
+ }
17
+
18
+ public displaySize(): Vec3 {
19
+ // We don't have a graphical display, export a reasonable size.
20
+ return Vec2.of(500, 500);
21
+ }
22
+
23
+ public clear(): void {
24
+ this.descriptionBuilder = [];
25
+ }
26
+
27
+ public getDescription(): string {
28
+ return this.descriptionBuilder.join('\n');
29
+ }
30
+
31
+ protected beginPath(_startPoint: Vec3): void {
32
+ }
33
+ protected endPath(_style: RenderingStyle): void {
34
+ }
35
+ protected lineTo(_point: Vec3): void {
36
+ }
37
+ protected moveTo(_point: Vec3): void {
38
+ }
39
+ protected traceCubicBezierCurve(_p1: Vec3, _p2: Vec3, _p3: Vec3): void {
40
+ }
41
+ protected traceQuadraticBezierCurve(_controlPoint: Vec3, _endPoint: Vec3): void {
42
+ }
43
+ public drawText(text: string, _transform: Mat33, _style: TextStyle): void {
44
+ this.descriptionBuilder.push(this.localizationTable.textNode(text));
45
+ }
46
+ public isTooSmallToRender(rect: Rect2): boolean {
47
+ return rect.maxDimension < 10 / this.getSizeOfCanvasPixelOnScreen();
48
+ }
49
+ public drawPoints(..._points: Vec3[]): void {
50
+ }
51
+ }
@@ -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: 2;
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 {
@@ -19,6 +19,7 @@ export default class TextTool extends BaseTool {
19
19
  private textInputElem: HTMLInputElement|null = null;
20
20
  private textTargetPosition: Vec2|null = null;
21
21
  private textMeasuringCtx: CanvasRenderingContext2D|null = null;
22
+ private textRotation: number;
22
23
 
23
24
  public constructor(private editor: Editor, description: string, private localizationTable: ToolLocalization) {
24
25
  super(editor.notifier, description);
@@ -73,6 +74,8 @@ export default class TextTool extends BaseTool {
73
74
  this.textTargetPosition
74
75
  ).rightMul(
75
76
  Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())
77
+ ).rightMul(
78
+ Mat33.zRotation(this.textRotation)
76
79
  );
77
80
 
78
81
  const textComponent = new Text(
@@ -92,7 +95,8 @@ export default class TextTool extends BaseTool {
92
95
  return;
93
96
  }
94
97
 
95
- const textScreenPos = this.editor.viewport.canvasToScreen(this.textTargetPosition);
98
+ const viewport = this.editor.viewport;
99
+ const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
96
100
  this.textInputElem.type = 'text';
97
101
  this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
98
102
  this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
@@ -103,8 +107,13 @@ export default class TextTool extends BaseTool {
103
107
 
104
108
  this.textInputElem.style.position = 'relative';
105
109
  this.textInputElem.style.left = `${textScreenPos.x}px`;
110
+ this.textInputElem.style.top = `${textScreenPos.y}px`;
111
+ this.textInputElem.style.margin = '0';
112
+
113
+ const rotation = this.textRotation + viewport.getRotationAngle();
106
114
  const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
107
- this.textInputElem.style.top = `${textScreenPos.y - ascent}px`;
115
+ this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
116
+ this.textInputElem.style.transformOrigin = 'top left';
108
117
  }
109
118
 
110
119
  private startTextInput(textCanvasPos: Vec2, initialText: string) {
@@ -113,6 +122,7 @@ export default class TextTool extends BaseTool {
113
122
  this.textInputElem = document.createElement('input');
114
123
  this.textInputElem.value = initialText;
115
124
  this.textTargetPosition = textCanvasPos;
125
+ this.textRotation = -this.editor.viewport.getRotationAngle();
116
126
  this.updateTextInput();
117
127
 
118
128
  this.textInputElem.oninput = () => {
@@ -121,16 +131,24 @@ export default class TextTool extends BaseTool {
121
131
  }
122
132
  };
123
133
  this.textInputElem.onblur = () => {
124
- this.flushInput();
134
+ // Don't remove the input within the context of a blur event handler.
135
+ // Doing so causes errors.
136
+ setTimeout(() => this.flushInput(), 0);
125
137
  };
126
138
  this.textInputElem.onkeyup = (evt) => {
127
139
  if (evt.key === 'Enter') {
128
140
  this.flushInput();
141
+ this.editor.focus();
142
+ } else if (evt.key === 'Escape') {
143
+ // Cancel input.
144
+ this.textInputElem?.remove();
145
+ this.textInputElem = null;
146
+ this.editor.focus();
129
147
  }
130
148
  };
131
149
 
132
150
  this.textEditOverlay.replaceChildren(this.textInputElem);
133
- setTimeout(() => this.textInputElem!.focus(), 100);
151
+ setTimeout(() => this.textInputElem?.focus(), 0);
134
152
  }
135
153
 
136
154
  public setEnabled(enabled: boolean) {
@@ -156,6 +174,11 @@ export default class TextTool extends BaseTool {
156
174
  return false;
157
175
  }
158
176
 
177
+ public onGestureCancel(): void {
178
+ this.flushInput();
179
+ this.editor.focus();
180
+ }
181
+
159
182
  private dispatchUpdateEvent() {
160
183
  this.updateTextInput();
161
184
  this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {