js-draw 0.1.8 → 0.1.11

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 (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +15 -3
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +3 -0
  5. package/dist/src/Editor.js +45 -16
  6. package/dist/src/UndoRedoHistory.js +3 -0
  7. package/dist/src/Viewport.js +2 -0
  8. package/dist/src/bundle/bundled.d.ts +2 -1
  9. package/dist/src/bundle/bundled.js +2 -1
  10. package/dist/src/geometry/LineSegment2.d.ts +1 -0
  11. package/dist/src/geometry/LineSegment2.js +16 -0
  12. package/dist/src/geometry/Rect2.d.ts +1 -0
  13. package/dist/src/geometry/Rect2.js +16 -0
  14. package/dist/src/localizations/en.d.ts +3 -0
  15. package/dist/src/localizations/en.js +4 -0
  16. package/dist/src/localizations/es.d.ts +3 -0
  17. package/dist/src/localizations/es.js +18 -0
  18. package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
  19. package/dist/src/localizations/getLocalizationTable.js +43 -0
  20. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
  21. package/dist/src/toolbar/HTMLToolbar.js +10 -8
  22. package/dist/src/toolbar/icons.js +13 -13
  23. package/dist/src/toolbar/localization.d.ts +2 -0
  24. package/dist/src/toolbar/localization.js +2 -0
  25. package/dist/src/toolbar/makeColorInput.js +22 -8
  26. package/dist/src/toolbar/widgets/BaseWidget.js +29 -0
  27. package/dist/src/toolbar/widgets/HandToolWidget.js +8 -1
  28. package/dist/src/tools/BaseTool.d.ts +2 -1
  29. package/dist/src/tools/BaseTool.js +3 -0
  30. package/dist/src/tools/PanZoom.d.ts +2 -1
  31. package/dist/src/tools/PanZoom.js +10 -4
  32. package/dist/src/tools/SelectionTool.d.ts +9 -2
  33. package/dist/src/tools/SelectionTool.js +131 -19
  34. package/dist/src/tools/ToolController.js +6 -2
  35. package/dist/src/tools/localization.d.ts +1 -0
  36. package/dist/src/tools/localization.js +1 -0
  37. package/dist/src/types.d.ts +9 -2
  38. package/dist/src/types.js +1 -0
  39. package/package.json +9 -1
  40. package/src/Editor.ts +54 -14
  41. package/src/UndoRedoHistory.ts +4 -0
  42. package/src/Viewport.ts +2 -0
  43. package/src/bundle/bundled.ts +2 -1
  44. package/src/geometry/LineSegment2.test.ts +15 -0
  45. package/src/geometry/LineSegment2.ts +20 -0
  46. package/src/geometry/Rect2.test.ts +20 -7
  47. package/src/geometry/Rect2.ts +19 -1
  48. package/src/localizations/en.ts +8 -0
  49. package/src/localizations/es.ts +62 -0
  50. package/src/localizations/getLocalizationTable.test.ts +27 -0
  51. package/src/localizations/getLocalizationTable.ts +53 -0
  52. package/src/toolbar/HTMLToolbar.ts +11 -9
  53. package/src/toolbar/icons.ts +13 -13
  54. package/src/toolbar/localization.ts +4 -0
  55. package/src/toolbar/makeColorInput.ts +25 -10
  56. package/src/toolbar/toolbar.css +6 -2
  57. package/src/toolbar/widgets/BaseWidget.ts +34 -0
  58. package/src/toolbar/widgets/HandToolWidget.ts +12 -1
  59. package/src/tools/BaseTool.ts +5 -1
  60. package/src/tools/PanZoom.ts +13 -4
  61. package/src/tools/SelectionTool.test.ts +24 -1
  62. package/src/tools/SelectionTool.ts +158 -23
  63. package/src/tools/ToolController.ts +6 -2
  64. package/src/tools/localization.ts +2 -0
  65. package/src/types.ts +10 -1
  66. package/dist-test/test-dist-bundle.html +0 -42
@@ -147,14 +147,27 @@ describe('Rect2', () => {
147
147
  it('division of empty square', () => {
148
148
  expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
149
149
  });
150
+
151
+ it('division of rectangle', () => {
152
+ expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
153
+ [
154
+ new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
155
+ new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
156
+ ]
157
+ );
158
+ });
150
159
  });
151
160
 
152
- it('division of rectangle', () => {
153
- expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
154
- [
155
- new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
156
- new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
157
- ]
158
- );
161
+ describe('should correctly return the closest point on the edge of a rectangle', () => {
162
+ it('with the unit square', () => {
163
+ const rect = Rect2.unitSquare;
164
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.zero)).objEq(Vec2.zero);
165
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, -1))).objEq(Vec2.zero);
166
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, 0.5))).objEq(Vec2.of(0, 0.5));
167
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(1, 0.5))).objEq(Vec2.of(1, 0.5));
168
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6));
169
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(2, 0.5))).objEq(Vec2.of(1, 0.5));
170
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6));
171
+ });
159
172
  });
160
173
  });
@@ -51,6 +51,7 @@ export default class Rect2 {
51
51
  return new Rect2(vec.x + this.x, vec.y + this.y, this.w, this.h);
52
52
  }
53
53
 
54
+ // Returns a copy of this with the given size (but same top-left).
54
55
  public resizedTo(size: Vec2): Rect2 {
55
56
  return new Rect2(this.x, this.y, size.x, size.y);
56
57
  }
@@ -163,6 +164,23 @@ export default class Rect2 {
163
164
  );
164
165
  }
165
166
 
167
+ public getClosestPointOnBoundaryTo(target: Point2) {
168
+ const closestEdgePoints = this.getEdges().map(edge => {
169
+ return edge.closestPointTo(target);
170
+ });
171
+
172
+ let closest: Point2|null = null;
173
+ let closestDist: number|null = null;
174
+ for (const point of closestEdgePoints) {
175
+ const dist = point.minus(target).length();
176
+ if (closestDist === null || dist < closestDist) {
177
+ closest = point;
178
+ closestDist = dist;
179
+ }
180
+ }
181
+ return closest!;
182
+ }
183
+
166
184
  public get corners(): Point2[] {
167
185
  return [
168
186
  this.bottomRight,
@@ -172,7 +190,7 @@ export default class Rect2 {
172
190
  ];
173
191
  }
174
192
 
175
- public get maxDimension(): number {
193
+ public get maxDimension() {
176
194
  return Math.max(this.w, this.h);
177
195
  }
178
196
 
@@ -0,0 +1,8 @@
1
+ import { defaultEditorLocalization, EditorLocalization } from '../localization';
2
+
3
+ // Default localizations are already in English.
4
+ const localization: EditorLocalization = {
5
+ ...defaultEditorLocalization,
6
+ };
7
+
8
+ export default localization;
@@ -0,0 +1,62 @@
1
+ import { defaultEditorLocalization, EditorLocalization } from '../localization';
2
+
3
+ // A partial Spanish localization.
4
+ const localization: EditorLocalization = {
5
+ ...defaultEditorLocalization,
6
+
7
+ // Strings for the main editor interface
8
+ // (see src/localization.ts)
9
+ loading: (percentage: number) => `Cargando: ${percentage}%...`,
10
+ imageEditor: 'Editor de dibujos',
11
+
12
+ undoAnnouncement: (commandDescription: string) => `${commandDescription} fue deshecho`,
13
+ redoAnnouncement: (commandDescription: string) => `${commandDescription} fue rehecho`,
14
+ undo: 'Deshace',
15
+ redo: 'Rehace',
16
+
17
+ // Strings for the toolbar
18
+ // (see src/toolbar/localization.ts)
19
+ pen: 'Lapiz',
20
+ eraser: 'Borrador',
21
+ select: 'Selecciona',
22
+ thicknessLabel: 'Tamaño: ',
23
+ colorLabel: 'Color: ',
24
+ doneLoading: 'El cargado terminó',
25
+ fontLabel: 'Fuente: ',
26
+ anyDevicePanning: 'Mover la pantalla con todo dispotivo',
27
+ touchPanning: 'Mover la pantalla con un dedo',
28
+ touchPanTool: 'Instrumento de mover la pantalla con un dedo',
29
+ outlinedRectanglePen: 'Rectángulo con nada más que un borde',
30
+ filledRectanglePen: 'Rectángulo sin borde',
31
+ linePen: 'Línea',
32
+ arrowPen: 'Flecha',
33
+ freehandPen: 'Dibuja sin restricción de forma',
34
+ selectObjectType: 'Forma de dibuja:',
35
+ handTool: 'Mover',
36
+ zoom: 'Zoom',
37
+ resetView: 'Reiniciar vista',
38
+ resizeImageToSelection: 'Redimensionar la imagen a lo que está seleccionado',
39
+ deleteSelection: 'Borra la selección',
40
+ duplicateSelection: 'Duplica la selección',
41
+ pickColorFronScreen: 'Selecciona un color de la pantalla',
42
+ dropdownShown(toolName: string): string {
43
+ return `Menú por ${toolName} es visible`;
44
+ },
45
+ dropdownHidden: function (toolName: string): string {
46
+ return `Menú por ${toolName} fue ocultado`;
47
+ },
48
+ colorChangedAnnouncement: function (color: string): string {
49
+ return `Color fue cambiado a ${color}`;
50
+ },
51
+ keyboardPanZoom: 'Mover la pantalla con el teclado',
52
+ penTool: function (penId: number): string {
53
+ return `Lapiz ${penId}`;
54
+ },
55
+ selectionTool: 'Selecciona',
56
+ eraserTool: 'Borrador',
57
+ textTool: 'Texto',
58
+ enterTextToInsert: 'Entra texto',
59
+ rerenderAsText: 'Redibuja la pantalla al texto',
60
+ };
61
+
62
+ export default localization;
@@ -0,0 +1,27 @@
1
+
2
+ import { defaultEditorLocalization } from '../localization';
3
+ import en from './en';
4
+ import es from './es';
5
+ import getLocalizationTable from './getLocalizationTable';
6
+
7
+ describe('getLocalizationTable', () => {
8
+ it('should return the en localization for es_TEST', () => {
9
+ expect(getLocalizationTable([ 'es_TEST' ]) === es).toBe(true);
10
+ });
11
+
12
+ it('should return the default localization for unsupported language', () => {
13
+ expect(getLocalizationTable([ 'test' ]) === defaultEditorLocalization).toBe(true);
14
+ });
15
+
16
+ it('should return the first localization matching a language in the list of user locales', () => {
17
+ expect(getLocalizationTable([ 'test_TEST1', 'test_TEST2', 'test_TEST3', 'en_TEST', 'notalanguage']) === en).toBe(true);
18
+ });
19
+
20
+ it('should return the default localization for unsupported language', () => {
21
+ expect(getLocalizationTable([ 'test' ]) === defaultEditorLocalization).toBe(true);
22
+ });
23
+
24
+ it('should return first of user\'s supported languages', () => {
25
+ expect(getLocalizationTable([ 'es_MX', 'es_ES', 'en_US' ]) === es).toBe(true);
26
+ });
27
+ });
@@ -0,0 +1,53 @@
1
+
2
+ import { defaultEditorLocalization, EditorLocalization } from '../localization';
3
+ import en from './en';
4
+ import es from './es';
5
+
6
+ const allLocales: Record<string, EditorLocalization> = {
7
+ en,
8
+ es,
9
+ };
10
+
11
+ // [locale]: A string in the format languageCode_Region or just languageCode. For example, en_US.
12
+ const languageFromLocale = (locale: string) => {
13
+ const matches = /^(\w+)[_-](\w+)$/.exec(locale);
14
+ if (!matches) {
15
+ // If not in languageCode_region format, the locale should be the
16
+ // languageCode. Return that.
17
+ return locale;
18
+ }
19
+
20
+ return matches[1];
21
+ };
22
+
23
+ const getLocalizationTable = (userLocales?: readonly string[]): EditorLocalization => {
24
+ userLocales ??= navigator.languages;
25
+
26
+ let prevLanguage: string|undefined;
27
+ for (const locale of userLocales) {
28
+ const language = languageFromLocale(locale);
29
+
30
+ // If the specific localization of the language is not available, but
31
+ // a localization for the language is,
32
+ if (prevLanguage && language !== prevLanguage) {
33
+ if (prevLanguage in allLocales) {
34
+ return allLocales[prevLanguage];
35
+ }
36
+ }
37
+
38
+ // If the full locale (e.g. en_US) is available,
39
+ if (locale in allLocales) {
40
+ return allLocales[locale];
41
+ }
42
+
43
+ prevLanguage = language;
44
+ }
45
+
46
+ if (prevLanguage && prevLanguage in allLocales) {
47
+ return allLocales[prevLanguage];
48
+ } else {
49
+ return defaultEditorLocalization;
50
+ }
51
+ };
52
+
53
+ export default getLocalizationTable;
@@ -25,6 +25,8 @@ export const toolbarCSSPrefix = 'toolbar-';
25
25
  export default class HTMLToolbar {
26
26
  private container: HTMLElement;
27
27
 
28
+ private static colorisStarted: boolean = false;
29
+
28
30
  public constructor(
29
31
  private editor: Editor, parent: HTMLElement,
30
32
  private localizationTable: ToolbarLocalization = defaultToolbarLocalization,
@@ -34,7 +36,10 @@ export default class HTMLToolbar {
34
36
  this.container.setAttribute('role', 'toolbar');
35
37
  parent.appendChild(this.container);
36
38
 
37
- colorisInit();
39
+ if (!HTMLToolbar.colorisStarted) {
40
+ colorisInit();
41
+ HTMLToolbar.colorisStarted = true;
42
+ }
38
43
  this.setupColorPickers();
39
44
  }
40
45
 
@@ -133,13 +138,13 @@ export default class HTMLToolbar {
133
138
  undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
134
139
 
135
140
  const undoButton = this.addActionButton({
136
- label: 'Undo',
141
+ label: this.localizationTable.undo,
137
142
  icon: makeUndoIcon()
138
143
  }, () => {
139
144
  this.editor.history.undo();
140
145
  }, undoRedoGroup);
141
146
  const redoButton = this.addActionButton({
142
- label: 'Redo',
147
+ label: this.localizationTable.redo,
143
148
  icon: makeRedoIcon(),
144
149
  }, () => {
145
150
  this.editor.history.redo();
@@ -195,12 +200,9 @@ export default class HTMLToolbar {
195
200
  (new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
196
201
  }
197
202
 
198
- for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
199
- if (!(tool instanceof PanZoom)) {
200
- throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
201
- }
202
-
203
- (new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
203
+ const panZoomTool = toolController.getMatchingTools(ToolType.PanZoom)[0];
204
+ if (panZoomTool && panZoomTool instanceof PanZoom) {
205
+ (new HandToolWidget(this.editor, panZoomTool, this.localizationTable)).addTo(this.container);
204
206
  }
205
207
 
206
208
  this.setupColorPickers();
@@ -9,11 +9,11 @@ import { StrokeDataPoint } from '../types';
9
9
  import Viewport from '../Viewport';
10
10
 
11
11
  const svgNamespace = 'http://www.w3.org/2000/svg';
12
- const primaryForegroundFill = `
13
- style='fill: var(--primary-foreground-color);'
12
+ const iconColorFill = `
13
+ style='fill: var(--icon-color);'
14
14
  `;
15
- const primaryForegroundStrokeFill = `
16
- style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
15
+ const iconColorStrokeFill = `
16
+ style='fill: var(--icon-color); stroke: var(--icon-color);'
17
17
  `;
18
18
  const checkerboardPatternDef = `
19
19
  <pattern
@@ -39,7 +39,7 @@ export const makeRedoIcon = (mirror: boolean = false) => {
39
39
  icon.innerHTML = `
40
40
  <style>
41
41
  .toolbar-svg-undo-redo-icon {
42
- stroke: var(--primary-foreground-color);
42
+ stroke: var(--icon-color);
43
43
  stroke-width: 12;
44
44
  stroke-linejoin: round;
45
45
  stroke-linecap: round;
@@ -63,7 +63,7 @@ export const makeDropdownIcon = () => {
63
63
  <g>
64
64
  <path
65
65
  d='M5,10 L50,90 L95,10 Z'
66
- ${primaryForegroundFill}
66
+ ${iconColorFill}
67
67
  />
68
68
  </g>
69
69
  `;
@@ -80,7 +80,7 @@ export const makeEraserIcon = () => {
80
80
  <rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
81
81
  <rect
82
82
  x=10 y=10 width=80 height=50
83
- ${primaryForegroundFill}
83
+ ${iconColorFill}
84
84
  />
85
85
  </g>
86
86
  `;
@@ -132,7 +132,7 @@ export const makeHandToolIcon = () => {
132
132
 
133
133
  fill='none'
134
134
  style='
135
- stroke: var(--primary-foreground-color);
135
+ stroke: var(--icon-color);
136
136
  stroke-width: 2;
137
137
  '
138
138
  />
@@ -175,7 +175,7 @@ export const makeTouchPanningIcon = () => {
175
175
  '
176
176
  fill='none'
177
177
  style='
178
- stroke: var(--primary-foreground-color);
178
+ stroke: var(--icon-color);
179
179
  stroke-width: 2;
180
180
  '
181
181
  />
@@ -241,7 +241,7 @@ export const makeAllDevicePanningIcon = () => {
241
241
  '
242
242
  fill='none'
243
243
  style='
244
- stroke: var(--primary-foreground-color);
244
+ stroke: var(--icon-color);
245
245
  stroke-width: 2;
246
246
  '
247
247
  />
@@ -263,7 +263,7 @@ export const makeZoomIcon = () => {
263
263
  textNode.style.textAlign = 'center';
264
264
  textNode.style.textAnchor = 'middle';
265
265
  textNode.style.fontSize = '55px';
266
- textNode.style.fill = 'var(--primary-foreground-color)';
266
+ textNode.style.fill = 'var(--icon-color)';
267
267
  textNode.style.fontFamily = 'monospace';
268
268
 
269
269
  icon.appendChild(textNode);
@@ -315,7 +315,7 @@ export const makePenIcon = (tipThickness: number, color: string) => {
315
315
  <!-- Pen grip -->
316
316
  <path
317
317
  d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
318
- ${primaryForegroundStrokeFill}
318
+ ${iconColorStrokeFill}
319
319
  />
320
320
  </g>
321
321
  <g>
@@ -389,7 +389,7 @@ export const makePipetteIcon = (color?: Color4) => {
389
389
  65,15 65,5 47,6
390
390
  Z
391
391
  `);
392
- pipette.style.fill = 'var(--primary-foreground-color)';
392
+ pipette.style.fill = 'var(--icon-color)';
393
393
 
394
394
  if (color) {
395
395
  const defs = document.createElementNS(svgNamespace, 'defs');
@@ -23,6 +23,8 @@ export interface ToolbarLocalization {
23
23
  undo: string;
24
24
  redo: string;
25
25
  zoom: string;
26
+ resetView: string;
27
+ selectionToolKeyboardShortcuts: string;
26
28
 
27
29
  dropdownShown: (toolName: string)=> string;
28
30
  dropdownHidden: (toolName: string)=> string;
@@ -36,6 +38,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
36
38
  select: 'Select',
37
39
  handTool: 'Pan',
38
40
  zoom: 'Zoom',
41
+ resetView: 'Reset view',
39
42
  thicknessLabel: 'Thickness: ',
40
43
  colorLabel: 'Color: ',
41
44
  fontLabel: 'Font: ',
@@ -46,6 +49,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
46
49
  redo: 'Redo',
47
50
  selectObjectType: 'Object type: ',
48
51
  pickColorFronScreen: 'Pick color from screen',
52
+ selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
49
53
 
50
54
  touchPanning: 'Touchscreen panning',
51
55
  anyDevicePanning: 'Any device panning',
@@ -20,7 +20,7 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
20
20
  colorInputContainer.appendChild(colorInput);
21
21
  addPipetteTool(editor, colorInputContainer, (color: Color4) => {
22
22
  colorInput.value = color.toHexString();
23
- handleColorInput();
23
+ onInputEnd();
24
24
 
25
25
  // Update the color preview, if it exists (may be managed by Coloris).
26
26
  const parentElem = colorInput.parentElement;
@@ -32,15 +32,20 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
32
32
  let currentColor: Color4|undefined;
33
33
  const handleColorInput = () => {
34
34
  currentColor = Color4.fromHex(colorInput.value);
35
- editor.announceForAccessibility(
36
- editor.localization.colorChangedAnnouncement(currentColor.toHexString())
37
- );
38
- onColorChange(currentColor);
35
+ };
36
+ const onInputEnd = () => {
37
+ handleColorInput();
39
38
 
40
- editor.notifier.dispatch(EditorEventType.ColorPickerColorSelected, {
41
- kind: EditorEventType.ColorPickerColorSelected,
42
- color: currentColor,
43
- });
39
+ if (currentColor) {
40
+ editor.announceForAccessibility(
41
+ editor.localization.colorChangedAnnouncement(currentColor.toHexString())
42
+ );
43
+ onColorChange(currentColor);
44
+ editor.notifier.dispatch(EditorEventType.ColorPickerColorSelected, {
45
+ kind: EditorEventType.ColorPickerColorSelected,
46
+ color: currentColor,
47
+ });
48
+ }
44
49
  };
45
50
 
46
51
  colorInput.oninput = handleColorInput;
@@ -55,6 +60,7 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
55
60
  kind: EditorEventType.ColorPickerToggled,
56
61
  open: false,
57
62
  });
63
+ onInputEnd();
58
64
  });
59
65
 
60
66
  return [ colorInput, colorInputContainer ];
@@ -72,10 +78,13 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
72
78
  updatePipetteIcon();
73
79
 
74
80
  const pipetteTool: PipetteTool|undefined = editor.toolController.getMatchingTools(ToolType.Pipette)[0] as PipetteTool|undefined;
75
- const pipetteColorSelect = (color: Color4|null) => {
81
+ const endColorSelectMode = () => {
76
82
  pipetteTool?.clearColorListener();
77
83
  updatePipetteIcon();
78
84
  pipetteButton.classList.remove('active');
85
+ };
86
+ const pipetteColorSelect = (color: Color4|null) => {
87
+ endColorSelectMode();
79
88
 
80
89
  if (color) {
81
90
  onColorChange(color);
@@ -90,6 +99,12 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
90
99
  };
91
100
 
92
101
  pipetteButton.onclick = () => {
102
+ // If already picking, cancel it.
103
+ if (pipetteButton.classList.contains('active')) {
104
+ endColorSelectMode();
105
+ return;
106
+ }
107
+
93
108
  pipetteTool?.setColorListener(
94
109
  pipetteColorPreview,
95
110
  pipetteColorSelect,
@@ -1,5 +1,7 @@
1
1
  .toolbar-root {
2
2
  background-color: var(--primary-background-color);
3
+ --icon-color: var(--primary-foreground-color);
4
+
3
5
 
4
6
  border: 1px solid var(--secondary-background-color);
5
7
  border-radius: 2px;
@@ -38,6 +40,7 @@
38
40
  text-align: center;
39
41
  border-radius: 6px;
40
42
 
43
+ --icon-color: var(--primary-foreground-color);
41
44
  background-color: var(--primary-background-color);
42
45
  color: var(--primary-foreground-color);
43
46
  border: none;
@@ -83,6 +86,7 @@
83
86
  .toolbar-toolContainer.selected .toolbar-button {
84
87
  background-color: var(--secondary-background-color);
85
88
  color: var(--secondary-foreground-color);
89
+ --icon-color: var(--secondary-foreground-color);
86
90
  }
87
91
 
88
92
  .toolbar-toolContainer:not(.selected):not(.dropdownShowable) > .toolbar-button > .toolbar-showHideDropdownIcon {
@@ -151,8 +155,7 @@
151
155
  }
152
156
 
153
157
  .toolbar-root .toolbar-zoomLevelEditor button {
154
- width: min-content;
155
- height: min-content;
158
+ min-width: 48px;
156
159
  }
157
160
 
158
161
  .color-input-container {
@@ -173,4 +176,5 @@
173
176
 
174
177
  .color-input-container .pipetteButton.active {
175
178
  background-color: var(--secondary-background-color);
179
+ --icon-color: var(--secondary-foreground-color);
176
180
  }
@@ -1,4 +1,5 @@
1
1
  import Editor from '../../Editor';
2
+ import { InputEvtType } from '../../types';
2
3
  import { toolbarCSSPrefix } from '../HTMLToolbar';
3
4
  import { makeDropdownIcon } from '../icons';
4
5
  import { ToolbarLocalization } from '../localization';
@@ -50,6 +51,39 @@ export default abstract class BaseWidget {
50
51
  }
51
52
 
52
53
  protected setupActionBtnClickListener(button: HTMLElement) {
54
+ const clickTriggers = { Enter: true, ' ': true, };
55
+ button.onkeydown = (evt) => {
56
+ let handled = false;
57
+
58
+ if (evt.key in clickTriggers) {
59
+ if (!this.disabled) {
60
+ this.handleClick();
61
+ handled = true;
62
+ }
63
+ }
64
+
65
+ // If we didn't do anything with the event, send it to the editor.
66
+ if (!handled) {
67
+ this.editor.toolController.dispatchInputEvent({
68
+ kind: InputEvtType.KeyPressEvent,
69
+ key: evt.key,
70
+ ctrlKey: evt.ctrlKey,
71
+ });
72
+ }
73
+ };
74
+
75
+ button.onkeyup = evt => {
76
+ if (evt.key in clickTriggers) {
77
+ return;
78
+ }
79
+
80
+ this.editor.toolController.dispatchInputEvent({
81
+ kind: InputEvtType.KeyUpEvent,
82
+ key: evt.key,
83
+ ctrlKey: evt.ctrlKey,
84
+ });
85
+ };
86
+
53
87
  button.onclick = () => {
54
88
  if (!this.disabled) {
55
89
  this.handleClick();
@@ -14,10 +14,12 @@ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor)
14
14
 
15
15
  const increaseButton = document.createElement('button');
16
16
  const decreaseButton = document.createElement('button');
17
+ const resetViewButton = document.createElement('button');
17
18
  const zoomLevelDisplay = document.createElement('span');
18
19
  increaseButton.innerText = '+';
19
20
  decreaseButton.innerText = '-';
20
- zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
21
+ resetViewButton.innerText = localizationTable.resetView;
22
+ zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton, resetViewButton);
21
23
 
22
24
  zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
23
25
  zoomLevelDisplay.classList.add('zoomDisplay');
@@ -42,6 +44,9 @@ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor)
42
44
  editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
43
45
  if (event.kind === EditorEventType.ViewportChanged) {
44
46
  updateZoomDisplay();
47
+
48
+ // Can't reset if already reset.
49
+ resetViewButton.disabled = event.newTransform.eq(Mat33.identity);
45
50
  }
46
51
  });
47
52
 
@@ -59,6 +64,12 @@ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor)
59
64
  zoomBy(4.0/5);
60
65
  };
61
66
 
67
+ resetViewButton.onclick = () => {
68
+ editor.dispatch(new Viewport.ViewportTransform(
69
+ editor.viewport.canvasToScreenTransform.inverse()
70
+ ), true);
71
+ };
72
+
62
73
  return zoomLevelRow;
63
74
  };
64
75
 
@@ -1,4 +1,4 @@
1
- import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent } from '../types';
1
+ import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types';
2
2
  import { ToolType } from './ToolController';
3
3
  import ToolEnabledGroup from './ToolEnabledGroup';
4
4
 
@@ -24,6 +24,10 @@ export default abstract class BaseTool implements PointerEvtListener {
24
24
  return false;
25
25
  }
26
26
 
27
+ public onKeyUp(_event: KeyUpEvent): boolean {
28
+ return false;
29
+ }
30
+
27
31
  public setEnabled(enabled: boolean) {
28
32
  this.enabled = enabled;
29
33
 
@@ -16,11 +16,13 @@ interface PinchData {
16
16
  dist: number;
17
17
  }
18
18
 
19
+
19
20
  export enum PanZoomMode {
20
21
  OneFingerTouchGestures = 0x1,
21
22
  TwoFingerTouchGestures = 0x1 << 1,
22
23
  RightClickDrags = 0x1 << 2,
23
24
  SinglePointerGestures = 0x1 << 3,
25
+ Keyboard = 0x1 << 4,
24
26
  }
25
27
 
26
28
  export default class PanZoom extends BaseTool {
@@ -156,9 +158,9 @@ export default class PanZoom extends BaseTool {
156
158
  }
157
159
 
158
160
  public onWheel({ delta, screenPos }: WheelEvt): boolean {
159
- if (this.transform === null) {
160
- this.transform = new Viewport.ViewportTransform(Mat33.identity);
161
- }
161
+ // Reset the transformation -- wheel events are individual events, so we don't
162
+ // need to unapply/reapply.
163
+ this.transform = new Viewport.ViewportTransform(Mat33.identity);
162
164
 
163
165
  const canvasPos = this.editor.viewport.screenToCanvas(screenPos);
164
166
  const toCanvas = this.editor.viewport.screenToCanvasTransform;
@@ -170,7 +172,7 @@ export default class PanZoom extends BaseTool {
170
172
  );
171
173
  const pinchZoomScaleFactor = 1.04;
172
174
  const transformUpdate = Mat33.scaling2D(
173
- Math.pow(pinchZoomScaleFactor, -delta.z), canvasPos
175
+ Math.max(0.25, Math.min(Math.pow(pinchZoomScaleFactor, -delta.z), 4)), canvasPos
174
176
  ).rightMul(
175
177
  Mat33.translation(translation)
176
178
  );
@@ -180,6 +182,13 @@ export default class PanZoom extends BaseTool {
180
182
  }
181
183
 
182
184
  public onKeyPress({ key }: KeyPressEvent): boolean {
185
+ if (!(this.mode & PanZoomMode.Keyboard)) {
186
+ return false;
187
+ }
188
+
189
+ // No need to keep the same the transform for keyboard events.
190
+ this.transform = new Viewport.ViewportTransform(Mat33.identity);
191
+
183
192
  let translation = Vec2.zero;
184
193
  let scale = 1;
185
194
  let rotation = 0;