js-draw 0.0.4 → 0.0.7
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 +14 -0
- package/README.md +108 -0
- 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 +8 -1
- package/dist/src/Editor.js +30 -5
- package/dist/src/EditorImage.d.ts +2 -2
- package/dist/src/SVGLoader.d.ts +2 -0
- package/dist/src/SVGLoader.js +15 -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/SVGGlobalAttributesObject.d.ts +15 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +29 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +15 -3
- package/dist/src/geometry/Path.js +11 -0
- package/dist/src/localization.d.ts +1 -1
- package/dist/src/localization.js +2 -2
- package/dist/src/rendering/SVGRenderer.d.ts +2 -0
- package/dist/src/rendering/SVGRenderer.js +25 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -2
- package/dist/src/toolbar/HTMLToolbar.js +2 -20
- 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 -19
- package/dist/src/tools/SelectionTool.js +1 -1
- package/dist-test/test-dist-bundle.html +35 -0
- package/package.json +12 -3
- package/src/Display.ts +3 -1
- package/src/Editor.css +0 -1
- package/src/Editor.ts +54 -11
- package/src/EditorImage.test.ts +5 -3
- package/src/SVGLoader.ts +18 -1
- package/src/bundle/bundled.ts +7 -0
- package/src/components/SVGGlobalAttributesObject.ts +39 -0
- package/src/components/builders/FreehandLineBuilder.ts +23 -4
- package/src/geometry/Path.fromString.test.ts +11 -24
- package/src/geometry/Path.ts +13 -0
- package/src/localization.ts +2 -3
- package/src/rendering/SVGRenderer.ts +27 -0
- package/src/toolbar/HTMLToolbar.ts +2 -24
- package/src/toolbar/localization.ts +44 -0
- package/src/toolbar/types.ts +0 -22
- package/src/tools/SelectionTool.test.ts +1 -1
- package/src/tools/SelectionTool.ts +1 -1
@@ -0,0 +1,29 @@
|
|
1
|
+
import Rect2 from '../geometry/Rect2';
|
2
|
+
import SVGRenderer from '../rendering/SVGRenderer';
|
3
|
+
import AbstractComponent from './AbstractComponent';
|
4
|
+
// Stores global SVG attributes (e.g. namespace identifiers.)
|
5
|
+
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
6
|
+
constructor(attrs) {
|
7
|
+
super();
|
8
|
+
this.attrs = attrs;
|
9
|
+
this.contentBBox = Rect2.empty;
|
10
|
+
}
|
11
|
+
render(canvas, _visibleRect) {
|
12
|
+
if (!(canvas instanceof SVGRenderer)) {
|
13
|
+
// Don't draw unrenderable objects if we can't
|
14
|
+
return;
|
15
|
+
}
|
16
|
+
console.log('Rendering to SVG.', this.attrs);
|
17
|
+
for (const [attr, value] of this.attrs) {
|
18
|
+
canvas.setRootSVGAttribute(attr, value);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
intersects(_lineSegment) {
|
22
|
+
return false;
|
23
|
+
}
|
24
|
+
applyTransformation(_affineTransfm) {
|
25
|
+
}
|
26
|
+
description(localization) {
|
27
|
+
return localization.svgObject;
|
28
|
+
}
|
29
|
+
}
|
@@ -148,9 +148,21 @@ export default class FreehandLineBuilder {
|
|
148
148
|
projectionT = 0.9;
|
149
149
|
}
|
150
150
|
}
|
151
|
-
const
|
152
|
-
|
153
|
-
|
151
|
+
const halfVecT = projectionT;
|
152
|
+
let halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
|
153
|
+
.normalized().times(this.curveStartWidth / 2 * halfVecT
|
154
|
+
+ this.curveEndWidth / 2 * (1 - halfVecT));
|
155
|
+
// Computes a boundary curve. [direction] should be either +1 or -1 (determines the side
|
156
|
+
// of the center curve to place the boundary).
|
157
|
+
const computeBoundaryCurve = (direction, halfVec) => {
|
158
|
+
return new Bezier(startPt.plus(startVec.times(direction)), controlPoint.plus(halfVec.times(direction)), endPt.plus(endVec.times(direction)));
|
159
|
+
};
|
160
|
+
const upperBoundary = computeBoundaryCurve(1, halfVec);
|
161
|
+
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
162
|
+
// If the boundaries have two intersections, increasing the half vector's length could fix this.
|
163
|
+
if (upperBoundary.intersects(lowerBoundary).length === 2) {
|
164
|
+
halfVec = halfVec.times(2);
|
165
|
+
}
|
154
166
|
const pathCommands = [
|
155
167
|
{
|
156
168
|
kind: PathCommandType.QuadraticBezierTo,
|
@@ -277,14 +277,24 @@ export default class Path {
|
|
277
277
|
pathString = pathString.split('\n').join(' ');
|
278
278
|
let lastPos = Vec2.zero;
|
279
279
|
let firstPos = null;
|
280
|
+
let isFirstCommand = true;
|
280
281
|
const commands = [];
|
281
282
|
const moveTo = (point) => {
|
283
|
+
// The first moveTo/lineTo is already handled by the [startPoint] parameter of the Path constructor.
|
284
|
+
if (isFirstCommand) {
|
285
|
+
isFirstCommand = false;
|
286
|
+
return;
|
287
|
+
}
|
282
288
|
commands.push({
|
283
289
|
kind: PathCommandType.MoveTo,
|
284
290
|
point,
|
285
291
|
});
|
286
292
|
};
|
287
293
|
const lineTo = (point) => {
|
294
|
+
if (isFirstCommand) {
|
295
|
+
isFirstCommand = false;
|
296
|
+
return;
|
297
|
+
}
|
288
298
|
commands.push({
|
289
299
|
kind: PathCommandType.LineTo,
|
290
300
|
point,
|
@@ -390,6 +400,7 @@ export default class Path {
|
|
390
400
|
if (args.length > 0) {
|
391
401
|
firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = args[0]);
|
392
402
|
}
|
403
|
+
isFirstCommand = false;
|
393
404
|
}
|
394
405
|
return new Path(firstPos !== null && firstPos !== void 0 ? firstPos : Vec2.zero, commands);
|
395
406
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { CommandLocalization } from './commands/localization';
|
2
2
|
import { ImageComponentLocalization } from './components/localization';
|
3
|
-
import { ToolbarLocalization } from './toolbar/
|
3
|
+
import { ToolbarLocalization } from './toolbar/localization';
|
4
4
|
import { ToolLocalization } from './tools/localization';
|
5
5
|
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
|
6
6
|
undoAnnouncement: (actionDescription: string) => string;
|
package/dist/src/localization.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { defaultCommandLocalization } from './commands/localization';
|
2
2
|
import { defaultComponentLocalization } from './components/localization';
|
3
|
-
import
|
3
|
+
import { defaultToolbarLocalization } from './toolbar/localization';
|
4
4
|
import { defaultToolLocalization } from './tools/localization';
|
5
|
-
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},
|
5
|
+
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
|
@@ -10,7 +10,9 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
10
10
|
private lastPath;
|
11
11
|
private lastPathStart;
|
12
12
|
private mainGroup;
|
13
|
+
private overwrittenAttrs;
|
13
14
|
constructor(elem: SVGSVGElement, viewport: Viewport);
|
15
|
+
setRootSVGAttribute(name: string, value: string | null): void;
|
14
16
|
displaySize(): Vec2;
|
15
17
|
clear(): void;
|
16
18
|
protected beginPath(startPoint: Point2): void;
|
@@ -6,13 +6,38 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
6
6
|
constructor(elem, viewport) {
|
7
7
|
super(viewport);
|
8
8
|
this.elem = elem;
|
9
|
+
this.overwrittenAttrs = {};
|
9
10
|
this.clear();
|
10
11
|
}
|
12
|
+
// Sets an attribute on the root SVG element.
|
13
|
+
setRootSVGAttribute(name, value) {
|
14
|
+
// Make the original value of the attribute restorable on clear
|
15
|
+
if (!(name in this.overwrittenAttrs)) {
|
16
|
+
this.overwrittenAttrs[name] = this.elem.getAttribute(name);
|
17
|
+
}
|
18
|
+
if (value !== null) {
|
19
|
+
this.elem.setAttribute(name, value);
|
20
|
+
}
|
21
|
+
else {
|
22
|
+
this.elem.removeAttribute(name);
|
23
|
+
}
|
24
|
+
}
|
11
25
|
displaySize() {
|
12
26
|
return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
|
13
27
|
}
|
14
28
|
clear() {
|
15
29
|
this.mainGroup = document.createElementNS(svgNameSpace, 'g');
|
30
|
+
// Restore all alltributes
|
31
|
+
for (const attrName in this.overwrittenAttrs) {
|
32
|
+
const value = this.overwrittenAttrs[attrName];
|
33
|
+
if (value) {
|
34
|
+
this.elem.setAttribute(attrName, value);
|
35
|
+
}
|
36
|
+
else {
|
37
|
+
this.elem.removeAttribute(attrName);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
this.overwrittenAttrs = {};
|
16
41
|
// Remove all children
|
17
42
|
this.elem.replaceChildren(this.mainGroup);
|
18
43
|
}
|
@@ -1,11 +1,10 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
|
-
import { ToolbarLocalization } from './
|
2
|
+
import { ToolbarLocalization } from './localization';
|
3
3
|
export default class HTMLToolbar {
|
4
4
|
private editor;
|
5
5
|
private localizationTable;
|
6
6
|
private container;
|
7
7
|
private penTypes;
|
8
|
-
static defaultLocalization: ToolbarLocalization;
|
9
8
|
constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
|
10
9
|
setupColorPickers(): void;
|
11
10
|
addActionButton(text: string, command: () => void, parent?: Element): HTMLButtonElement;
|
@@ -13,6 +13,7 @@ import EventDispatcher from '../EventDispatcher';
|
|
13
13
|
import { makeArrowBuilder } from '../components/builders/ArrowBuilder';
|
14
14
|
import { makeLineBuilder } from '../components/builders/LineBuilder';
|
15
15
|
import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../components/builders/RectangleBuilder';
|
16
|
+
import { defaultToolbarLocalization } from './localization';
|
16
17
|
const primaryForegroundFill = `
|
17
18
|
style='fill: var(--primary-foreground-color);'
|
18
19
|
`;
|
@@ -431,7 +432,7 @@ class PenWidget extends ToolbarWidget {
|
|
431
432
|
}
|
432
433
|
PenWidget.idCounter = 0;
|
433
434
|
export default class HTMLToolbar {
|
434
|
-
constructor(editor, parent, localizationTable =
|
435
|
+
constructor(editor, parent, localizationTable = defaultToolbarLocalization) {
|
435
436
|
this.editor = editor;
|
436
437
|
this.localizationTable = localizationTable;
|
437
438
|
this.container = document.createElement('div');
|
@@ -550,22 +551,3 @@ export default class HTMLToolbar {
|
|
550
551
|
this.addUndoRedoButtons();
|
551
552
|
}
|
552
553
|
}
|
553
|
-
HTMLToolbar.defaultLocalization = {
|
554
|
-
pen: 'Pen',
|
555
|
-
eraser: 'Eraser',
|
556
|
-
select: 'Select',
|
557
|
-
touchDrawing: 'Touch Drawing',
|
558
|
-
thicknessLabel: 'Thickness: ',
|
559
|
-
colorLabel: 'Color: ',
|
560
|
-
resizeImageToSelection: 'Resize image to selection',
|
561
|
-
undo: 'Undo',
|
562
|
-
redo: 'Redo',
|
563
|
-
selectObjectType: 'Object type: ',
|
564
|
-
freehandPen: 'Freehand',
|
565
|
-
arrowPen: 'Arrow',
|
566
|
-
linePen: 'Line',
|
567
|
-
outlinedRectanglePen: 'Outlined rectangle',
|
568
|
-
filledRectanglePen: 'Filled rectangle',
|
569
|
-
dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
|
570
|
-
dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
|
571
|
-
};
|
@@ -0,0 +1,20 @@
|
|
1
|
+
export interface ToolbarLocalization {
|
2
|
+
outlinedRectanglePen: string;
|
3
|
+
filledRectanglePen: string;
|
4
|
+
linePen: string;
|
5
|
+
arrowPen: string;
|
6
|
+
freehandPen: string;
|
7
|
+
selectObjectType: string;
|
8
|
+
colorLabel: string;
|
9
|
+
pen: string;
|
10
|
+
eraser: string;
|
11
|
+
select: string;
|
12
|
+
touchDrawing: string;
|
13
|
+
thicknessLabel: string;
|
14
|
+
resizeImageToSelection: string;
|
15
|
+
undo: string;
|
16
|
+
redo: string;
|
17
|
+
dropdownShown: (toolName: string) => string;
|
18
|
+
dropdownHidden: (toolName: string) => string;
|
19
|
+
}
|
20
|
+
export declare const defaultToolbarLocalization: ToolbarLocalization;
|
@@ -0,0 +1,19 @@
|
|
1
|
+
export const defaultToolbarLocalization = {
|
2
|
+
pen: 'Pen',
|
3
|
+
eraser: 'Eraser',
|
4
|
+
select: 'Select',
|
5
|
+
touchDrawing: 'Touch Drawing',
|
6
|
+
thicknessLabel: 'Thickness: ',
|
7
|
+
colorLabel: 'Color: ',
|
8
|
+
resizeImageToSelection: 'Resize image to selection',
|
9
|
+
undo: 'Undo',
|
10
|
+
redo: 'Redo',
|
11
|
+
selectObjectType: 'Object type: ',
|
12
|
+
freehandPen: 'Freehand',
|
13
|
+
arrowPen: 'Arrow',
|
14
|
+
linePen: 'Line',
|
15
|
+
outlinedRectanglePen: 'Outlined rectangle',
|
16
|
+
filledRectanglePen: 'Filled rectangle',
|
17
|
+
dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
|
18
|
+
dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
|
19
|
+
};
|
@@ -2,22 +2,3 @@ export declare enum ToolbarButtonType {
|
|
2
2
|
ToggleButton = 0,
|
3
3
|
ActionButton = 1
|
4
4
|
}
|
5
|
-
export interface ToolbarLocalization {
|
6
|
-
outlinedRectanglePen: string;
|
7
|
-
filledRectanglePen: string;
|
8
|
-
linePen: string;
|
9
|
-
arrowPen: string;
|
10
|
-
freehandPen: string;
|
11
|
-
selectObjectType: string;
|
12
|
-
colorLabel: string;
|
13
|
-
pen: string;
|
14
|
-
eraser: string;
|
15
|
-
select: string;
|
16
|
-
touchDrawing: string;
|
17
|
-
thicknessLabel: string;
|
18
|
-
resizeImageToSelection: string;
|
19
|
-
undo: string;
|
20
|
-
redo: string;
|
21
|
-
dropdownShown: (toolName: string) => string;
|
22
|
-
dropdownHidden: (toolName: string) => string;
|
23
|
-
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
5
|
+
<meta charset="utf-8"/>
|
6
|
+
<title>Editor from a bundle</title>
|
7
|
+
<style>
|
8
|
+
body .imageEditorContainer {
|
9
|
+
height: 800px;
|
10
|
+
|
11
|
+
--primary-background-color: green;
|
12
|
+
--primary-background-color-transparent: rgba(255, 240, 200, 0.5);
|
13
|
+
--secondary-background-color: yellow;
|
14
|
+
--primary-foreground-color: black;
|
15
|
+
--secondary-foreground-color: black;
|
16
|
+
}
|
17
|
+
</style>
|
18
|
+
</head>
|
19
|
+
<body>
|
20
|
+
<p>
|
21
|
+
This file tests the bundled version of <code>js-draw</code>.
|
22
|
+
Be sure to run <code>yarn build</code> before opening this!
|
23
|
+
</p>
|
24
|
+
<script src="../dist/bundle.js"></script>
|
25
|
+
<script>
|
26
|
+
const editor1 = new jsdraw.Editor(document.body, {
|
27
|
+
wheelEventsEnabled: false,
|
28
|
+
});
|
29
|
+
editor1.addToolbar();
|
30
|
+
|
31
|
+
const editor2 = new jsdraw.Editor(document.body);
|
32
|
+
editor2.addToolbar();
|
33
|
+
</script>
|
34
|
+
</body>
|
35
|
+
</html>
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.7",
|
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",
|
@@ -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,8 +70,13 @@
|
|
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.8.2",
|
79
|
+
"webpack": "^5.74.0"
|
71
80
|
},
|
72
81
|
"bugs": {
|
73
82
|
"url": "https://github.com/personalizedrefrigerator/js-draw/issues"
|
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);
|
@@ -160,8 +186,18 @@ export class Editor {
|
|
160
186
|
evt, pointers[evt.pointerId]?.down ?? false, this.viewport
|
161
187
|
);
|
162
188
|
if (pointer.down) {
|
163
|
-
pointers[pointer.id]
|
189
|
+
const prevData = pointers[pointer.id];
|
190
|
+
|
191
|
+
if (prevData) {
|
192
|
+
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
164
193
|
|
194
|
+
// If the pointer moved less than two pixels, don't send a new event.
|
195
|
+
if (distanceMoved < 2) {
|
196
|
+
return;
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
pointers[pointer.id] = pointer;
|
165
201
|
if (this.toolController.dispatchInputEvent({
|
166
202
|
kind: InputEvtType.PointerMoveEvt,
|
167
203
|
current: pointer,
|
@@ -210,6 +246,12 @@ export class Editor {
|
|
210
246
|
this.container.addEventListener('wheel', evt => {
|
211
247
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
212
248
|
|
249
|
+
// Process wheel events if the ctrl key is down -- we do want to handle
|
250
|
+
// pinch-zooming.
|
251
|
+
if (!this.settings.wheelEventsEnabled && !evt.ctrlKey) {
|
252
|
+
return;
|
253
|
+
}
|
254
|
+
|
213
255
|
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
214
256
|
delta = delta.times(15);
|
215
257
|
} else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
@@ -405,6 +447,7 @@ export class Editor {
|
|
405
447
|
result.setAttribute('viewBox', `${rect.x} ${rect.y} ${rect.w} ${rect.h}`);
|
406
448
|
result.setAttribute('width', `${rect.w}`);
|
407
449
|
result.setAttribute('height', `${rect.h}`);
|
450
|
+
console.log('res', result);
|
408
451
|
|
409
452
|
// Ensure the image can be identified as an SVG if downloaded.
|
410
453
|
// See https://jwatt.org/svg/authoring/
|
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/SVGLoader.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import Color4 from './Color4';
|
2
2
|
import AbstractComponent from './components/AbstractComponent';
|
3
3
|
import Stroke from './components/Stroke';
|
4
|
+
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
4
5
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
5
6
|
import Path from './geometry/Path';
|
6
7
|
import Rect2 from './geometry/Rect2';
|
@@ -66,10 +67,14 @@ export default class SVGLoader implements ImageLoader {
|
|
66
67
|
const parts = pathData.split('M');
|
67
68
|
let isFirst = true;
|
68
69
|
for (const part of parts) {
|
69
|
-
|
70
|
+
// Skip effective no-ops -- moveTos without additional commands.
|
71
|
+
const isNoOpMoveTo = /^[0-9., \t\n]+$/.exec(part);
|
72
|
+
|
73
|
+
if (part !== '' && !isNoOpMoveTo) {
|
70
74
|
// We split the path by moveTo commands, so add the 'M' back in
|
71
75
|
// if it was present.
|
72
76
|
const current = !isFirst ? `M${part}` : part;
|
77
|
+
|
73
78
|
const path = Path.fromString(current);
|
74
79
|
const spec = path.toRenderable(style);
|
75
80
|
result.push(spec);
|
@@ -123,6 +128,10 @@ export default class SVGLoader implements ImageLoader {
|
|
123
128
|
this.rootViewBox = new Rect2(x, y, width, height);
|
124
129
|
}
|
125
130
|
|
131
|
+
private updateSVGAttrs(node: SVGSVGElement) {
|
132
|
+
this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
133
|
+
}
|
134
|
+
|
126
135
|
private async visit(node: Element) {
|
127
136
|
this.totalToProcess += node.childElementCount;
|
128
137
|
|
@@ -135,6 +144,7 @@ export default class SVGLoader implements ImageLoader {
|
|
135
144
|
break;
|
136
145
|
case 'svg':
|
137
146
|
this.updateViewBox(node as SVGSVGElement);
|
147
|
+
this.updateSVGAttrs(node as SVGSVGElement);
|
138
148
|
break;
|
139
149
|
default:
|
140
150
|
console.warn('Unknown SVG element,', node);
|
@@ -154,6 +164,13 @@ export default class SVGLoader implements ImageLoader {
|
|
154
164
|
await this.onProgress?.(this.processedCount, this.totalToProcess);
|
155
165
|
}
|
156
166
|
|
167
|
+
// Get SVG element attributes (e.g. xlink=...)
|
168
|
+
private getSourceAttrs(node: SVGSVGElement): Array<[string, string|null]> {
|
169
|
+
return node.getAttributeNames().map(attr => {
|
170
|
+
return [ attr, node.getAttribute(attr) ];
|
171
|
+
});
|
172
|
+
}
|
173
|
+
|
157
174
|
public async start(
|
158
175
|
onAddComponent: ComponentAddedListener, onProgress: OnProgressListener
|
159
176
|
): Promise<Rect2> {
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Mat33 from '../geometry/Mat33';
|
3
|
+
import Rect2 from '../geometry/Rect2';
|
4
|
+
import AbstractRenderer from '../rendering/AbstractRenderer';
|
5
|
+
import SVGRenderer from '../rendering/SVGRenderer';
|
6
|
+
import AbstractComponent from './AbstractComponent';
|
7
|
+
import { ImageComponentLocalization } from './localization';
|
8
|
+
|
9
|
+
// Stores global SVG attributes (e.g. namespace identifiers.)
|
10
|
+
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
11
|
+
protected contentBBox: Rect2;
|
12
|
+
public constructor(private readonly attrs: Array<[string, string|null]>) {
|
13
|
+
super();
|
14
|
+
this.contentBBox = Rect2.empty;
|
15
|
+
}
|
16
|
+
|
17
|
+
public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
|
18
|
+
if (!(canvas instanceof SVGRenderer)) {
|
19
|
+
// Don't draw unrenderable objects if we can't
|
20
|
+
return;
|
21
|
+
}
|
22
|
+
|
23
|
+
console.log('Rendering to SVG.', this.attrs);
|
24
|
+
for (const [ attr, value ] of this.attrs) {
|
25
|
+
canvas.setRootSVGAttribute(attr, value);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
public intersects(_lineSegment: LineSegment2): boolean {
|
30
|
+
return false;
|
31
|
+
}
|
32
|
+
|
33
|
+
protected applyTransformation(_affineTransfm: Mat33): void {
|
34
|
+
}
|
35
|
+
|
36
|
+
public description(localization: ImageComponentLocalization): string {
|
37
|
+
return localization.svgObject;
|
38
|
+
}
|
39
|
+
}
|