js-draw 0.7.2 → 0.8.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 (44) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +1 -0
  2. package/CHANGELOG.md +4 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/components/Stroke.js +10 -3
  5. package/dist/src/components/builders/FreehandLineBuilder.d.ts +10 -23
  6. package/dist/src/components/builders/FreehandLineBuilder.js +70 -396
  7. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +36 -0
  8. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +339 -0
  9. package/dist/src/components/lib.d.ts +2 -0
  10. package/dist/src/components/lib.js +2 -0
  11. package/dist/src/components/util/StrokeSmoother.d.ts +35 -0
  12. package/dist/src/components/util/StrokeSmoother.js +206 -0
  13. package/dist/src/math/Mat33.d.ts +2 -0
  14. package/dist/src/math/Mat33.js +4 -0
  15. package/dist/src/math/Path.d.ts +2 -0
  16. package/dist/src/math/Path.js +39 -0
  17. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -0
  18. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
  19. package/dist/src/rendering/renderers/SVGRenderer.js +20 -0
  20. package/dist/src/toolbar/localization.d.ts +1 -0
  21. package/dist/src/toolbar/localization.js +1 -0
  22. package/dist/src/toolbar/widgets/PenToolWidget.js +6 -1
  23. package/dist/src/tools/Pen.d.ts +2 -2
  24. package/dist/src/tools/Pen.js +2 -2
  25. package/dist/src/tools/SelectionTool/Selection.d.ts +1 -0
  26. package/dist/src/tools/SelectionTool/Selection.js +8 -1
  27. package/dist/src/tools/ToolController.js +2 -1
  28. package/package.json +1 -1
  29. package/src/components/Stroke.ts +16 -3
  30. package/src/components/builders/FreehandLineBuilder.ts +54 -495
  31. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +454 -0
  32. package/src/components/lib.ts +3 -1
  33. package/src/components/util/StrokeSmoother.ts +290 -0
  34. package/src/math/Mat33.ts +5 -0
  35. package/src/math/Path.test.ts +25 -0
  36. package/src/math/Path.ts +45 -0
  37. package/src/rendering/renderers/CanvasRenderer.ts +2 -0
  38. package/src/rendering/renderers/SVGRenderer.ts +24 -0
  39. package/src/toolbar/localization.ts +2 -0
  40. package/src/toolbar/widgets/PenToolWidget.ts +6 -1
  41. package/src/tools/Pen.test.ts +2 -2
  42. package/src/tools/Pen.ts +1 -1
  43. package/src/tools/SelectionTool/Selection.ts +10 -1
  44. package/src/tools/ToolController.ts +2 -1
@@ -64,6 +64,8 @@ export default class CanvasRenderer extends AbstractRenderer {
64
64
  if (style.stroke) {
65
65
  this.ctx.strokeStyle = style.stroke.color.toHexString();
66
66
  this.ctx.lineWidth = this.getSizeOfCanvasPixelOnScreen() * style.stroke.width;
67
+ this.ctx.lineCap = 'round';
68
+ this.ctx.lineJoin = 'round';
67
69
  this.ctx.stroke();
68
70
  }
69
71
  this.ctx.closePath();
@@ -6,6 +6,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
6
6
  import Viewport from '../../Viewport';
7
7
  import RenderingStyle from '../RenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
9
+ export declare const renderedStylesheetId = "js-draw-style-sheet";
9
10
  export default class SVGRenderer extends AbstractRenderer {
10
11
  private elem;
11
12
  private sanitize;
@@ -14,6 +15,7 @@ export default class SVGRenderer extends AbstractRenderer {
14
15
  private objectElems;
15
16
  private overwrittenAttrs;
16
17
  constructor(elem: SVGSVGElement, viewport: Viewport, sanitize?: boolean);
18
+ private addStyleSheet;
17
19
  setRootSVGAttribute(name: string, value: string | null): void;
18
20
  displaySize(): Vec2;
19
21
  clear(): void;
@@ -4,6 +4,7 @@ import { toRoundedString } from '../../math/rounding';
4
4
  import { Vec2 } from '../../math/Vec2';
5
5
  import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader';
6
6
  import AbstractRenderer from './AbstractRenderer';
7
+ export const renderedStylesheetId = 'js-draw-style-sheet';
7
8
  const svgNameSpace = 'http://www.w3.org/2000/svg';
8
9
  export default class SVGRenderer extends AbstractRenderer {
9
10
  // Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
@@ -18,6 +19,21 @@ export default class SVGRenderer extends AbstractRenderer {
18
19
  this.textContainer = null;
19
20
  this.textContainerTransform = null;
20
21
  this.clear();
22
+ this.addStyleSheet();
23
+ }
24
+ addStyleSheet() {
25
+ if (!this.elem.querySelector(`#${renderedStylesheetId}`)) {
26
+ // Default to rounded strokes.
27
+ const styleSheet = document.createElementNS('http://www.w3.org/2000/svg', 'style');
28
+ styleSheet.innerHTML = `
29
+ path {
30
+ stroke-linecap: round;
31
+ stroke-linejoin: round;
32
+ }
33
+ `.replace(/\s+/g, '');
34
+ styleSheet.setAttribute('id', renderedStylesheetId);
35
+ this.elem.appendChild(styleSheet);
36
+ }
21
37
  }
22
38
  // Sets an attribute on the root SVG element.
23
39
  setRootSVGAttribute(name, value) {
@@ -214,6 +230,10 @@ export default class SVGRenderer extends AbstractRenderer {
214
230
  if (this.sanitize) {
215
231
  return;
216
232
  }
233
+ // Don't add multiple copies of the default stylesheet.
234
+ if (elem.tagName.toLowerCase() === 'style' && elem.getAttribute('id') === renderedStylesheetId) {
235
+ return;
236
+ }
217
237
  this.elem.appendChild(elem.cloneNode(true));
218
238
  }
219
239
  isTooSmallToRender(_rect) {
@@ -6,6 +6,7 @@ export interface ToolbarLocalization {
6
6
  linePen: string;
7
7
  arrowPen: string;
8
8
  freehandPen: string;
9
+ pressureSensitiveFreehandPen: string;
9
10
  selectObjectType: string;
10
11
  colorLabel: string;
11
12
  pen: string;
@@ -19,6 +19,7 @@ export const defaultToolbarLocalization = {
19
19
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
20
20
  touchPanning: 'Touchscreen panning',
21
21
  freehandPen: 'Freehand',
22
+ pressureSensitiveFreehandPen: 'Freehand (pressure sensitive)',
22
23
  arrowPen: 'Arrow',
23
24
  linePen: 'Line',
24
25
  outlinedRectanglePen: 'Outlined rectangle',
@@ -1,5 +1,6 @@
1
1
  import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';
2
2
  import { makeFreehandLineBuilder } from '../../components/builders/FreehandLineBuilder';
3
+ import { makePressureSensitiveFreehandLineBuilder } from '../../components/builders/PressureSensitiveFreehandLineBuilder';
3
4
  import { makeLineBuilder } from '../../components/builders/LineBuilder';
4
5
  import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../components/builders/RectangleBuilder';
5
6
  import { EditorEventType } from '../../types';
@@ -13,6 +14,10 @@ export default class PenToolWidget extends BaseToolWidget {
13
14
  this.updateInputs = () => { };
14
15
  // Default pen types
15
16
  this.penTypes = [
17
+ {
18
+ name: localization.pressureSensitiveFreehandPen,
19
+ factory: makePressureSensitiveFreehandLineBuilder,
20
+ },
16
21
  {
17
22
  name: localization.freehandPen,
18
23
  factory: makeFreehandLineBuilder,
@@ -50,7 +55,7 @@ export default class PenToolWidget extends BaseToolWidget {
50
55
  }
51
56
  createIcon() {
52
57
  const strokeFactory = this.tool.getStrokeFactory();
53
- if (strokeFactory === makeFreehandLineBuilder) {
58
+ if (strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
54
59
  // Use a square-root scale to prevent the pen's tip from overflowing.
55
60
  const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
56
61
  const color = this.tool.getColor();
@@ -11,10 +11,10 @@ export interface PenStyle {
11
11
  export default class Pen extends BaseTool {
12
12
  private editor;
13
13
  private style;
14
+ private builderFactory;
14
15
  protected builder: ComponentBuilder | null;
15
- protected builderFactory: ComponentBuilderFactory;
16
16
  private lastPoint;
17
- constructor(editor: Editor, description: string, style: PenStyle);
17
+ constructor(editor: Editor, description: string, style: PenStyle, builderFactory?: ComponentBuilderFactory);
18
18
  private getPressureMultiplier;
19
19
  protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
20
20
  protected previewStroke(): void;
@@ -4,12 +4,12 @@ import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuil
4
4
  import { EditorEventType } from '../types';
5
5
  import BaseTool from './BaseTool';
6
6
  export default class Pen extends BaseTool {
7
- constructor(editor, description, style) {
7
+ constructor(editor, description, style, builderFactory = makeFreehandLineBuilder) {
8
8
  super(editor.notifier, description);
9
9
  this.editor = editor;
10
10
  this.style = style;
11
+ this.builderFactory = builderFactory;
11
12
  this.builder = null;
12
- this.builderFactory = makeFreehandLineBuilder;
13
13
  this.lastPoint = null;
14
14
  }
15
15
  getPressureMultiplier() {
@@ -18,6 +18,7 @@ export default class Selection {
18
18
  private selectedElems;
19
19
  private container;
20
20
  private backgroundElem;
21
+ private hasParent;
21
22
  constructor(startPoint: Point2, editor: Editor);
22
23
  getTransform(): Mat33;
23
24
  get preTransformRegion(): Rect2;
@@ -30,6 +30,7 @@ export default class Selection {
30
30
  this.transform = Mat33.identity;
31
31
  this.transformCommands = [];
32
32
  this.selectedElems = [];
33
+ this.hasParent = true;
33
34
  this.targetHandle = null;
34
35
  this.backgroundDragging = false;
35
36
  this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
@@ -96,7 +97,7 @@ export default class Selection {
96
97
  // Applies, previews, but doesn't finalize the given transformation.
97
98
  setTransform(transform, preview = true) {
98
99
  this.transform = transform;
99
- if (preview) {
100
+ if (preview && this.hasParent) {
100
101
  this.previewTransformCmds();
101
102
  this.scrollTo();
102
103
  }
@@ -190,6 +191,10 @@ export default class Selection {
190
191
  }
191
192
  // @internal
192
193
  updateUI() {
194
+ // Don't update old selections.
195
+ if (!this.hasParent) {
196
+ return;
197
+ }
193
198
  // marginLeft, marginTop: Display relative to the top left of the selection overlay.
194
199
  // left, top don't work for this.
195
200
  this.backgroundElem.style.marginLeft = `${this.screenRegion.topLeft.x}px`;
@@ -267,6 +272,7 @@ export default class Selection {
267
272
  this.container.remove();
268
273
  }
269
274
  elem.appendChild(this.container);
275
+ this.hasParent = true;
270
276
  }
271
277
  setToPoint(point) {
272
278
  this.originalRegion = this.originalRegion.grownToPoint(point);
@@ -277,6 +283,7 @@ export default class Selection {
277
283
  this.container.remove();
278
284
  }
279
285
  this.originalRegion = Rect2.empty;
286
+ this.hasParent = false;
280
287
  }
281
288
  setSelectedObjects(objects, bbox) {
282
289
  this.originalRegion = bbox;
@@ -11,6 +11,7 @@ import PipetteTool from './PipetteTool';
11
11
  import ToolSwitcherShortcut from './ToolSwitcherShortcut';
12
12
  import PasteHandler from './PasteHandler';
13
13
  import ToolbarShortcutHandler from './ToolbarShortcutHandler';
14
+ import { makePressureSensitiveFreehandLineBuilder } from '../components/builders/PressureSensitiveFreehandLineBuilder';
14
15
  export default class ToolController {
15
16
  /** @internal */
16
17
  constructor(editor, localization) {
@@ -25,7 +26,7 @@ export default class ToolController {
25
26
  primaryPenTool,
26
27
  new Pen(editor, localization.penTool(2), { color: Color4.clay, thickness: 4 }),
27
28
  // Highlighter-like pen with width=64
28
- new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
29
+ new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }, makePressureSensitiveFreehandLineBuilder),
29
30
  new Eraser(editor, localization.eraserTool),
30
31
  new SelectionTool(editor, localization.selectionTool),
31
32
  new TextTool(editor, localization.textTool, localization),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
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/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
@@ -61,7 +61,7 @@ export default class Stroke extends AbstractComponent {
61
61
  }
62
62
 
63
63
  const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 2 || bbox.size.y > visibleRect.size.y * 2;
64
- if (muchBiggerThanVisible && !part.path.closedRoughlyIntersects(visibleRect)) {
64
+ if (muchBiggerThanVisible && !part.path.roughlyIntersects(visibleRect, part.style.stroke?.width)) {
65
65
  continue;
66
66
  }
67
67
  }
@@ -87,7 +87,20 @@ export default class Stroke extends AbstractComponent {
87
87
  // Update each part
88
88
  this.parts = this.parts.map((part) => {
89
89
  const newPath = part.path.transformedBy(affineTransfm);
90
- const newBBox = this.bboxForPart(newPath.bbox, part.style);
90
+ const newStyle = {
91
+ ...part.style,
92
+ stroke: part.style.stroke ? {
93
+ ...part.style.stroke,
94
+ } : undefined,
95
+ };
96
+
97
+ // Approximate the scale factor.
98
+ if (newStyle.stroke) {
99
+ const scaleFactor = affineTransfm.getScaleFactor();
100
+ newStyle.stroke.width *= scaleFactor;
101
+ }
102
+
103
+ const newBBox = this.bboxForPart(newPath.bbox, newStyle);
91
104
 
92
105
  if (isFirstPart) {
93
106
  this.contentBBox = newBBox;
@@ -100,7 +113,7 @@ export default class Stroke extends AbstractComponent {
100
113
  path: newPath,
101
114
  startPoint: newPath.startPoint,
102
115
  commands: newPath.parts,
103
- style: part.style,
116
+ style: newStyle,
104
117
  };
105
118
  });
106
119
  }