js-draw 1.28.0 → 1.29.1

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.
@@ -28,7 +28,19 @@ export declare class Viewport {
28
28
  screenToCanvas(screenPoint: Point2): Point2;
29
29
  /** @returns the given point transformed into screen coordinates. */
30
30
  canvasToScreen(canvasPoint: Point2): Point2;
31
- /** @returns a command that transforms the canvas by `transform`. */
31
+ /**
32
+ * @returns a command that transforms the canvas by `transform`.
33
+ *
34
+ * For example, `Viewport.transformBy(moveRight).apply(editor)` would move the canvas to the right
35
+ * (and thus the viewport to the left):
36
+ * ```ts,runnable
37
+ * import { Editor, Viewport, Mat33, Vec2 } from 'js-draw';
38
+ * const editor = new Editor(document.body);
39
+ * const moveRight = Mat33.translation(Vec2.unitX.times(500));
40
+ * // Move the **canvas** right by 500 units:
41
+ * Viewport.transformBy(moveRight).apply(editor);
42
+ * ```
43
+ */
32
44
  static transformBy(transform: Mat33): ViewportTransform;
33
45
  /**
34
46
  * Updates the transformation directly. Using `transformBy` is preferred.
@@ -49,7 +49,19 @@ export class Viewport {
49
49
  canvasToScreen(canvasPoint) {
50
50
  return this.transform.transformVec2(canvasPoint);
51
51
  }
52
- /** @returns a command that transforms the canvas by `transform`. */
52
+ /**
53
+ * @returns a command that transforms the canvas by `transform`.
54
+ *
55
+ * For example, `Viewport.transformBy(moveRight).apply(editor)` would move the canvas to the right
56
+ * (and thus the viewport to the left):
57
+ * ```ts,runnable
58
+ * import { Editor, Viewport, Mat33, Vec2 } from 'js-draw';
59
+ * const editor = new Editor(document.body);
60
+ * const moveRight = Mat33.translation(Vec2.unitX.times(500));
61
+ * // Move the **canvas** right by 500 units:
62
+ * Viewport.transformBy(moveRight).apply(editor);
63
+ * ```
64
+ */
53
65
  static transformBy(transform) {
54
66
  return new Viewport.ViewportTransform(transform);
55
67
  }
@@ -206,9 +218,11 @@ export class Viewport {
206
218
  }
207
219
  return transform;
208
220
  }
209
- // Returns a Command that transforms the view such that [rect] is visible, and perhaps
221
+ // Returns a Command that transforms the view such that `toMakeVisible` is visible, and perhaps
210
222
  // centered in the viewport.
211
- // Returns null if no transformation is necessary
223
+ //
224
+ // If the content is already roughly centered in the screen and at a reasonable zoom level,
225
+ // the resultant command does nothing.
212
226
  //
213
227
  // @see {@link computeZoomToTransform}
214
228
  zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
@@ -9,11 +9,21 @@ import Command from '../commands/Command';
9
9
  export declare const sortLeavesByZIndex: (leaves: Array<ImageNode>) => void;
10
10
  export declare enum EditorImageEventType {
11
11
  ExportViewportChanged = 0,
12
- AutoresizeModeChanged = 1
12
+ AutoresizeModeChanged = 1,
13
+ ComponentAdded = 2,
14
+ ComponentRemoved = 3
13
15
  }
14
- export type EditorImageNotifier = EventDispatcher<EditorImageEventType, {
16
+ interface StateChangeEvent {
17
+ kind: EditorImageEventType.ExportViewportChanged | EditorImageEventType.AutoresizeModeChanged;
15
18
  image: EditorImage;
16
- }>;
19
+ }
20
+ interface ComponentAddRemoveEvent {
21
+ kind: EditorImageEventType.ComponentAdded | EditorImageEventType.ComponentRemoved;
22
+ image: EditorImage;
23
+ componentId: string;
24
+ }
25
+ export type EditorImageEvent = StateChangeEvent | ComponentAddRemoveEvent;
26
+ export type EditorImageNotifier = EventDispatcher<EditorImageEventType, EditorImageEvent>;
17
27
  /**
18
28
  * A callback used to
19
29
  * 1. pause the render process
@@ -16,8 +16,14 @@ export const sortLeavesByZIndex = (leaves) => {
16
16
  };
17
17
  export var EditorImageEventType;
18
18
  (function (EditorImageEventType) {
19
+ // @internal
19
20
  EditorImageEventType[EditorImageEventType["ExportViewportChanged"] = 0] = "ExportViewportChanged";
21
+ // @internal
20
22
  EditorImageEventType[EditorImageEventType["AutoresizeModeChanged"] = 1] = "AutoresizeModeChanged";
23
+ // Type for events fired whenever components are added to the image
24
+ EditorImageEventType[EditorImageEventType["ComponentAdded"] = 2] = "ComponentAdded";
25
+ // Type for events fired whenever components are removed from the image
26
+ EditorImageEventType[EditorImageEventType["ComponentRemoved"] = 3] = "ComponentRemoved";
21
27
  })(EditorImageEventType || (EditorImageEventType = {}));
22
28
  let debugMode = false;
23
29
  /**
@@ -43,7 +49,7 @@ class EditorImage {
43
49
  this.settingExportRect = false;
44
50
  this.root = new RootImageNode();
45
51
  this.background = new RootImageNode();
46
- this.componentsById = Object.create(null);
52
+ this.componentsById = new Map();
47
53
  this.notifier = new EventDispatcher();
48
54
  this.importExportViewport = new Viewport(() => {
49
55
  this.onExportViewportChanged();
@@ -170,13 +176,25 @@ class EditorImage {
170
176
  /** Called whenever (just after) an element is completely removed. @internal */
171
177
  onDestroyElement(elem) {
172
178
  this.componentCount--;
173
- delete this.componentsById[elem.getId()];
179
+ const componentId = elem.getId();
180
+ this.componentsById.delete(componentId);
181
+ this.notifier.dispatch(EditorImageEventType.ComponentRemoved, {
182
+ kind: EditorImageEventType.ComponentRemoved,
183
+ image: this,
184
+ componentId: componentId,
185
+ });
174
186
  this.autoresizeExportViewport();
175
187
  }
176
188
  /** Called just after an element is added. @internal */
177
189
  onElementAdded(elem) {
178
190
  this.componentCount++;
179
- this.componentsById[elem.getId()] = elem;
191
+ const elementId = elem.getId();
192
+ this.componentsById.set(elem.getId(), elem);
193
+ this.notifier.dispatch(EditorImageEventType.ComponentAdded, {
194
+ kind: EditorImageEventType.ComponentAdded,
195
+ image: this,
196
+ componentId: elementId,
197
+ });
180
198
  this.autoresizeExportViewport();
181
199
  }
182
200
  /**
@@ -185,7 +203,7 @@ class EditorImage {
185
203
  * @see {@link AbstractComponent.getId}
186
204
  */
187
205
  lookupElement(id) {
188
- return this.componentsById[id] ?? null;
206
+ return this.componentsById.get(id) ?? null;
189
207
  }
190
208
  addComponentDirectly(elem) {
191
209
  // Because onAddToImage can affect the element's bounding box,
@@ -298,6 +316,7 @@ class EditorImage {
298
316
  if (shouldAutoresize !== this.shouldAutoresizeExportViewport) {
299
317
  this.shouldAutoresizeExportViewport = shouldAutoresize;
300
318
  this.notifier.dispatch(EditorImageEventType.AutoresizeModeChanged, {
319
+ kind: EditorImageEventType.AutoresizeModeChanged,
301
320
  image: this,
302
321
  });
303
322
  }
@@ -343,6 +362,7 @@ class EditorImage {
343
362
  // called.
344
363
  if (!this.settingExportRect) {
345
364
  this.notifier.dispatch(EditorImageEventType.ExportViewportChanged, {
365
+ kind: EditorImageEventType.ExportViewportChanged,
346
366
  image: this,
347
367
  });
348
368
  }
@@ -58,6 +58,7 @@ export default class IconProvider {
58
58
  makePipetteIcon(color?: Color4): IconElemType;
59
59
  makeShapeAutocorrectIcon(): IconElemType;
60
60
  makeStrokeSmoothingIcon(): IconElemType;
61
+ makePressureSensitivityIcon(): IconElemType;
61
62
  /** Unused. @deprecated */
62
63
  makeFormatSelectionIcon(): IconElemType;
63
64
  makeResizeImageToSelectionIcon(): IconElemType;
@@ -645,6 +645,27 @@ class IconProvider {
645
645
  M 75,17.3 40,59.7 38.2,77.6 55.5,72.4 90.5,30 Z
646
646
  `, fill, strokeColor, '7px');
647
647
  }
648
+ makePressureSensitivityIcon() {
649
+ const icon = document.createElementNS(svgNamespace, 'svg');
650
+ icon.setAttribute('viewBox', '4 -10 100 100');
651
+ icon.replaceChildren(...createSvgPaths({
652
+ d: `
653
+ M 39.7,77.7
654
+ C 39.7,77.7 3.4,78.1 4.2,60 4.7,45.2 33.2,30.5 40,25 55.9,12.1 7.4,4.8 7.4,4.8
655
+ c 0,0 40.2,5.5 40.2,15.4
656
+ C 47.6,29.1 21.2,35.1 23.9,60 25,70 39.7,77.7 39.7,77.7
657
+ Z`,
658
+ fill: 'var(--icon-color)',
659
+ stroke: 'var(--icon-color)',
660
+ 'stroke-width': '2px',
661
+ }, {
662
+ d: 'M 86.4,15.6 101.4,28.8 65,70 47.5,74.6 50,56.7Z',
663
+ fill: 'transparent',
664
+ stroke: 'var(--icon-color)',
665
+ 'stroke-width': '6px',
666
+ }));
667
+ return icon;
668
+ }
648
669
  /** Unused. @deprecated */
649
670
  makeFormatSelectionIcon() {
650
671
  return this.makeIconFromPath(`
@@ -57,6 +57,7 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
57
57
  about: string;
58
58
  inputStabilization: string;
59
59
  strokeAutocorrect: string;
60
+ pressureSensitivity: string;
60
61
  errorImageHasZeroSize: string;
61
62
  describeTheImage: string;
62
63
  fileInput__loading: string;
@@ -67,6 +68,7 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
67
68
  penDropdown__penTypeHelpText: string;
68
69
  penDropdown__autocorrectHelpText: string;
69
70
  penDropdown__stabilizationHelpText: string;
71
+ penDropdown__pressureSensitivityHelpText: string;
70
72
  handDropdown__baseHelpText: string;
71
73
  handDropdown__zoomDisplayHelpText: string;
72
74
  handDropdown__zoomInHelpText: string;
@@ -47,6 +47,7 @@ export const defaultToolbarLocalization = {
47
47
  about: 'About',
48
48
  inputStabilization: 'Stabilization',
49
49
  strokeAutocorrect: 'Autocorrect',
50
+ pressureSensitivity: 'Pressure',
50
51
  touchPanning: 'Scroll with touch',
51
52
  roundedTipPen: 'Round',
52
53
  roundedTipPen2: 'Polyline',
@@ -69,6 +70,7 @@ export const defaultToolbarLocalization = {
69
70
  penDropdown__penTypeHelpText: 'Changes the pen style.\n\nEither a “pen” style or “shape” can be chosen. Choosing a “pen” style draws freehand lines. Choosing a “shape” draws shapes.',
70
71
  penDropdown__autocorrectHelpText: 'Converts approximate freehand lines and rectangles to perfect ones.\n\nThe pen must be held stationary at the end of a stroke to trigger a correction.',
71
72
  penDropdown__stabilizationHelpText: 'Draws smoother strokes.\n\nThis also adds a short delay between the mouse/stylus and the stroke.',
73
+ penDropdown__pressureSensitivityHelpText: 'Changes the thickness of strokes according to how hard you press, when using a compatible device like a stylus.',
72
74
  handDropdown__baseHelpText: 'This tool is responsible for scrolling, rotating, and zooming the editor.',
73
75
  handDropdown__zoomInHelpText: 'Zooms in.',
74
76
  handDropdown__zoomOutHelpText: 'Zooms out.',
@@ -216,13 +216,19 @@ class PenToolWidget extends BaseToolWidget {
216
216
  autocorrectOption.setOnInputListener((enabled) => {
217
217
  this.tool.setStrokeAutocorrectEnabled(enabled);
218
218
  });
219
+ const pressureSensitivityOption = addToggleButton(this.localizationTable.pressureSensitivity, this.editor.icons.makePressureSensitivityIcon());
220
+ pressureSensitivityOption.setOnInputListener((enabled) => {
221
+ this.tool.setPressureSensitivityEnabled(enabled);
222
+ });
219
223
  // Help text
220
224
  autocorrectOption.addHelpText(this.localizationTable.penDropdown__autocorrectHelpText);
221
225
  stabilizationOption.addHelpText(this.localizationTable.penDropdown__stabilizationHelpText);
226
+ pressureSensitivityOption.addHelpText(this.localizationTable.penDropdown__pressureSensitivityHelpText);
222
227
  return {
223
228
  update: () => {
224
229
  stabilizationOption.setChecked(!!this.tool.getInputMapper());
225
230
  autocorrectOption.setChecked(this.tool.getStrokeAutocorrectionEnabled());
231
+ pressureSensitivityOption.setChecked(this.tool.getPressureSensitivityEnabled());
226
232
  },
227
233
  addTo: (parent) => {
228
234
  parent.appendChild(container);
@@ -305,6 +311,7 @@ class PenToolWidget extends BaseToolWidget {
305
311
  strokeFactoryId: this.getCurrentPenType()?.id,
306
312
  inputStabilization: !!this.tool.getInputMapper(),
307
313
  strokeAutocorrect: this.tool.getStrokeAutocorrectionEnabled(),
314
+ pressureSensitivity: this.tool.getPressureSensitivityEnabled(),
308
315
  };
309
316
  }
310
317
  deserializeFrom(state) {
@@ -340,6 +347,9 @@ class PenToolWidget extends BaseToolWidget {
340
347
  if (state.strokeAutocorrect !== undefined) {
341
348
  this.tool.setStrokeAutocorrectEnabled(!!state.strokeAutocorrect);
342
349
  }
350
+ if (state.pressureSensitivity !== undefined) {
351
+ this.tool.setPressureSensitivityEnabled(!!state.pressureSensitivity);
352
+ }
343
353
  }
344
354
  }
345
355
  // A counter variable that ensures different HTML elements are given unique names/ids.
@@ -157,14 +157,16 @@ export default class InputStabilizer extends InputMapper {
157
157
  onEvent(event) {
158
158
  if (isPointerEvt(event) || event.kind === InputEvtType.GestureCancelEvt) {
159
159
  if (event.kind === InputEvtType.PointerDownEvt) {
160
- if (this.stabilizer === null) {
161
- this.stabilizer = new StylusInputStabilizer(event.current.screenPos, (screenPoint, timeStamp) => this.emitPointerMove(screenPoint, timeStamp), this.options);
162
- }
163
- else if (event.allPointers.length > 1) {
160
+ if (event.allPointers.length > 1) {
164
161
  // Do not attempt to stabilize multiple pointers.
165
- this.stabilizer.cancel();
162
+ this.stabilizer?.cancel();
166
163
  this.stabilizer = null;
167
164
  }
165
+ else {
166
+ // Create a new stabilizer for the new stroke.
167
+ this.stabilizer?.cancel();
168
+ this.stabilizer = new StylusInputStabilizer(event.current.screenPos, (screenPoint, timeStamp) => this.emitPointerMove(screenPoint, timeStamp), this.options);
169
+ }
168
170
  }
169
171
  const handled = this.mapPointerEvent(event);
170
172
  if (event.kind === InputEvtType.PointerUpEvt ||
@@ -27,6 +27,7 @@ export default class Pen extends BaseTool {
27
27
  private styleValue;
28
28
  private style;
29
29
  private shapeAutocompletionEnabled;
30
+ private pressureSensitivityEnabled;
30
31
  private autocorrectedShape;
31
32
  private lastAutocorrectedShape;
32
33
  private removedAutocorrectedShapeTime;
@@ -59,6 +60,8 @@ export default class Pen extends BaseTool {
59
60
  setHasStabilization(hasStabilization: boolean): void;
60
61
  setStrokeAutocorrectEnabled(enabled: boolean): void;
61
62
  getStrokeAutocorrectionEnabled(): boolean;
63
+ setPressureSensitivityEnabled(enabled: boolean): void;
64
+ getPressureSensitivityEnabled(): boolean;
62
65
  getThickness(): number;
63
66
  getColor(): Color4;
64
67
  getStrokeFactory(): ComponentBuilderFactory;
@@ -25,6 +25,7 @@ export default class Pen extends BaseTool {
25
25
  this.currentDeviceType = null;
26
26
  this.currentPointerId = null;
27
27
  this.shapeAutocompletionEnabled = false;
28
+ this.pressureSensitivityEnabled = true;
28
29
  this.autocorrectedShape = null;
29
30
  this.lastAutocorrectedShape = null;
30
31
  this.removedAutocorrectedShapeTime = 0;
@@ -47,6 +48,7 @@ export default class Pen extends BaseTool {
47
48
  // Converts a `pointer` to a `StrokeDataPoint`.
48
49
  toStrokePoint(pointer) {
49
50
  const minPressure = 0.3;
51
+ const defaultPressure = 0.5; // https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pressure#value
50
52
  let pressure = Math.max(pointer.pressure ?? 1.0, minPressure);
51
53
  if (!isFinite(pressure)) {
52
54
  console.warn('Non-finite pressure!', pointer);
@@ -56,6 +58,9 @@ export default class Pen extends BaseTool {
56
58
  console.assert(isFinite(pointer.screenPos.length()), 'Non-finite screen position!');
57
59
  console.assert(isFinite(pointer.timeStamp), 'Non-finite timeStamp on pointer!');
58
60
  const pos = pointer.canvasPos;
61
+ if (!this.getPressureSensitivityEnabled()) {
62
+ pressure = defaultPressure;
63
+ }
59
64
  return {
60
65
  pos,
61
66
  width: pressure * this.getPressureMultiplier(),
@@ -288,6 +293,15 @@ export default class Pen extends BaseTool {
288
293
  getStrokeAutocorrectionEnabled() {
289
294
  return this.shapeAutocompletionEnabled;
290
295
  }
296
+ setPressureSensitivityEnabled(enabled) {
297
+ if (enabled !== this.pressureSensitivityEnabled) {
298
+ this.pressureSensitivityEnabled = enabled;
299
+ this.noteUpdated();
300
+ }
301
+ }
302
+ getPressureSensitivityEnabled() {
303
+ return this.pressureSensitivityEnabled;
304
+ }
291
305
  getThickness() {
292
306
  return this.style.thickness;
293
307
  }
@@ -8,6 +8,7 @@ interface ElementToPropertiesMap {
8
8
  d: string;
9
9
  fill: string;
10
10
  stroke: string;
11
+ 'stroke-width': string;
11
12
  transform: string;
12
13
  };
13
14
  rect: {
@@ -5,5 +5,5 @@
5
5
  */
6
6
  export default {
7
7
  // Note: Auto-updated by prebuild.js:
8
- number: '1.28.0',
8
+ number: '1.29.1',
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.28.0",
3
+ "version": "1.29.1",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -64,11 +64,11 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.28.0",
67
+ "@js-draw/math": "^1.29.1",
68
68
  "@melloware/coloris": "0.22.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@js-draw/build-tool": "^1.28.0",
71
+ "@js-draw/build-tool": "^1.29.1",
72
72
  "@types/jest": "29.5.5",
73
73
  "@types/jsdom": "21.1.3"
74
74
  },
@@ -86,5 +86,5 @@
86
86
  "freehand",
87
87
  "svg"
88
88
  ],
89
- "gitHead": "03242acbf2250f5a41aa86a84146bb08583cf955"
89
+ "gitHead": "c82d0fc2a1c0d6257a54fde9cf40d5a05cb51213"
90
90
  }
@@ -1,9 +1,11 @@
1
1
  // Repeat selector for extra specificity
2
2
  :root .toolbar--pen-tool-toggle-buttons.toolbar--pen-tool-toggle-buttons {
3
3
  display: flex;
4
+ flex-wrap: wrap;
4
5
  justify-content: stretch;
5
6
  padding-top: 0;
6
7
  padding-bottom: 5px;
8
+ gap: 5px;
7
9
 
8
10
  // Some styles rely on left being start.
9
11
  direction: ltr;
@@ -11,21 +13,19 @@
11
13
  & > * {
12
14
  flex-grow: 1;
13
15
  text-align: start;
14
- margin-inline-end: 5px;
16
+ width: min-content;
15
17
 
16
- .icon {
17
- margin: 0;
18
- margin-inline-start: 4px;
18
+ > .icon {
19
+ margin-inline-start: 6px;
19
20
  margin-inline-end: 10px;
20
21
  }
21
22
  }
22
23
 
23
- & > :nth-child(1) {
24
- direction: ltr;
24
+ & > :nth-child(2) {
25
+ text-align: center;
25
26
  }
26
27
 
27
- & > :last-child {
28
- // Reverse the last (show icon on the right)
28
+ & > :nth-child(3) {
29
29
  direction: rtl;
30
30
  }
31
31
  }