js-draw 0.0.2 → 0.0.5
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.
- package/CHANGELOG.md +13 -0
- package/README.md +100 -3
- package/build_tools/BundledFile.ts +167 -0
- package/build_tools/bundle.ts +11 -0
- package/dist/build_tools/BundledFile.d.ts +13 -0
- package/dist/build_tools/BundledFile.js +157 -0
- package/dist/build_tools/bundle.d.ts +1 -0
- package/dist/build_tools/bundle.js +5 -0
- package/dist/bundle.js +1 -0
- package/dist/src/Display.js +4 -1
- package/dist/src/Editor.d.ts +9 -2
- package/dist/src/Editor.js +29 -7
- package/dist/src/EditorImage.d.ts +2 -0
- package/dist/src/EditorImage.js +29 -5
- package/dist/src/Pointer.js +1 -1
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/bundle/bundled.d.ts +4 -0
- package/dist/src/bundle/bundled.js +5 -0
- package/dist/src/components/AbstractComponent.d.ts +1 -1
- package/dist/src/components/Stroke.d.ts +1 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/UnknownSVGObject.d.ts +1 -1
- package/dist/src/components/builders/ArrowBuilder.d.ts +17 -0
- package/dist/src/components/builders/ArrowBuilder.js +83 -0
- package/dist/src/{StrokeBuilder.d.ts → components/builders/FreehandLineBuilder.d.ts} +9 -13
- package/dist/src/{StrokeBuilder.js → components/builders/FreehandLineBuilder.js} +27 -9
- package/dist/src/components/builders/LineBuilder.d.ts +16 -0
- package/dist/src/components/builders/LineBuilder.js +57 -0
- package/dist/src/components/builders/RectangleBuilder.d.ts +18 -0
- package/dist/src/components/builders/RectangleBuilder.js +41 -0
- package/dist/src/components/builders/types.d.ts +12 -0
- package/dist/src/components/builders/types.js +1 -0
- package/dist/src/geometry/Path.d.ts +1 -0
- package/dist/src/geometry/Path.js +32 -0
- package/dist/src/geometry/Vec3.d.ts +2 -0
- package/dist/src/geometry/Vec3.js +13 -0
- package/dist/src/localization.d.ts +1 -1
- package/dist/src/localization.js +2 -2
- package/dist/src/rendering/AbstractRenderer.js +3 -25
- package/dist/src/toolbar/HTMLToolbar.d.ts +5 -3
- package/dist/src/toolbar/HTMLToolbar.js +139 -30
- package/dist/src/toolbar/localization.d.ts +20 -0
- package/dist/src/toolbar/localization.js +19 -0
- package/dist/src/toolbar/types.d.ts +0 -13
- package/dist/src/tools/Pen.d.ts +13 -3
- package/dist/src/tools/Pen.js +37 -28
- package/dist/src/tools/SelectionTool.js +2 -1
- package/dist/src/tools/ToolController.js +3 -3
- package/dist/src/types.d.ts +14 -2
- package/dist/src/types.js +1 -0
- package/dist-test/test-dist-bundle.html +35 -0
- package/package.json +15 -5
- package/src/Display.ts +3 -1
- package/src/Editor.css +0 -1
- package/src/Editor.ts +51 -12
- package/src/EditorImage.test.ts +5 -3
- package/src/EditorImage.ts +31 -3
- package/src/Pointer.ts +1 -1
- package/src/Viewport.ts +1 -1
- package/src/bundle/bundled.ts +7 -0
- package/src/components/AbstractComponent.ts +1 -1
- package/src/components/Stroke.ts +2 -2
- package/src/components/UnknownSVGObject.ts +1 -1
- package/src/components/builders/ArrowBuilder.ts +104 -0
- package/src/{StrokeBuilder.ts → components/builders/FreehandLineBuilder.ts} +36 -18
- package/src/components/builders/LineBuilder.ts +75 -0
- package/src/components/builders/RectangleBuilder.ts +59 -0
- package/src/components/builders/types.ts +15 -0
- package/src/geometry/Path.ts +43 -0
- package/src/geometry/Vec2.test.ts +1 -0
- package/src/geometry/Vec3.test.ts +14 -0
- package/src/geometry/Vec3.ts +16 -0
- package/src/localization.ts +2 -3
- package/src/rendering/AbstractRenderer.ts +3 -32
- package/src/{editorStyles.js → styles.js} +0 -0
- package/src/toolbar/HTMLToolbar.ts +167 -39
- package/src/toolbar/localization.ts +44 -0
- package/src/toolbar/toolbar.css +12 -0
- package/src/toolbar/types.ts +0 -16
- package/src/tools/Pen.ts +56 -34
- package/src/tools/SelectionTool.test.ts +1 -1
- package/src/tools/SelectionTool.ts +1 -1
- package/src/tools/ToolController.ts +3 -3
- package/src/types.ts +16 -1
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.5",
|
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",
|
@@ -10,7 +10,7 @@
|
|
10
10
|
"default": "./dist/src/Editor.js"
|
11
11
|
},
|
12
12
|
"./styles": {
|
13
|
-
"default": "./src/
|
13
|
+
"default": "./src/styles.js"
|
14
14
|
},
|
15
15
|
"./Editor": {
|
16
16
|
"types": "./dist/src/Editor.d.ts",
|
@@ -29,6 +29,9 @@
|
|
29
29
|
},
|
30
30
|
"./toolbar/toolbar.css": {
|
31
31
|
"default": "./src/toolbar/toolbar.css"
|
32
|
+
},
|
33
|
+
"./bundle": {
|
34
|
+
"default": "./dist/bundle.js"
|
32
35
|
}
|
33
36
|
},
|
34
37
|
"repository": {
|
@@ -40,7 +43,7 @@
|
|
40
43
|
"private": false,
|
41
44
|
"scripts": {
|
42
45
|
"test": "jest",
|
43
|
-
"build": "yarn tsc",
|
46
|
+
"build": "rm -rf ./dist; mkdir dist && yarn tsc && ts-node ./build_tools/bundle.ts",
|
44
47
|
"lint": "eslint .",
|
45
48
|
"linter-precommit": "eslint --fix --ext .js --ext .ts",
|
46
49
|
"lint-staged": "lint-staged",
|
@@ -59,6 +62,7 @@
|
|
59
62
|
"@types/node": "^18.7.9",
|
60
63
|
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
61
64
|
"@typescript-eslint/parser": "^5.33.1",
|
65
|
+
"css-loader": "^6.7.1",
|
62
66
|
"eslint": "^8.22.0",
|
63
67
|
"husky": "^8.0.1",
|
64
68
|
"jest": "^28.1.3",
|
@@ -66,20 +70,26 @@
|
|
66
70
|
"jsdom": "^20.0.0",
|
67
71
|
"lint-staged": "^13.0.3",
|
68
72
|
"pinst": "^3.0.0",
|
73
|
+
"style-loader": "^3.3.1",
|
74
|
+
"terser-webpack-plugin": "^5.3.5",
|
69
75
|
"ts-jest": "^28.0.8",
|
70
|
-
"
|
76
|
+
"ts-loader": "^9.3.1",
|
77
|
+
"ts-node": "^10.9.1",
|
78
|
+
"typescript": "^4.7.4",
|
79
|
+
"webpack": "^5.74.0"
|
71
80
|
},
|
72
81
|
"bugs": {
|
73
82
|
"url": "https://github.com/personalizedrefrigerator/js-draw/issues"
|
74
83
|
},
|
75
84
|
"homepage": "https://github.com/personalizedrefrigerator/js-draw#readme",
|
76
85
|
"directories": {
|
77
|
-
"doc": "
|
86
|
+
"doc": "docs"
|
78
87
|
},
|
79
88
|
"keywords": [
|
80
89
|
"ink",
|
81
90
|
"drawing",
|
82
91
|
"pen",
|
92
|
+
"freehand",
|
83
93
|
"svg"
|
84
94
|
]
|
85
95
|
}
|
package/src/Display.ts
CHANGED
@@ -22,9 +22,11 @@ export default class Display {
|
|
22
22
|
) {
|
23
23
|
if (mode === RenderingMode.CanvasRenderer) {
|
24
24
|
this.initializeCanvasRendering();
|
25
|
-
} else {
|
25
|
+
} else if (mode === RenderingMode.DummyRenderer) {
|
26
26
|
this.dryInkRenderer = new DummyRenderer(editor.viewport);
|
27
27
|
this.wetInkRenderer = new DummyRenderer(editor.viewport);
|
28
|
+
} else {
|
29
|
+
throw new Error(`Unknown rendering mode, ${mode}!`);
|
28
30
|
}
|
29
31
|
|
30
32
|
|
package/src/Editor.css
CHANGED
package/src/Editor.ts
CHANGED
@@ -19,6 +19,19 @@ import Mat33 from './geometry/Mat33';
|
|
19
19
|
import Rect2 from './geometry/Rect2';
|
20
20
|
import { defaultEditorLocalization, EditorLocalization } from './localization';
|
21
21
|
|
22
|
+
export interface EditorSettings {
|
23
|
+
// Defaults to RenderingMode.CanvasRenderer
|
24
|
+
renderingMode: RenderingMode,
|
25
|
+
|
26
|
+
// Uses a default English localization if a translation is not given.
|
27
|
+
localization: Partial<EditorLocalization>,
|
28
|
+
|
29
|
+
// True if touchpad/mousewheel scrolling should scroll the editor instead of the document.
|
30
|
+
// This does not include pinch-zoom events.
|
31
|
+
// Defaults to true.
|
32
|
+
wheelEventsEnabled: boolean;
|
33
|
+
}
|
34
|
+
|
22
35
|
export class Editor {
|
23
36
|
// Wrapper around the viewport and toolbar
|
24
37
|
private container: HTMLElement;
|
@@ -39,23 +52,29 @@ export class Editor {
|
|
39
52
|
private loadingWarning: HTMLElement;
|
40
53
|
private accessibilityAnnounceArea: HTMLElement;
|
41
54
|
|
55
|
+
private settings: EditorSettings;
|
56
|
+
|
42
57
|
public constructor(
|
43
58
|
parent: HTMLElement,
|
44
|
-
|
45
|
-
|
46
|
-
// Uses a default English localization if a translation is not given.
|
47
|
-
localization?: Partial<EditorLocalization>,
|
59
|
+
settings: Partial<EditorSettings> = {},
|
48
60
|
) {
|
61
|
+
this.localization = {
|
62
|
+
...this.localization,
|
63
|
+
...settings.localization,
|
64
|
+
};
|
65
|
+
|
66
|
+
// Fill default settings.
|
67
|
+
this.settings = {
|
68
|
+
wheelEventsEnabled: settings.wheelEventsEnabled ?? true,
|
69
|
+
renderingMode: settings.renderingMode ?? RenderingMode.CanvasRenderer,
|
70
|
+
localization: this.localization,
|
71
|
+
};
|
72
|
+
|
49
73
|
this.container = document.createElement('div');
|
50
74
|
this.renderingRegion = document.createElement('div');
|
51
75
|
this.container.appendChild(this.renderingRegion);
|
52
76
|
this.container.className = 'imageEditorContainer';
|
53
77
|
|
54
|
-
this.localization = {
|
55
|
-
...this.localization,
|
56
|
-
...localization,
|
57
|
-
};
|
58
|
-
|
59
78
|
this.loadingWarning = document.createElement('div');
|
60
79
|
this.loadingWarning.classList.add('loadingMessage');
|
61
80
|
this.loadingWarning.ariaLive = 'polite';
|
@@ -74,7 +93,7 @@ export class Editor {
|
|
74
93
|
this.notifier = new EventDispatcher();
|
75
94
|
this.importExportViewport = new Viewport(this.notifier);
|
76
95
|
this.viewport = new Viewport(this.notifier);
|
77
|
-
this.display = new Display(this, renderingMode, this.renderingRegion);
|
96
|
+
this.display = new Display(this, this.settings.renderingMode, this.renderingRegion);
|
78
97
|
this.image = new EditorImage();
|
79
98
|
this.history = new UndoRedoHistory(this, this.announceRedoCallback, this.announceUndoCallback);
|
80
99
|
this.toolController = new ToolController(this, this.localization);
|
@@ -93,6 +112,13 @@ export class Editor {
|
|
93
112
|
this.hideLoadingWarning();
|
94
113
|
}
|
95
114
|
|
115
|
+
// Returns a reference to this' container.
|
116
|
+
// Example usage:
|
117
|
+
// editor.getRootElement().style.height = '500px';
|
118
|
+
public getRootElement(): HTMLElement {
|
119
|
+
return this.container;
|
120
|
+
}
|
121
|
+
|
96
122
|
// [fractionLoaded] should be a number from 0 to 1, where 1 represents completely loaded.
|
97
123
|
public showLoadingWarning(fractionLoaded: number) {
|
98
124
|
const loadingPercent = Math.round(fractionLoaded * 100);
|
@@ -110,8 +136,15 @@ export class Editor {
|
|
110
136
|
this.accessibilityAnnounceArea.innerText = message;
|
111
137
|
}
|
112
138
|
|
113
|
-
public addToolbar(): HTMLToolbar {
|
114
|
-
|
139
|
+
public addToolbar(defaultLayout: boolean = true): HTMLToolbar {
|
140
|
+
const toolbar = new HTMLToolbar(this, this.container, this.localization);
|
141
|
+
|
142
|
+
if (defaultLayout) {
|
143
|
+
toolbar.addDefaultToolWidgets();
|
144
|
+
toolbar.addDefaultActionButtons();
|
145
|
+
}
|
146
|
+
|
147
|
+
return toolbar;
|
115
148
|
}
|
116
149
|
|
117
150
|
private registerListeners() {
|
@@ -203,6 +236,12 @@ export class Editor {
|
|
203
236
|
this.container.addEventListener('wheel', evt => {
|
204
237
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
205
238
|
|
239
|
+
// Process wheel events if the ctrl key is down -- we do want to handle
|
240
|
+
// pinch-zooming.
|
241
|
+
if (!this.settings.wheelEventsEnabled && !evt.ctrlKey) {
|
242
|
+
return;
|
243
|
+
}
|
244
|
+
|
206
245
|
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
207
246
|
delta = delta.times(15);
|
208
247
|
} else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
package/src/EditorImage.test.ts
CHANGED
@@ -10,6 +10,8 @@ import { RenderingMode } from './Display';
|
|
10
10
|
import DummyRenderer from './rendering/DummyRenderer';
|
11
11
|
import { RenderingStyle } from './rendering/AbstractRenderer';
|
12
12
|
|
13
|
+
const createEditor = () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
|
14
|
+
|
13
15
|
describe('EditorImage', () => {
|
14
16
|
const testStroke = new Stroke([
|
15
17
|
{
|
@@ -29,7 +31,7 @@ describe('EditorImage', () => {
|
|
29
31
|
const addTestStrokeCommand = new EditorImage.AddElementCommand(testStroke);
|
30
32
|
|
31
33
|
it('elements added to the image should be findable', () => {
|
32
|
-
const editor =
|
34
|
+
const editor = createEditor();
|
33
35
|
const image = editor.image;
|
34
36
|
|
35
37
|
// We haven't activated the command, so testStroke's parent should be null.
|
@@ -39,7 +41,7 @@ describe('EditorImage', () => {
|
|
39
41
|
});
|
40
42
|
|
41
43
|
it('should render an element added to the image', () => {
|
42
|
-
const editor =
|
44
|
+
const editor = createEditor();
|
43
45
|
const renderer = editor.display.getDryInkRenderer();
|
44
46
|
if (!(renderer instanceof DummyRenderer)) {
|
45
47
|
throw new Error('Wrong display type!');
|
@@ -56,7 +58,7 @@ describe('EditorImage', () => {
|
|
56
58
|
});
|
57
59
|
|
58
60
|
it('should have a 1-deep tree if two non-overlapping strokes are added', () => {
|
59
|
-
const editor =
|
61
|
+
const editor = createEditor();
|
60
62
|
const image = editor.image;
|
61
63
|
|
62
64
|
const leftmostStroke = new Stroke([
|
package/src/EditorImage.ts
CHANGED
@@ -107,6 +107,8 @@ export class ImageNode {
|
|
107
107
|
private bbox: Rect2;
|
108
108
|
private children: ImageNode[];
|
109
109
|
private targetChildCount: number = 30;
|
110
|
+
private minZIndex: number|null;
|
111
|
+
private maxZIndex: number|null;
|
110
112
|
|
111
113
|
public constructor(
|
112
114
|
private parent: ImageNode|null = null
|
@@ -114,6 +116,9 @@ export class ImageNode {
|
|
114
116
|
this.children = [];
|
115
117
|
this.bbox = Rect2.empty;
|
116
118
|
this.content = null;
|
119
|
+
|
120
|
+
this.minZIndex = null;
|
121
|
+
this.maxZIndex = null;
|
117
122
|
}
|
118
123
|
|
119
124
|
public getContent(): AbstractComponent|null {
|
@@ -130,7 +135,7 @@ export class ImageNode {
|
|
130
135
|
});
|
131
136
|
}
|
132
137
|
|
133
|
-
//
|
138
|
+
// Returns a list of `ImageNode`s with content (and thus no children).
|
134
139
|
public getLeavesInRegion(region: Rect2, minFractionOfRegion: number = 0): ImageNode[] {
|
135
140
|
const result: ImageNode[] = [];
|
136
141
|
|
@@ -228,16 +233,36 @@ export class ImageNode {
|
|
228
233
|
}
|
229
234
|
|
230
235
|
// Recomputes this' bounding box. If [bubbleUp], also recompute
|
231
|
-
// this' ancestors bounding boxes
|
236
|
+
// this' ancestors bounding boxes. This also re-computes this' bounding box
|
237
|
+
// in the z-direction (z-indicies).
|
232
238
|
public recomputeBBox(bubbleUp: boolean) {
|
233
239
|
const oldBBox = this.bbox;
|
234
240
|
if (this.content !== null) {
|
235
241
|
this.bbox = this.content.getBBox();
|
242
|
+
this.minZIndex = this.content.zIndex;
|
243
|
+
this.maxZIndex = this.content.zIndex;
|
236
244
|
} else {
|
237
245
|
this.bbox = Rect2.empty;
|
246
|
+
this.minZIndex = null;
|
247
|
+
this.maxZIndex = null;
|
248
|
+
let isFirst = true;
|
238
249
|
|
239
250
|
for (const child of this.children) {
|
240
|
-
|
251
|
+
if (isFirst) {
|
252
|
+
this.bbox = child.getBBox();
|
253
|
+
isFirst = false;
|
254
|
+
} else {
|
255
|
+
this.bbox = this.bbox.union(child.getBBox());
|
256
|
+
}
|
257
|
+
|
258
|
+
this.minZIndex ??= child.minZIndex;
|
259
|
+
this.maxZIndex ??= child.maxZIndex;
|
260
|
+
if (child.minZIndex !== null && this.minZIndex !== null) {
|
261
|
+
this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
|
262
|
+
}
|
263
|
+
if (child.maxZIndex !== null && this.maxZIndex !== null) {
|
264
|
+
this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
|
265
|
+
}
|
241
266
|
}
|
242
267
|
}
|
243
268
|
|
@@ -270,6 +295,9 @@ export class ImageNode {
|
|
270
295
|
|
271
296
|
// Remove this node and all of its children
|
272
297
|
public remove() {
|
298
|
+
this.minZIndex = null;
|
299
|
+
this.maxZIndex = null;
|
300
|
+
|
273
301
|
if (!this.parent) {
|
274
302
|
this.content = null;
|
275
303
|
this.children = [];
|
package/src/Pointer.ts
CHANGED
package/src/Viewport.ts
CHANGED
@@ -5,7 +5,7 @@ import Mat33 from './geometry/Mat33';
|
|
5
5
|
import Rect2 from './geometry/Rect2';
|
6
6
|
import { Point2, Vec2 } from './geometry/Vec2';
|
7
7
|
import Vec3 from './geometry/Vec3';
|
8
|
-
import { StrokeDataPoint } from './
|
8
|
+
import { StrokeDataPoint } from './types';
|
9
9
|
import { EditorEventType, EditorNotifier } from './types';
|
10
10
|
|
11
11
|
// Returns the base type of some type of point/number
|
@@ -23,7 +23,7 @@ export default abstract class AbstractComponent {
|
|
23
23
|
public getBBox(): Rect2 {
|
24
24
|
return this.contentBBox;
|
25
25
|
}
|
26
|
-
public abstract render(canvas: AbstractRenderer, visibleRect
|
26
|
+
public abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
27
27
|
public abstract intersects(lineSegment: LineSegment2): boolean;
|
28
28
|
|
29
29
|
// Private helper for transformBy: Apply the given transformation to all points of this.
|
package/src/components/Stroke.ts
CHANGED
@@ -50,11 +50,11 @@ export default class Stroke extends AbstractComponent {
|
|
50
50
|
return false;
|
51
51
|
}
|
52
52
|
|
53
|
-
public render(canvas: AbstractRenderer, visibleRect
|
53
|
+
public render(canvas: AbstractRenderer, visibleRect?: Rect2): void {
|
54
54
|
canvas.startObject(this.getBBox());
|
55
55
|
for (const part of this.parts) {
|
56
56
|
const bbox = part.bbox;
|
57
|
-
if (bbox.intersects(visibleRect)) {
|
57
|
+
if (!visibleRect || bbox.intersects(visibleRect)) {
|
58
58
|
canvas.drawPath(part);
|
59
59
|
}
|
60
60
|
}
|
@@ -14,7 +14,7 @@ export default class UnknownSVGObject extends AbstractComponent {
|
|
14
14
|
this.contentBBox = Rect2.of(svgObject.getBoundingClientRect());
|
15
15
|
}
|
16
16
|
|
17
|
-
public render(canvas: AbstractRenderer, _visibleRect
|
17
|
+
public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
|
18
18
|
if (!(canvas instanceof SVGRenderer)) {
|
19
19
|
// Don't draw unrenderable objects if we can't
|
20
20
|
return;
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import { PathCommandType } from '../../geometry/Path';
|
2
|
+
import Rect2 from '../../geometry/Rect2';
|
3
|
+
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
4
|
+
import { StrokeDataPoint } from '../../types';
|
5
|
+
import Viewport from '../../Viewport';
|
6
|
+
import AbstractComponent from '../AbstractComponent';
|
7
|
+
import Stroke from '../Stroke';
|
8
|
+
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
9
|
+
|
10
|
+
export const makeArrowBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
|
11
|
+
return new ArrowBuilder(initialPoint);
|
12
|
+
};
|
13
|
+
|
14
|
+
export default class ArrowBuilder implements ComponentBuilder {
|
15
|
+
private endPoint: StrokeDataPoint;
|
16
|
+
|
17
|
+
public constructor(private readonly startPoint: StrokeDataPoint) {
|
18
|
+
this.endPoint = startPoint;
|
19
|
+
}
|
20
|
+
|
21
|
+
private getLineWidth(): number {
|
22
|
+
return Math.max(this.endPoint.width, this.startPoint.width);
|
23
|
+
}
|
24
|
+
|
25
|
+
public getBBox(): Rect2 {
|
26
|
+
const preview = this.buildPreview();
|
27
|
+
return preview.getBBox();
|
28
|
+
}
|
29
|
+
|
30
|
+
private buildPreview(): Stroke {
|
31
|
+
const startPoint = this.startPoint.pos;
|
32
|
+
const endPoint = this.endPoint.pos;
|
33
|
+
const toEnd = endPoint.minus(startPoint).normalized();
|
34
|
+
const arrowLength = endPoint.minus(startPoint).length();
|
35
|
+
|
36
|
+
// Ensure that the arrow tip is smaller than the arrow.
|
37
|
+
const arrowTipSize = Math.min(this.getLineWidth(), arrowLength / 2);
|
38
|
+
const startSize = this.startPoint.width / 2;
|
39
|
+
const endSize = this.endPoint.width / 2;
|
40
|
+
|
41
|
+
const arrowTipBase = endPoint.minus(toEnd.times(arrowTipSize));
|
42
|
+
|
43
|
+
// Scaled normal vectors.
|
44
|
+
const lineNormal = toEnd.orthog();
|
45
|
+
const scaledStartNormal = lineNormal.times(startSize);
|
46
|
+
const scaledBaseNormal = lineNormal.times(endSize);
|
47
|
+
|
48
|
+
const preview = new Stroke([
|
49
|
+
{
|
50
|
+
startPoint: arrowTipBase.minus(scaledBaseNormal),
|
51
|
+
commands: [
|
52
|
+
// Stem
|
53
|
+
{
|
54
|
+
kind: PathCommandType.LineTo,
|
55
|
+
point: startPoint.minus(scaledStartNormal),
|
56
|
+
},
|
57
|
+
{
|
58
|
+
kind: PathCommandType.LineTo,
|
59
|
+
point: startPoint.plus(scaledStartNormal),
|
60
|
+
},
|
61
|
+
{
|
62
|
+
kind: PathCommandType.LineTo,
|
63
|
+
point: arrowTipBase.plus(scaledBaseNormal),
|
64
|
+
},
|
65
|
+
|
66
|
+
// Head
|
67
|
+
{
|
68
|
+
kind: PathCommandType.LineTo,
|
69
|
+
point: arrowTipBase.plus(lineNormal.times(arrowTipSize).plus(scaledBaseNormal))
|
70
|
+
},
|
71
|
+
{
|
72
|
+
kind: PathCommandType.LineTo,
|
73
|
+
point: endPoint.plus(toEnd.times(endSize)),
|
74
|
+
},
|
75
|
+
{
|
76
|
+
kind: PathCommandType.LineTo,
|
77
|
+
point: arrowTipBase.plus(lineNormal.times(-arrowTipSize).minus(scaledBaseNormal)),
|
78
|
+
},
|
79
|
+
{
|
80
|
+
kind: PathCommandType.LineTo,
|
81
|
+
point: arrowTipBase.minus(scaledBaseNormal),
|
82
|
+
},
|
83
|
+
],
|
84
|
+
style: {
|
85
|
+
fill: this.startPoint.color,
|
86
|
+
}
|
87
|
+
}
|
88
|
+
]);
|
89
|
+
|
90
|
+
return preview;
|
91
|
+
}
|
92
|
+
|
93
|
+
public build(): AbstractComponent {
|
94
|
+
return this.buildPreview();
|
95
|
+
}
|
96
|
+
|
97
|
+
public preview(renderer: AbstractRenderer): void {
|
98
|
+
this.buildPreview().render(renderer);
|
99
|
+
}
|
100
|
+
|
101
|
+
public addPoint(point: StrokeDataPoint): void {
|
102
|
+
this.endPoint = point;
|
103
|
+
}
|
104
|
+
}
|
@@ -1,22 +1,28 @@
|
|
1
|
-
import Color4 from './Color4';
|
2
1
|
import { Bezier } from 'bezier-js';
|
3
|
-
import { RenderingStyle, RenderablePathSpec } from '
|
4
|
-
import { Point2, Vec2 } from '
|
5
|
-
import Rect2 from '
|
6
|
-
import { PathCommand, PathCommandType } from '
|
7
|
-
import LineSegment2 from '
|
8
|
-
import Stroke from '
|
9
|
-
import Viewport from '
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
2
|
+
import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/AbstractRenderer';
|
3
|
+
import { Point2, Vec2 } from '../../geometry/Vec2';
|
4
|
+
import Rect2 from '../../geometry/Rect2';
|
5
|
+
import { PathCommand, PathCommandType } from '../../geometry/Path';
|
6
|
+
import LineSegment2 from '../../geometry/LineSegment2';
|
7
|
+
import Stroke from '../Stroke';
|
8
|
+
import Viewport from '../../Viewport';
|
9
|
+
import { StrokeDataPoint } from '../../types';
|
10
|
+
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
11
|
+
|
12
|
+
export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
|
13
|
+
// Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
|
14
|
+
// less than ± 2 px from the curve.
|
15
|
+
const canvasTransform = viewport.screenToCanvasTransform;
|
16
|
+
const maxSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 7;
|
17
|
+
const minSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 2;
|
18
|
+
|
19
|
+
return new FreehandLineBuilder(
|
20
|
+
initialPoint, minSmoothingDist, maxSmoothingDist
|
21
|
+
);
|
22
|
+
};
|
17
23
|
|
18
24
|
// Handles stroke smoothing and creates Strokes from user/stylus input.
|
19
|
-
export default class
|
25
|
+
export default class FreehandLineBuilder implements ComponentBuilder {
|
20
26
|
private segments: RenderablePathSpec[];
|
21
27
|
private buffer: Point2[];
|
22
28
|
private lastPoint: StrokeDataPoint;
|
@@ -59,7 +65,7 @@ export default class StrokeBuilder {
|
|
59
65
|
}
|
60
66
|
|
61
67
|
// Get the segments that make up this' path. Can be called after calling build()
|
62
|
-
|
68
|
+
private getPreview(): RenderablePathSpec[] {
|
63
69
|
if (this.currentCurve && this.lastPoint) {
|
64
70
|
const currentPath = this.currentSegmentToPath();
|
65
71
|
return this.segments.concat(currentPath);
|
@@ -68,6 +74,12 @@ export default class StrokeBuilder {
|
|
68
74
|
return this.segments;
|
69
75
|
}
|
70
76
|
|
77
|
+
public preview(renderer: AbstractRenderer) {
|
78
|
+
for (const part of this.getPreview()) {
|
79
|
+
renderer.drawPath(part);
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
71
83
|
public build(): Stroke {
|
72
84
|
if (this.lastPoint) {
|
73
85
|
this.finalizeCurrentCurve();
|
@@ -231,7 +243,13 @@ export default class StrokeBuilder {
|
|
231
243
|
}
|
232
244
|
|
233
245
|
const threshold = Math.min(this.lastPoint.width, newPoint.width) / 4;
|
234
|
-
|
246
|
+
const shouldSnapToInitial = this.startPoint.pos.minus(newPoint.pos).magnitude() < threshold
|
247
|
+
&& this.segments.length === 0;
|
248
|
+
|
249
|
+
// Snap to the starting point if the stroke is contained within a small ball centered
|
250
|
+
// at the starting point.
|
251
|
+
// This allows us to create a circle/dot at the start of the stroke.
|
252
|
+
if (shouldSnapToInitial) {
|
235
253
|
return;
|
236
254
|
}
|
237
255
|
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import { PathCommandType } from '../../geometry/Path';
|
2
|
+
import Rect2 from '../../geometry/Rect2';
|
3
|
+
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
4
|
+
import { StrokeDataPoint } from '../../types';
|
5
|
+
import Viewport from '../../Viewport';
|
6
|
+
import AbstractComponent from '../AbstractComponent';
|
7
|
+
import Stroke from '../Stroke';
|
8
|
+
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
9
|
+
|
10
|
+
export const makeLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
|
11
|
+
return new LineBuilder(initialPoint);
|
12
|
+
};
|
13
|
+
|
14
|
+
export default class LineBuilder implements ComponentBuilder {
|
15
|
+
private endPoint: StrokeDataPoint;
|
16
|
+
|
17
|
+
public constructor(private readonly startPoint: StrokeDataPoint) {
|
18
|
+
this.endPoint = startPoint;
|
19
|
+
}
|
20
|
+
|
21
|
+
public getBBox(): Rect2 {
|
22
|
+
const preview = this.buildPreview();
|
23
|
+
return preview.getBBox();
|
24
|
+
}
|
25
|
+
|
26
|
+
private buildPreview(): Stroke {
|
27
|
+
const startPoint = this.startPoint.pos;
|
28
|
+
const endPoint = this.endPoint.pos;
|
29
|
+
const toEnd = endPoint.minus(startPoint).normalized();
|
30
|
+
|
31
|
+
const startSize = this.startPoint.width / 2;
|
32
|
+
const endSize = this.endPoint.width / 2;
|
33
|
+
|
34
|
+
const lineNormal = toEnd.orthog();
|
35
|
+
const scaledStartNormal = lineNormal.times(startSize);
|
36
|
+
const scaledEndNormal = lineNormal.times(endSize);
|
37
|
+
|
38
|
+
const preview = new Stroke([
|
39
|
+
{
|
40
|
+
startPoint: startPoint.minus(scaledStartNormal),
|
41
|
+
commands: [
|
42
|
+
{
|
43
|
+
kind: PathCommandType.LineTo,
|
44
|
+
point: startPoint.plus(scaledStartNormal),
|
45
|
+
},
|
46
|
+
{
|
47
|
+
kind: PathCommandType.LineTo,
|
48
|
+
point: endPoint.plus(scaledEndNormal),
|
49
|
+
},
|
50
|
+
{
|
51
|
+
kind: PathCommandType.LineTo,
|
52
|
+
point: endPoint.minus(scaledEndNormal),
|
53
|
+
},
|
54
|
+
],
|
55
|
+
style: {
|
56
|
+
fill: this.startPoint.color,
|
57
|
+
}
|
58
|
+
}
|
59
|
+
]);
|
60
|
+
|
61
|
+
return preview;
|
62
|
+
}
|
63
|
+
|
64
|
+
public build(): AbstractComponent {
|
65
|
+
return this.buildPreview();
|
66
|
+
}
|
67
|
+
|
68
|
+
public preview(renderer: AbstractRenderer): void {
|
69
|
+
this.buildPreview().render(renderer);
|
70
|
+
}
|
71
|
+
|
72
|
+
public addPoint(point: StrokeDataPoint): void {
|
73
|
+
this.endPoint = point;
|
74
|
+
}
|
75
|
+
}
|