js-draw 0.9.3 → 0.10.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.
- package/.firebase/hosting.ZG9jcw.cache +338 -0
- package/.github/ISSUE_TEMPLATE/translation.yml +9 -1
- package/CHANGELOG.md +8 -0
- package/build_tools/buildTranslationTemplate.ts +6 -4
- package/dist/build_tools/buildTranslationTemplate.js +5 -4
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +1 -0
- package/dist/src/Color4.js +34 -15
- package/dist/src/Editor.d.ts +2 -2
- package/dist/src/Editor.js +2 -3
- package/dist/src/EditorImage.d.ts +1 -1
- package/dist/src/EventDispatcher.d.ts +1 -1
- package/dist/src/SVGLoader.d.ts +2 -2
- package/dist/src/SVGLoader.js +25 -11
- package/dist/src/UndoRedoHistory.d.ts +2 -2
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/commands/SerializableCommand.d.ts +1 -1
- package/dist/src/components/AbstractComponent.d.ts +3 -3
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.js +3 -3
- package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
- package/dist/src/components/builders/types.d.ts +1 -1
- package/dist/src/components/util/StrokeSmoother.d.ts +1 -1
- package/dist/src/math/Mat33.d.ts +1 -1
- package/dist/src/math/Path.d.ts +1 -1
- package/dist/src/math/Vec2.d.ts +2 -2
- package/dist/src/rendering/caching/testUtils.d.ts +1 -1
- package/dist/src/rendering/caching/types.d.ts +2 -2
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -0
- package/dist/src/rendering/renderers/SVGRenderer.js +23 -6
- package/dist/src/toolbar/HTMLToolbar.d.ts +6 -1
- package/dist/src/toolbar/HTMLToolbar.js +24 -27
- package/dist/src/toolbar/IconProvider.d.ts +1 -1
- package/dist/src/toolbar/makeColorInput.d.ts +2 -2
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -3
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +2 -2
- package/dist/src/toolbar/widgets/BaseWidget.js +9 -4
- package/dist/src/tools/BaseTool.js +4 -4
- package/dist/src/tools/PanZoom.d.ts +5 -1
- package/dist/src/tools/PanZoom.js +108 -10
- package/dist/src/tools/PipetteTool.d.ts +1 -1
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -3
- package/dist/src/tools/SelectionTool/SelectionHandle.js +1 -1
- package/dist/src/tools/ToolbarShortcutHandler.d.ts +1 -1
- package/dist/src/types.d.ts +8 -8
- package/dist/src/{language → util}/assertions.d.ts +0 -0
- package/dist/src/{language → util}/assertions.js +1 -0
- package/dist/src/util/untilNextAnimationFrame.d.ts +3 -0
- package/dist/src/util/untilNextAnimationFrame.js +7 -0
- package/package.json +1 -1
- package/src/Color4.test.ts +7 -0
- package/src/Color4.ts +47 -18
- package/src/Editor.toSVG.test.ts +84 -0
- package/src/Editor.ts +2 -3
- package/src/SVGLoader.ts +26 -10
- package/src/components/builders/FreehandLineBuilder.ts +3 -3
- package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +1 -1
- package/src/rendering/renderers/SVGRenderer.ts +23 -5
- package/src/toolbar/HTMLToolbar.ts +33 -30
- package/src/toolbar/widgets/ActionButtonWidget.ts +2 -2
- package/src/toolbar/widgets/BaseWidget.ts +9 -4
- package/src/tools/PanZoom.ts +124 -7
- package/src/tools/SelectionTool/SelectionHandle.ts +1 -1
- package/src/{language → util}/assertions.ts +1 -0
- package/src/util/untilNextAnimationFrame.ts +9 -0
@@ -1,15 +1,15 @@
|
|
1
1
|
import { EditorEventType } from '../types';
|
2
2
|
export default class BaseTool {
|
3
|
-
onPointerDown(_event) { return false; }
|
4
|
-
onPointerMove(_event) { }
|
5
|
-
onPointerUp(_event) { }
|
6
|
-
onGestureCancel() { }
|
7
3
|
constructor(notifier, description) {
|
8
4
|
this.notifier = notifier;
|
9
5
|
this.description = description;
|
10
6
|
this.enabled = true;
|
11
7
|
this.group = null;
|
12
8
|
}
|
9
|
+
onPointerDown(_event) { return false; }
|
10
|
+
onPointerMove(_event) { }
|
11
|
+
onPointerUp(_event) { }
|
12
|
+
onGestureCancel() { }
|
13
13
|
onWheel(_event) {
|
14
14
|
return false;
|
15
15
|
}
|
@@ -24,15 +24,19 @@ export default class PanZoom extends BaseTool {
|
|
24
24
|
private lastAngle;
|
25
25
|
private lastDist;
|
26
26
|
private lastScreenCenter;
|
27
|
+
private lastTimestamp;
|
28
|
+
private inertialScroller;
|
29
|
+
private velocity;
|
27
30
|
constructor(editor: Editor, mode: PanZoomMode, description: string);
|
28
31
|
computePinchData(p1: Pointer, p2: Pointer): PinchData;
|
29
32
|
private allPointersAreOfType;
|
30
33
|
onPointerDown({ allPointers: pointers }: PointerEvt): boolean;
|
34
|
+
private updateVelocity;
|
31
35
|
private getCenterDelta;
|
32
36
|
private handleTwoFingerMove;
|
33
37
|
private handleOneFingerMove;
|
34
38
|
onPointerMove({ allPointers }: PointerEvt): void;
|
35
|
-
onPointerUp(
|
39
|
+
onPointerUp(event: PointerEvt): void;
|
36
40
|
onGestureCancel(): void;
|
37
41
|
private updateTransform;
|
38
42
|
onWheel({ delta, screenPos }: WheelEvt): boolean;
|
@@ -1,8 +1,18 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
1
10
|
import Mat33 from '../math/Mat33';
|
2
11
|
import { Vec2 } from '../math/Vec2';
|
3
12
|
import Vec3 from '../math/Vec3';
|
4
13
|
import { PointerDevice } from '../Pointer';
|
5
14
|
import { EditorEventType } from '../types';
|
15
|
+
import untilNextAnimationFrame from '../util/untilNextAnimationFrame';
|
6
16
|
import { Viewport } from '../Viewport';
|
7
17
|
import BaseTool from './BaseTool';
|
8
18
|
export var PanZoomMode;
|
@@ -14,12 +24,55 @@ export var PanZoomMode;
|
|
14
24
|
PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard";
|
15
25
|
PanZoomMode[PanZoomMode["RotationLocked"] = 32] = "RotationLocked";
|
16
26
|
})(PanZoomMode || (PanZoomMode = {}));
|
27
|
+
class InertialScroller {
|
28
|
+
constructor(initialVelocity, scrollBy, onComplete) {
|
29
|
+
this.initialVelocity = initialVelocity;
|
30
|
+
this.scrollBy = scrollBy;
|
31
|
+
this.onComplete = onComplete;
|
32
|
+
this.running = false;
|
33
|
+
this.start();
|
34
|
+
}
|
35
|
+
start() {
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
37
|
+
if (this.running) {
|
38
|
+
return;
|
39
|
+
}
|
40
|
+
let currentVelocity = this.initialVelocity;
|
41
|
+
let lastTime = (new Date()).getTime();
|
42
|
+
this.running = true;
|
43
|
+
const maxSpeed = 8000; // units/s
|
44
|
+
const minSpeed = 200; // units/s
|
45
|
+
if (currentVelocity.magnitude() > maxSpeed) {
|
46
|
+
currentVelocity = currentVelocity.normalized().times(maxSpeed);
|
47
|
+
}
|
48
|
+
while (this.running && currentVelocity.magnitude() > minSpeed) {
|
49
|
+
const nowTime = (new Date()).getTime();
|
50
|
+
const dt = (nowTime - lastTime) / 1000;
|
51
|
+
currentVelocity = currentVelocity.times(Math.pow(1 / 8, dt));
|
52
|
+
this.scrollBy(currentVelocity.times(dt));
|
53
|
+
yield untilNextAnimationFrame();
|
54
|
+
lastTime = nowTime;
|
55
|
+
}
|
56
|
+
if (this.running) {
|
57
|
+
this.stop();
|
58
|
+
}
|
59
|
+
});
|
60
|
+
}
|
61
|
+
stop() {
|
62
|
+
if (this.running) {
|
63
|
+
this.running = false;
|
64
|
+
this.onComplete();
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
17
68
|
export default class PanZoom extends BaseTool {
|
18
69
|
constructor(editor, mode, description) {
|
19
70
|
super(editor.notifier, description);
|
20
71
|
this.editor = editor;
|
21
72
|
this.mode = mode;
|
22
73
|
this.transform = null;
|
74
|
+
this.inertialScroller = null;
|
75
|
+
this.velocity = null;
|
23
76
|
}
|
24
77
|
// Returns information about the pointers in a gesture
|
25
78
|
computePinchData(p1, p2) {
|
@@ -34,8 +87,9 @@ export default class PanZoom extends BaseTool {
|
|
34
87
|
return pointers.every(pointer => pointer.device === kind);
|
35
88
|
}
|
36
89
|
onPointerDown({ allPointers: pointers }) {
|
37
|
-
var _a;
|
90
|
+
var _a, _b;
|
38
91
|
let handlingGesture = false;
|
92
|
+
(_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
|
39
93
|
const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
|
40
94
|
const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
|
41
95
|
if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
|
@@ -52,11 +106,25 @@ export default class PanZoom extends BaseTool {
|
|
52
106
|
handlingGesture = true;
|
53
107
|
}
|
54
108
|
if (handlingGesture) {
|
55
|
-
|
109
|
+
this.lastTimestamp = (new Date()).getTime();
|
110
|
+
(_b = this.transform) !== null && _b !== void 0 ? _b : (this.transform = Viewport.transformBy(Mat33.identity));
|
56
111
|
this.editor.display.setDraftMode(true);
|
57
112
|
}
|
58
113
|
return handlingGesture;
|
59
114
|
}
|
115
|
+
updateVelocity(currentCenter) {
|
116
|
+
const deltaPos = currentCenter.minus(this.lastScreenCenter);
|
117
|
+
const deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
|
118
|
+
const currentVelocity = deltaPos.times(1 / deltaTime);
|
119
|
+
let smoothedVelocity = currentVelocity;
|
120
|
+
if (deltaTime === 0) {
|
121
|
+
return;
|
122
|
+
}
|
123
|
+
if (this.velocity) {
|
124
|
+
smoothedVelocity = this.velocity.lerp(smoothedVelocity, 0.5);
|
125
|
+
}
|
126
|
+
this.velocity = smoothedVelocity;
|
127
|
+
}
|
60
128
|
// Returns the change in position of the center of the given group of pointers.
|
61
129
|
// Assumes this.lastScreenCenter has been set appropriately.
|
62
130
|
getCenterDelta(screenCenter) {
|
@@ -71,6 +139,7 @@ export default class PanZoom extends BaseTool {
|
|
71
139
|
if (this.isRotationLocked()) {
|
72
140
|
rotation = 0;
|
73
141
|
}
|
142
|
+
this.updateVelocity(screenCenter);
|
74
143
|
const transformUpdate = Mat33.translation(delta)
|
75
144
|
.rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
|
76
145
|
.rightMul(Mat33.zRotation(rotation, canvasCenter));
|
@@ -82,6 +151,7 @@ export default class PanZoom extends BaseTool {
|
|
82
151
|
handleOneFingerMove(pointer) {
|
83
152
|
const delta = this.getCenterDelta(pointer.screenPos);
|
84
153
|
this.transform = Viewport.transformBy(this.transform.transform.rightMul(Mat33.translation(delta)));
|
154
|
+
this.updateVelocity(pointer.screenPos);
|
85
155
|
this.lastScreenCenter = pointer.screenPos;
|
86
156
|
}
|
87
157
|
onPointerMove({ allPointers }) {
|
@@ -96,18 +166,42 @@ export default class PanZoom extends BaseTool {
|
|
96
166
|
}
|
97
167
|
lastTransform.unapply(this.editor);
|
98
168
|
this.transform.apply(this.editor);
|
169
|
+
this.lastTimestamp = (new Date()).getTime();
|
99
170
|
}
|
100
|
-
onPointerUp(
|
101
|
-
|
102
|
-
|
103
|
-
|
171
|
+
onPointerUp(event) {
|
172
|
+
var _a;
|
173
|
+
const onComplete = () => {
|
174
|
+
if (this.transform) {
|
175
|
+
this.transform.unapply(this.editor);
|
176
|
+
this.editor.dispatch(this.transform, false);
|
177
|
+
}
|
178
|
+
this.editor.display.setDraftMode(false);
|
179
|
+
this.transform = null;
|
180
|
+
this.velocity = Vec2.zero;
|
181
|
+
};
|
182
|
+
const shouldInertialScroll = event.current.device === PointerDevice.Touch && event.allPointers.length === 1;
|
183
|
+
if (shouldInertialScroll && this.velocity !== null) {
|
184
|
+
(_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
|
185
|
+
this.inertialScroller = new InertialScroller(this.velocity, (scrollDelta) => {
|
186
|
+
if (!this.transform) {
|
187
|
+
return;
|
188
|
+
}
|
189
|
+
const canvasDelta = this.editor.viewport.screenToCanvasTransform.transformVec3(scrollDelta);
|
190
|
+
// Scroll by scrollDelta
|
191
|
+
this.transform.unapply(this.editor);
|
192
|
+
this.transform = Viewport.transformBy(this.transform.transform.rightMul(Mat33.translation(canvasDelta)));
|
193
|
+
this.transform.apply(this.editor);
|
194
|
+
}, onComplete);
|
195
|
+
}
|
196
|
+
else {
|
197
|
+
onComplete();
|
104
198
|
}
|
105
|
-
this.editor.display.setDraftMode(false);
|
106
|
-
this.transform = null;
|
107
199
|
}
|
108
200
|
onGestureCancel() {
|
109
|
-
var _a;
|
110
|
-
(_a = this.
|
201
|
+
var _a, _b;
|
202
|
+
(_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
|
203
|
+
this.velocity = Vec2.zero;
|
204
|
+
(_b = this.transform) === null || _b === void 0 ? void 0 : _b.unapply(this.editor);
|
111
205
|
this.editor.display.setDraftMode(false);
|
112
206
|
this.transform = null;
|
113
207
|
}
|
@@ -127,6 +221,8 @@ export default class PanZoom extends BaseTool {
|
|
127
221
|
}
|
128
222
|
}
|
129
223
|
onWheel({ delta, screenPos }) {
|
224
|
+
var _a;
|
225
|
+
(_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
|
130
226
|
// Reset the transformation -- wheel events are individual events, so we don't
|
131
227
|
// need to unapply/reapply.
|
132
228
|
this.transform = Viewport.transformBy(Mat33.identity);
|
@@ -140,6 +236,8 @@ export default class PanZoom extends BaseTool {
|
|
140
236
|
return true;
|
141
237
|
}
|
142
238
|
onKeyPress({ key, ctrlKey, altKey }) {
|
239
|
+
var _a;
|
240
|
+
(_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
|
143
241
|
if (!(this.mode & PanZoomMode.Keyboard)) {
|
144
242
|
return false;
|
145
243
|
}
|
@@ -2,7 +2,7 @@ import Color4 from '../Color4';
|
|
2
2
|
import Editor from '../Editor';
|
3
3
|
import { PointerEvt } from '../types';
|
4
4
|
import BaseTool from './BaseTool';
|
5
|
-
type ColorListener = (color: Color4 | null) => void;
|
5
|
+
declare type ColorListener = (color: Color4 | null) => void;
|
6
6
|
export default class PipetteTool extends BaseTool {
|
7
7
|
private editor;
|
8
8
|
private colorPreviewListener;
|
@@ -6,9 +6,9 @@ export declare enum HandleShape {
|
|
6
6
|
Square = 1
|
7
7
|
}
|
8
8
|
export declare const handleSize = 30;
|
9
|
-
export type DragStartCallback = (startPoint: Point2) => void;
|
10
|
-
export type DragUpdateCallback = (canvasPoint: Point2) => void;
|
11
|
-
export type DragEndCallback = () => void;
|
9
|
+
export declare type DragStartCallback = (startPoint: Point2) => void;
|
10
|
+
export declare type DragUpdateCallback = (canvasPoint: Point2) => void;
|
11
|
+
export declare type DragEndCallback = () => void;
|
12
12
|
export default class SelectionHandle {
|
13
13
|
readonly shape: HandleShape;
|
14
14
|
private readonly parentSide;
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
2
|
import { KeyPressEvent } from '../types';
|
3
3
|
import BaseTool from './BaseTool';
|
4
|
-
type KeyPressListener = (event: KeyPressEvent) => boolean;
|
4
|
+
declare type KeyPressListener = (event: KeyPressEvent) => boolean;
|
5
5
|
export default class ToolbarShortcutHandler extends BaseTool {
|
6
6
|
private listeners;
|
7
7
|
constructor(editor: Editor);
|
package/dist/src/types.d.ts
CHANGED
@@ -68,9 +68,9 @@ export interface PointerMoveEvt extends PointerEvtBase {
|
|
68
68
|
export interface PointerUpEvt extends PointerEvtBase {
|
69
69
|
readonly kind: InputEvtType.PointerUpEvt;
|
70
70
|
}
|
71
|
-
export type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
|
72
|
-
export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt | CopyEvent | PasteEvent;
|
73
|
-
export type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
|
71
|
+
export declare type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
|
72
|
+
export declare type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt | CopyEvent | PasteEvent;
|
73
|
+
export declare type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
|
74
74
|
export declare enum EditorEventType {
|
75
75
|
ToolEnabled = 0,
|
76
76
|
ToolDisabled = 1,
|
@@ -85,7 +85,7 @@ export declare enum EditorEventType {
|
|
85
85
|
ColorPickerColorSelected = 10,
|
86
86
|
ToolbarDropdownShown = 11
|
87
87
|
}
|
88
|
-
type EditorToolEventType = EditorEventType.ToolEnabled | EditorEventType.ToolDisabled | EditorEventType.ToolUpdated;
|
88
|
+
declare type EditorToolEventType = EditorEventType.ToolEnabled | EditorEventType.ToolDisabled | EditorEventType.ToolUpdated;
|
89
89
|
export interface EditorToolEvent {
|
90
90
|
readonly kind: EditorToolEventType;
|
91
91
|
readonly tool: BaseTool;
|
@@ -128,10 +128,10 @@ export interface ToolbarDropdownShownEvent {
|
|
128
128
|
readonly kind: EditorEventType.ToolbarDropdownShown;
|
129
129
|
readonly parentWidget: BaseWidget;
|
130
130
|
}
|
131
|
-
export type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent | ColorPickerToggled | ColorPickerColorSelected | ToolbarDropdownShownEvent;
|
132
|
-
export type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
|
133
|
-
export type ComponentAddedListener = (component: AbstractComponent) => void;
|
134
|
-
export type OnDetermineExportRectListener = (exportRect: Rect2) => void;
|
131
|
+
export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent | ColorPickerToggled | ColorPickerColorSelected | ToolbarDropdownShownEvent;
|
132
|
+
export declare type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
|
133
|
+
export declare type ComponentAddedListener = (component: AbstractComponent) => void;
|
134
|
+
export declare type OnDetermineExportRectListener = (exportRect: Rect2) => void;
|
135
135
|
export interface ImageLoader {
|
136
136
|
start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener): Promise<void>;
|
137
137
|
}
|
File without changes
|
package/package.json
CHANGED
package/src/Color4.test.ts
CHANGED
@@ -9,4 +9,11 @@ describe('Color4', () => {
|
|
9
9
|
it('should create #RRGGBBAA-format hex strings when there is an alpha component', () => {
|
10
10
|
expect(Color4.ofRGBA(1, 1, 1, 0.5).toHexString()).toBe('#ffffff80');
|
11
11
|
});
|
12
|
+
|
13
|
+
it('should parse rgb and rgba-format strings', () => {
|
14
|
+
expect(Color4.fromString('rgb(0, 0, 0)')).objEq(Color4.black);
|
15
|
+
expect(Color4.fromString('rgb ( 255, 0,\t 0)')).objEq(Color4.ofRGBA(1, 0, 0, 1));
|
16
|
+
expect(Color4.fromString('rgba ( 255, 0,\t 0, 0.5)')).objEq(Color4.ofRGBA(1, 0, 0, 0.5));
|
17
|
+
expect(Color4.fromString('rgba( 0, 0, 128, 0)')).objEq(Color4.ofRGBA(0, 0, 128/255, 0));
|
18
|
+
});
|
12
19
|
});
|
package/src/Color4.ts
CHANGED
@@ -73,26 +73,51 @@ export default class Color4 {
|
|
73
73
|
public static fromString(text: string): Color4 {
|
74
74
|
if (text.startsWith('#')) {
|
75
75
|
return Color4.fromHex(text);
|
76
|
-
}
|
76
|
+
}
|
77
|
+
|
78
|
+
if (text === 'none' || text === 'transparent') {
|
77
79
|
return Color4.transparent;
|
78
|
-
} else {
|
79
|
-
// Otherwise, try to use an HTML5Canvas to determine the color
|
80
|
-
const canvas = document.createElement('canvas');
|
81
|
-
canvas.width = 1;
|
82
|
-
canvas.height = 1;
|
83
|
-
|
84
|
-
const ctx = canvas.getContext('2d')!;
|
85
|
-
ctx.fillStyle = text;
|
86
|
-
ctx.fillRect(0, 0, 1, 1);
|
87
|
-
|
88
|
-
const data = ctx.getImageData(0, 0, 1, 1);
|
89
|
-
const red = data.data[0] / 255;
|
90
|
-
const green = data.data[1] / 255;
|
91
|
-
const blue = data.data[2] / 255;
|
92
|
-
const alpha = data.data[3] / 255;
|
93
|
-
|
94
|
-
return Color4.ofRGBA(red, green, blue, alpha);
|
95
80
|
}
|
81
|
+
|
82
|
+
// rgba?: Match both rgb and rgba strings.
|
83
|
+
// ([,0-9.]+): Match any string of only numeric, '.' and ',' characters.
|
84
|
+
const rgbRegex = /^rgba?\(([,0-9.]+)\)$/i;
|
85
|
+
const rgbMatch = text.replace(/\s*/g, '').match(rgbRegex);
|
86
|
+
|
87
|
+
if (rgbMatch) {
|
88
|
+
const componentsListStr = rgbMatch[1];
|
89
|
+
const componentsList = JSON.parse(`[ ${componentsListStr} ]`);
|
90
|
+
|
91
|
+
if (componentsList.length === 3) {
|
92
|
+
return Color4.ofRGB(
|
93
|
+
componentsList[0] / 255, componentsList[1] / 255, componentsList[2] / 255
|
94
|
+
);
|
95
|
+
} else if (componentsList.length === 4) {
|
96
|
+
return Color4.ofRGBA(
|
97
|
+
componentsList[0] / 255, componentsList[1] / 255, componentsList[2] / 255, componentsList[3]
|
98
|
+
);
|
99
|
+
} else {
|
100
|
+
throw new Error(`RGB string, ${text}, has wrong number of components: ${componentsList.length}`);
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
// Otherwise, try to use an HTMLCanvasElement to determine the color.
|
105
|
+
// Note: We may be unable to create an HTMLCanvasElement if running as a unit test.
|
106
|
+
const canvas = document.createElement('canvas');
|
107
|
+
canvas.width = 1;
|
108
|
+
canvas.height = 1;
|
109
|
+
|
110
|
+
const ctx = canvas.getContext('2d')!;
|
111
|
+
ctx.fillStyle = text;
|
112
|
+
ctx.fillRect(0, 0, 1, 1);
|
113
|
+
|
114
|
+
const data = ctx.getImageData(0, 0, 1, 1);
|
115
|
+
const red = data.data[0] / 255;
|
116
|
+
const green = data.data[1] / 255;
|
117
|
+
const blue = data.data[2] / 255;
|
118
|
+
const alpha = data.data[3] / 255;
|
119
|
+
|
120
|
+
return Color4.ofRGBA(red, green, blue, alpha);
|
96
121
|
}
|
97
122
|
|
98
123
|
/** @returns true if `this` and `other` are approximately equal. */
|
@@ -139,6 +164,10 @@ export default class Color4 {
|
|
139
164
|
return this.hexString;
|
140
165
|
}
|
141
166
|
|
167
|
+
public toString() {
|
168
|
+
return this.toHexString();
|
169
|
+
}
|
170
|
+
|
142
171
|
public static transparent = Color4.ofRGBA(0, 0, 0, 0);
|
143
172
|
public static red = Color4.ofRGB(1.0, 0.0, 0.0);
|
144
173
|
public static green = Color4.ofRGB(0.0, 1.0, 0.0);
|
package/src/Editor.toSVG.test.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { TextStyle } from './components/TextComponent';
|
2
2
|
import { Color4, Mat33, Rect2, TextComponent, EditorImage, Vec2 } from './lib';
|
3
|
+
import SVGLoader from './SVGLoader';
|
3
4
|
import createEditor from './testing/createEditor';
|
4
5
|
|
5
6
|
describe('Editor.toSVG', () => {
|
@@ -23,5 +24,88 @@ describe('Editor.toSVG', () => {
|
|
23
24
|
expect(allTSpans).toHaveLength(1);
|
24
25
|
expect(allTSpans[0].getAttribute('x')).toBe('0');
|
25
26
|
expect(allTSpans[0].getAttribute('y')).toBe('100');
|
27
|
+
expect(allTSpans[0].style.transform).toBe('');
|
28
|
+
});
|
29
|
+
|
30
|
+
it('should preserve empty tspans', async () => {
|
31
|
+
const editor = createEditor();
|
32
|
+
await editor.loadFrom(SVGLoader.fromString(`
|
33
|
+
<svg viewBox="0 0 500 500" width="500" height="500" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
|
34
|
+
<style id="js-draw-style-sheet">
|
35
|
+
path {
|
36
|
+
stroke-linecap:round;
|
37
|
+
stroke-linejoin:round;
|
38
|
+
}
|
39
|
+
</style>
|
40
|
+
<text style="transform: matrix(1, 0, 0, 1, 12, 35); font-family: sans-serif; font-size: 32px; fill: rgb(128, 51, 128);">Testing...<tspan x="3" y="40" style="font-family: sans-serif; font-size: 33px; fill: rgb(128, 51, 128);"></tspan><tspan x="3" y="70">Test 2. ☺</tspan></text>
|
41
|
+
</svg>
|
42
|
+
`, true));
|
43
|
+
|
44
|
+
const textNodesInImage = editor.image.getAllElements().filter(elem => elem instanceof TextComponent);
|
45
|
+
expect(
|
46
|
+
textNodesInImage
|
47
|
+
).toHaveLength(1);
|
48
|
+
|
49
|
+
const asSVG = editor.toSVG();
|
50
|
+
const textObject = asSVG.querySelector('text');
|
51
|
+
|
52
|
+
if (!textObject) {
|
53
|
+
throw new Error('No text object found');
|
54
|
+
}
|
55
|
+
|
56
|
+
const childTextNodes = textObject.querySelectorAll('tspan');
|
57
|
+
expect(childTextNodes).toHaveLength(2);
|
58
|
+
});
|
59
|
+
|
60
|
+
it('should preserve text child size/placement while not saving additional properties', async () => {
|
61
|
+
const secondLineText = 'This is a test of a thing that has been known to break. Will this test catch the issue?';
|
62
|
+
const thirdLineText = 'This is a test of saving/loading multi-line text...';
|
63
|
+
|
64
|
+
const editor = createEditor();
|
65
|
+
await editor.loadFrom(SVGLoader.fromString(`
|
66
|
+
<svg viewBox="0 0 500 500" width="500" height="500" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
|
67
|
+
<style id="js-draw-style-sheet">
|
68
|
+
path {
|
69
|
+
stroke-linecap:round;
|
70
|
+
stroke-linejoin:round;
|
71
|
+
}
|
72
|
+
</style>
|
73
|
+
<text style="transform: matrix(1, 0, 0, 1, 12, 35); font-family: sans-serif; font-size: 32px; fill: rgb(128, 51, 128);">Testing...<tspan x="3" y="40" style="font-family: sans-serif; font-size: 33px; fill: rgb(128, 51, 128);">${secondLineText}</tspan><tspan x="0" y="72" style="font-family: sans-serif; font-size: 32px; fill: rgb(128, 51, 128);">${thirdLineText}</tspan><tspan x="0" y="112" style="font-family: sans-serif; font-size: 32px; fill: rgb(128, 51, 128);">Will it pass or fail?</tspan></text>
|
74
|
+
</svg>
|
75
|
+
`, true));
|
76
|
+
|
77
|
+
expect(
|
78
|
+
editor.image.getAllElements().filter(elem => elem instanceof TextComponent)
|
79
|
+
).toHaveLength(1);
|
80
|
+
|
81
|
+
const asSVG = editor.toSVG();
|
82
|
+
const textObject = asSVG.querySelector('text');
|
83
|
+
|
84
|
+
if (!textObject) {
|
85
|
+
throw new Error('No text object found');
|
86
|
+
}
|
87
|
+
|
88
|
+
expect(textObject.style.transform.replace(/\s+/g, '')).toBe('matrix(1,0,0,1,12,35)');
|
89
|
+
expect(textObject.style.fontFamily).toBe('sans-serif');
|
90
|
+
expect(textObject.style.fontSize).toBe('32px');
|
91
|
+
|
92
|
+
const childTextNodes = textObject.querySelectorAll('tspan');
|
93
|
+
expect(childTextNodes).toHaveLength(3);
|
94
|
+
const firstChild = childTextNodes[0];
|
95
|
+
|
96
|
+
expect(firstChild.textContent).toBe(secondLineText);
|
97
|
+
expect(firstChild.style.transform).toBe('');
|
98
|
+
expect(firstChild.style.fontSize).toBe('33px');
|
99
|
+
expect(firstChild.getAttribute('x')).toBe('3');
|
100
|
+
expect(firstChild.getAttribute('y')).toBe('40');
|
101
|
+
|
102
|
+
// Should not save a fontSize when not necessary (same fill as parent text node)
|
103
|
+
const secondChild = childTextNodes[1];
|
104
|
+
expect(secondChild.style.fontSize ?? '').toBe('');
|
105
|
+
|
106
|
+
// Should not save additional "style" attributes when not necessary
|
107
|
+
// TODO: Uncomment before some future major version release. Currently a "fill" is set for every
|
108
|
+
// tspan to work around a loading bug.
|
109
|
+
//expect(secondChild.outerHTML).toBe(`<tspan x="0" y="72">${thirdLineText}</tspan>`);
|
26
110
|
});
|
27
111
|
});
|
package/src/Editor.ts
CHANGED
@@ -40,6 +40,7 @@ import getLocalizationTable from './localizations/getLocalizationTable';
|
|
40
40
|
import IconProvider from './toolbar/IconProvider';
|
41
41
|
import { toRoundedString } from './math/rounding';
|
42
42
|
import CanvasRenderer from './rendering/renderers/CanvasRenderer';
|
43
|
+
import untilNextAnimationFrame from './util/untilNextAnimationFrame';
|
43
44
|
|
44
45
|
type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
|
45
46
|
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
|
@@ -888,9 +889,7 @@ export class Editor {
|
|
888
889
|
if (countProcessed % 500 === 0) {
|
889
890
|
this.showLoadingWarning(countProcessed / totalToProcess);
|
890
891
|
this.rerender();
|
891
|
-
return
|
892
|
-
requestAnimationFrame(() => resolve());
|
893
|
-
});
|
892
|
+
return untilNextAnimationFrame();
|
894
893
|
}
|
895
894
|
|
896
895
|
return null;
|
package/src/SVGLoader.ts
CHANGED
@@ -44,12 +44,14 @@ export default class SVGLoader implements ImageLoader {
|
|
44
44
|
private source: SVGSVGElement, private onFinish?: OnFinishListener, private readonly storeUnknown: boolean = true) {
|
45
45
|
}
|
46
46
|
|
47
|
-
|
47
|
+
// If [computedStyles] is given, it is preferred to directly accessing node's style object.
|
48
|
+
private getStyle(node: SVGElement, computedStyles?: CSSStyleDeclaration) {
|
48
49
|
const style: RenderingStyle = {
|
49
50
|
fill: Color4.transparent,
|
50
51
|
};
|
51
52
|
|
52
|
-
|
53
|
+
// If possible, use computedStyles (allows property inheritance).
|
54
|
+
const fillAttribute = node.getAttribute('fill') ?? computedStyles?.fill ?? node.style.fill;
|
53
55
|
if (fillAttribute) {
|
54
56
|
try {
|
55
57
|
style.fill = Color4.fromString(fillAttribute);
|
@@ -58,19 +60,23 @@ export default class SVGLoader implements ImageLoader {
|
|
58
60
|
}
|
59
61
|
}
|
60
62
|
|
61
|
-
const strokeAttribute = node.getAttribute('stroke') ?? node.style.stroke;
|
62
|
-
const strokeWidthAttr = node.getAttribute('stroke-width') ?? node.style.strokeWidth;
|
63
|
-
if (strokeAttribute) {
|
63
|
+
const strokeAttribute = node.getAttribute('stroke') ?? computedStyles?.stroke ?? node.style.stroke;
|
64
|
+
const strokeWidthAttr = node.getAttribute('stroke-width') ?? computedStyles?.strokeWidth ?? node.style.strokeWidth;
|
65
|
+
if (strokeAttribute && strokeWidthAttr) {
|
64
66
|
try {
|
65
67
|
let width = parseFloat(strokeWidthAttr ?? '1');
|
66
68
|
if (!isFinite(width)) {
|
67
69
|
width = 0;
|
68
70
|
}
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
72
|
+
const strokeColor = Color4.fromString(strokeAttribute);
|
73
|
+
|
74
|
+
if (strokeColor.a > 0) {
|
75
|
+
style.stroke = {
|
76
|
+
width,
|
77
|
+
color: strokeColor,
|
78
|
+
};
|
79
|
+
}
|
74
80
|
} catch (e) {
|
75
81
|
console.error('Error parsing stroke data:', e);
|
76
82
|
}
|
@@ -230,6 +236,11 @@ export default class SVGLoader implements ImageLoader {
|
|
230
236
|
}
|
231
237
|
}
|
232
238
|
|
239
|
+
// If no content, the content is an empty string.
|
240
|
+
if (contentList.length === 0) {
|
241
|
+
contentList.push('');
|
242
|
+
}
|
243
|
+
|
233
244
|
// Compute styles.
|
234
245
|
const computedStyles = window.getComputedStyle(elem);
|
235
246
|
const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
|
@@ -247,7 +258,7 @@ export default class SVGLoader implements ImageLoader {
|
|
247
258
|
const style: TextStyle = {
|
248
259
|
size: fontSize,
|
249
260
|
fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
|
250
|
-
renderingStyle: this.getStyle(elem),
|
261
|
+
renderingStyle: this.getStyle(elem, computedStyles),
|
251
262
|
};
|
252
263
|
|
253
264
|
const supportedAttrs: string[] = [];
|
@@ -354,6 +365,9 @@ export default class SVGLoader implements ImageLoader {
|
|
354
365
|
this.updateViewBox(node as SVGSVGElement);
|
355
366
|
this.updateSVGAttrs(node as SVGSVGElement);
|
356
367
|
break;
|
368
|
+
case 'style':
|
369
|
+
this.addUnknownNode(node as SVGStyleElement);
|
370
|
+
break;
|
357
371
|
default:
|
358
372
|
console.warn('Unknown SVG element,', node);
|
359
373
|
if (!(node instanceof SVGElement)) {
|
@@ -432,6 +446,8 @@ export default class SVGLoader implements ImageLoader {
|
|
432
446
|
<html>
|
433
447
|
<head>
|
434
448
|
<title>SVG Loading Sandbox</title>
|
449
|
+
<meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
|
450
|
+
<meta charset='utf-8'/>
|
435
451
|
</head>
|
436
452
|
<body>
|
437
453
|
<script>
|