js-draw 0.13.0 → 0.14.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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +4 -4
- package/dist/src/SVGLoader.js +8 -2
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +6 -3
- package/dist/src/math/Path.js +10 -3
- package/dist/src/toolbar/IconProvider.d.ts +28 -2
- package/dist/src/toolbar/IconProvider.js +27 -2
- package/dist/src/toolbar/makeColorInput.js +8 -1
- package/dist/src/tools/PanZoom.d.ts +3 -1
- package/dist/src/tools/PanZoom.js +30 -5
- package/dist/src/tools/SelectionTool/Selection.d.ts +6 -0
- package/dist/src/tools/SelectionTool/Selection.js +12 -3
- package/dist/src/tools/SelectionTool/SelectionTool.js +5 -3
- package/dist/src/tools/SelectionTool/TransformMode.js +1 -1
- package/package.json +1 -1
- package/src/Editor.ts +4 -4
- package/src/SVGLoader.ts +9 -2
- package/src/Viewport.ts +7 -3
- package/src/math/Path.toString.test.ts +10 -0
- package/src/math/Path.ts +11 -3
- package/src/toolbar/IconProvider.ts +28 -6
- package/src/toolbar/makeColorInput.ts +9 -1
- package/src/toolbar/toolbar.css +3 -0
- package/src/tools/PanZoom.ts +36 -6
- package/src/tools/SelectionTool/Selection.ts +16 -5
- package/src/tools/SelectionTool/SelectionTool.ts +5 -4
- package/src/tools/SelectionTool/TransformMode.ts +1 -1
package/dist/src/Editor.js
CHANGED
@@ -196,7 +196,7 @@ export class Editor {
|
|
196
196
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
197
197
|
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
198
198
|
// pinch-zooming.
|
199
|
-
if (!evt.ctrlKey) {
|
199
|
+
if (!evt.ctrlKey && !evt.metaKey) {
|
200
200
|
if (!this.settings.wheelEventsEnabled) {
|
201
201
|
return;
|
202
202
|
}
|
@@ -213,7 +213,7 @@ export class Editor {
|
|
213
213
|
else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
214
214
|
delta = delta.times(100);
|
215
215
|
}
|
216
|
-
if (evt.ctrlKey) {
|
216
|
+
if (evt.ctrlKey || evt.metaKey) {
|
217
217
|
delta = Vec3.of(0, 0, evt.deltaY);
|
218
218
|
}
|
219
219
|
// Ensure that `pos` is relative to `this.container`
|
@@ -441,7 +441,7 @@ export class Editor {
|
|
441
441
|
else if (this.toolController.dispatchInputEvent({
|
442
442
|
kind: InputEvtType.KeyPressEvent,
|
443
443
|
key: evt.key,
|
444
|
-
ctrlKey: evt.ctrlKey,
|
444
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
445
445
|
altKey: evt.altKey,
|
446
446
|
})) {
|
447
447
|
evt.preventDefault();
|
@@ -454,7 +454,7 @@ export class Editor {
|
|
454
454
|
if (this.toolController.dispatchInputEvent({
|
455
455
|
kind: InputEvtType.KeyUpEvent,
|
456
456
|
key: evt.key,
|
457
|
-
ctrlKey: evt.ctrlKey,
|
457
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
458
458
|
altKey: evt.altKey,
|
459
459
|
})) {
|
460
460
|
evt.preventDefault();
|
package/dist/src/SVGLoader.js
CHANGED
@@ -202,7 +202,13 @@ export default class SVGLoader {
|
|
202
202
|
}
|
203
203
|
// Compute styles.
|
204
204
|
const computedStyles = window.getComputedStyle(elem);
|
205
|
-
const
|
205
|
+
const fontSizeExp = /^([-0-9.e]+)px/i;
|
206
|
+
// In some environments, computedStyles.fontSize can be increased by the system.
|
207
|
+
// Thus, to prevent text from growing on load/save, prefer .style.fontSize.
|
208
|
+
let fontSizeMatch = fontSizeExp.exec(elem.style.fontSize);
|
209
|
+
if (!fontSizeMatch) {
|
210
|
+
fontSizeMatch = fontSizeExp.exec(computedStyles.fontSize);
|
211
|
+
}
|
206
212
|
const supportedStyleAttrs = [
|
207
213
|
'fontFamily',
|
208
214
|
'transform',
|
@@ -390,7 +396,7 @@ export default class SVGLoader {
|
|
390
396
|
<meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
|
391
397
|
<meta charset='utf-8'/>
|
392
398
|
</head>
|
393
|
-
<body>
|
399
|
+
<body style='font-size: 12px;'>
|
394
400
|
<script>
|
395
401
|
console.error('JavaScript should not be able to run here!');
|
396
402
|
throw new Error(
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -44,6 +44,7 @@ export declare class Viewport {
|
|
44
44
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
45
45
|
*/
|
46
46
|
getScaleFactorToNearestPowerOfTen(): number;
|
47
|
+
private getScaleFactorToNearestPowerOf;
|
47
48
|
snapToGrid(canvasPos: Point2): Vec3;
|
48
49
|
/** Returns the size of one screen pixel in canvas units. */
|
49
50
|
getSizeOfPixelOnCanvas(): number;
|
package/dist/src/Viewport.js
CHANGED
@@ -84,13 +84,16 @@ export class Viewport {
|
|
84
84
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
85
85
|
*/
|
86
86
|
getScaleFactorToNearestPowerOfTen() {
|
87
|
+
return this.getScaleFactorToNearestPowerOf(10);
|
88
|
+
}
|
89
|
+
getScaleFactorToNearestPowerOf(powerOf) {
|
87
90
|
const scaleFactor = this.getScaleFactor();
|
88
|
-
return Math.pow(
|
91
|
+
return Math.pow(powerOf, Math.round(Math.log(scaleFactor) / Math.log(powerOf)));
|
89
92
|
}
|
90
93
|
snapToGrid(canvasPos) {
|
91
94
|
const snapCoordinate = (coordinate) => {
|
92
|
-
const scaleFactor = this.
|
93
|
-
const roundFactor = scaleFactor /
|
95
|
+
const scaleFactor = this.getScaleFactorToNearestPowerOf(2);
|
96
|
+
const roundFactor = scaleFactor / 50;
|
94
97
|
const snapped = Math.round(coordinate * roundFactor) / roundFactor;
|
95
98
|
return snapped;
|
96
99
|
};
|
package/dist/src/math/Path.js
CHANGED
@@ -347,6 +347,7 @@ export default class Path {
|
|
347
347
|
// @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
|
348
348
|
// conversions can lead to smaller output strings, but also take time.
|
349
349
|
static toString(startPoint, parts, onlyAbsCommands) {
|
350
|
+
var _a;
|
350
351
|
const result = [];
|
351
352
|
let prevPoint;
|
352
353
|
const addCommand = (command, ...points) => {
|
@@ -382,7 +383,7 @@ export default class Path {
|
|
382
383
|
commandString = `${command.toLowerCase()}${relativeCommandParts.join(' ')}`;
|
383
384
|
}
|
384
385
|
// Don't add no-ops.
|
385
|
-
if (commandString === 'l0,0') {
|
386
|
+
if (commandString === 'l0,0' || commandString === 'm0,0') {
|
386
387
|
return;
|
387
388
|
}
|
388
389
|
result.push(commandString);
|
@@ -390,9 +391,15 @@ export default class Path {
|
|
390
391
|
prevPoint = points[points.length - 1];
|
391
392
|
}
|
392
393
|
};
|
393
|
-
|
394
|
+
// Don't add two moveTos in a row (this can happen if
|
395
|
+
// the start point corresponds to a moveTo _and_ the first command is
|
396
|
+
// also a moveTo)
|
397
|
+
if (((_a = parts[0]) === null || _a === void 0 ? void 0 : _a.kind) !== PathCommandType.MoveTo) {
|
398
|
+
addCommand('M', startPoint);
|
399
|
+
}
|
394
400
|
let exhaustivenessCheck;
|
395
|
-
for (
|
401
|
+
for (let i = 0; i < parts.length; i++) {
|
402
|
+
const part = parts[i];
|
396
403
|
switch (part.kind) {
|
397
404
|
case PathCommandType.MoveTo:
|
398
405
|
addCommand('M', part.point);
|
@@ -2,7 +2,34 @@ import Color4 from '../Color4';
|
|
2
2
|
import { ComponentBuilderFactory } from '../components/builders/types';
|
3
3
|
import { TextStyle } from '../components/TextComponent';
|
4
4
|
import Pen from '../tools/Pen';
|
5
|
-
type IconType =
|
5
|
+
export type IconType = HTMLImageElement | SVGElement;
|
6
|
+
/**
|
7
|
+
* Provides icons that can be used in the toolbar, etc.
|
8
|
+
* Extend this class and override methods to customize icons.
|
9
|
+
*
|
10
|
+
* @example
|
11
|
+
* ```ts
|
12
|
+
* class CustomIconProvider extends jsdraw.IconProvider {
|
13
|
+
* // Use '☺' instead of the default dropdown symbol.
|
14
|
+
* public makeDropdownIcon() {
|
15
|
+
* const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
16
|
+
* icon.innerHTML = `
|
17
|
+
* <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
|
18
|
+
* `;
|
19
|
+
* icon.setAttribute('viewBox', '0 0 100 100');
|
20
|
+
* return icon;
|
21
|
+
* }
|
22
|
+
* }
|
23
|
+
*
|
24
|
+
* const icons = new CustomIconProvider();
|
25
|
+
* const editor = new jsdraw.Editor(document.body, {
|
26
|
+
* iconProvider: icons,
|
27
|
+
* });
|
28
|
+
*
|
29
|
+
* // Add a toolbar that uses these icons
|
30
|
+
* editor.addToolbar();
|
31
|
+
* ```
|
32
|
+
*/
|
6
33
|
export default class IconProvider {
|
7
34
|
makeUndoIcon(): IconType;
|
8
35
|
makeRedoIcon(mirror?: boolean): IconType;
|
@@ -30,4 +57,3 @@ export default class IconProvider {
|
|
30
57
|
makeDeleteSelectionIcon(): IconType;
|
31
58
|
makeSaveIcon(): IconType;
|
32
59
|
}
|
33
|
-
export {};
|
@@ -24,8 +24,33 @@ const checkerboardPatternDef = `
|
|
24
24
|
</pattern>
|
25
25
|
`;
|
26
26
|
const checkerboardPatternRef = 'url(#checkerboard)';
|
27
|
-
|
28
|
-
|
27
|
+
/**
|
28
|
+
* Provides icons that can be used in the toolbar, etc.
|
29
|
+
* Extend this class and override methods to customize icons.
|
30
|
+
*
|
31
|
+
* @example
|
32
|
+
* ```ts
|
33
|
+
* class CustomIconProvider extends jsdraw.IconProvider {
|
34
|
+
* // Use '☺' instead of the default dropdown symbol.
|
35
|
+
* public makeDropdownIcon() {
|
36
|
+
* const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
37
|
+
* icon.innerHTML = `
|
38
|
+
* <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
|
39
|
+
* `;
|
40
|
+
* icon.setAttribute('viewBox', '0 0 100 100');
|
41
|
+
* return icon;
|
42
|
+
* }
|
43
|
+
* }
|
44
|
+
*
|
45
|
+
* const icons = new CustomIconProvider();
|
46
|
+
* const editor = new jsdraw.Editor(document.body, {
|
47
|
+
* iconProvider: icons,
|
48
|
+
* });
|
49
|
+
*
|
50
|
+
* // Add a toolbar that uses these icons
|
51
|
+
* editor.addToolbar();
|
52
|
+
* ```
|
53
|
+
*/
|
29
54
|
export default class IconProvider {
|
30
55
|
makeUndoIcon() {
|
31
56
|
return this.makeRedoIcon(true);
|
@@ -9,7 +9,7 @@ export const makeColorInput = (editor, onColorChange) => {
|
|
9
9
|
colorInput.classList.add('coloris_input');
|
10
10
|
colorInputContainer.classList.add('color-input-container');
|
11
11
|
colorInputContainer.appendChild(colorInput);
|
12
|
-
addPipetteTool(editor, colorInputContainer, (color) => {
|
12
|
+
const pipetteController = addPipetteTool(editor, colorInputContainer, (color) => {
|
13
13
|
colorInput.value = color.toHexString();
|
14
14
|
onInputEnd();
|
15
15
|
// Update the color preview, if it exists (may be managed by Coloris).
|
@@ -41,6 +41,7 @@ export const makeColorInput = (editor, onColorChange) => {
|
|
41
41
|
kind: EditorEventType.ColorPickerToggled,
|
42
42
|
open: true,
|
43
43
|
});
|
44
|
+
pipetteController.cancel();
|
44
45
|
});
|
45
46
|
colorInput.addEventListener('close', () => {
|
46
47
|
editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
|
@@ -102,5 +103,11 @@ const addPipetteTool = (editor, container, onColorChange) => {
|
|
102
103
|
}
|
103
104
|
};
|
104
105
|
container.appendChild(pipetteButton);
|
106
|
+
return {
|
107
|
+
// Cancel a pipette color selection if one is in progress.
|
108
|
+
cancel: () => {
|
109
|
+
endColorSelectMode();
|
110
|
+
},
|
111
|
+
};
|
105
112
|
};
|
106
113
|
export default makeColorInput;
|
@@ -21,11 +21,12 @@ export default class PanZoom extends BaseTool {
|
|
21
21
|
private editor;
|
22
22
|
private mode;
|
23
23
|
private transform;
|
24
|
-
private lastAngle;
|
25
24
|
private lastDist;
|
26
25
|
private lastScreenCenter;
|
27
26
|
private lastTimestamp;
|
28
27
|
private lastPointerDownTimestamp;
|
28
|
+
private initialTouchAngle;
|
29
|
+
private initialViewportRotation;
|
29
30
|
private inertialScroller;
|
30
31
|
private velocity;
|
31
32
|
constructor(editor: Editor, mode: PanZoomMode, description: string);
|
@@ -34,6 +35,7 @@ export default class PanZoom extends BaseTool {
|
|
34
35
|
onPointerDown({ allPointers: pointers, current: currentPointer }: PointerEvt): boolean;
|
35
36
|
private updateVelocity;
|
36
37
|
private getCenterDelta;
|
38
|
+
private toSnappedRotationDelta;
|
37
39
|
private handleTwoFingerMove;
|
38
40
|
private handleOneFingerMove;
|
39
41
|
onPointerMove({ allPointers }: PointerEvt): void;
|
@@ -78,6 +78,8 @@ export default class PanZoom extends BaseTool {
|
|
78
78
|
this.mode = mode;
|
79
79
|
this.transform = null;
|
80
80
|
this.lastPointerDownTimestamp = 0;
|
81
|
+
this.initialTouchAngle = 0;
|
82
|
+
this.initialViewportRotation = 0;
|
81
83
|
this.inertialScroller = null;
|
82
84
|
this.velocity = null;
|
83
85
|
}
|
@@ -104,9 +106,10 @@ export default class PanZoom extends BaseTool {
|
|
104
106
|
const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
|
105
107
|
if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
|
106
108
|
const { screenCenter, angle, dist } = this.computePinchData(pointers[0], pointers[1]);
|
107
|
-
this.lastAngle = angle;
|
108
109
|
this.lastDist = dist;
|
109
110
|
this.lastScreenCenter = screenCenter;
|
111
|
+
this.initialTouchAngle = angle;
|
112
|
+
this.initialViewportRotation = this.editor.viewport.getRotationAngle();
|
110
113
|
handlingGesture = true;
|
111
114
|
}
|
112
115
|
else if (pointers.length === 1 && ((this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
|
@@ -149,20 +152,42 @@ export default class PanZoom extends BaseTool {
|
|
149
152
|
const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenCenter.minus(this.lastScreenCenter));
|
150
153
|
return delta;
|
151
154
|
}
|
155
|
+
// Snaps `angle` to common desired rotations. For example, if `touchAngle` corresponds
|
156
|
+
// to a viewport rotation of 90.1 degrees, this function returns a rotation delta that,
|
157
|
+
// when applied to the viewport, rotates the viewport to 90.0 degrees.
|
158
|
+
//
|
159
|
+
// Returns a snapped rotation delta that, when applied to the viewport, rotates the viewport,
|
160
|
+
// from its position on the last touchDown event, by `touchAngle - initialTouchAngle`.
|
161
|
+
toSnappedRotationDelta(touchAngle) {
|
162
|
+
const deltaAngle = touchAngle - this.initialTouchAngle;
|
163
|
+
let fullRotation = deltaAngle + this.initialViewportRotation;
|
164
|
+
const snapToMultipleOf = Math.PI / 2;
|
165
|
+
const roundedFullRotation = Math.round(fullRotation / snapToMultipleOf) * snapToMultipleOf;
|
166
|
+
// The maximum angle for which we snap the given angle to a multiple of
|
167
|
+
// `snapToMultipleOf`.
|
168
|
+
const maxSnapAngle = 0.07;
|
169
|
+
// Snap the rotation
|
170
|
+
if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
|
171
|
+
fullRotation = roundedFullRotation;
|
172
|
+
}
|
173
|
+
return fullRotation - this.editor.viewport.getRotationAngle();
|
174
|
+
}
|
152
175
|
handleTwoFingerMove(allPointers) {
|
153
176
|
const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
|
154
177
|
const delta = this.getCenterDelta(screenCenter);
|
155
|
-
let
|
178
|
+
let deltaRotation;
|
156
179
|
if (this.isRotationLocked()) {
|
157
|
-
|
180
|
+
deltaRotation = 0;
|
181
|
+
}
|
182
|
+
else {
|
183
|
+
deltaRotation = this.toSnappedRotationDelta(angle);
|
158
184
|
}
|
159
185
|
this.updateVelocity(screenCenter);
|
160
186
|
const transformUpdate = Mat33.translation(delta)
|
161
187
|
.rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
|
162
|
-
.rightMul(Mat33.zRotation(
|
188
|
+
.rightMul(Mat33.zRotation(deltaRotation, canvasCenter));
|
163
189
|
this.lastScreenCenter = screenCenter;
|
164
190
|
this.lastDist = dist;
|
165
|
-
this.lastAngle = angle;
|
166
191
|
this.transform = Viewport.transformBy(this.transform.transform.rightMul(transformUpdate));
|
167
192
|
}
|
168
193
|
handleOneFingerMove(pointer) {
|
@@ -23,6 +23,12 @@ export default class Selection {
|
|
23
23
|
getTransform(): Mat33;
|
24
24
|
get preTransformRegion(): Rect2;
|
25
25
|
get region(): Rect2;
|
26
|
+
/**
|
27
|
+
* Computes and returns the bounding box of the selection without
|
28
|
+
* any additional padding. Computes directly from the elements that are selected.
|
29
|
+
* @internal
|
30
|
+
*/
|
31
|
+
computeTightBoundingBox(): Rect2;
|
26
32
|
get regionRotation(): number;
|
27
33
|
get preTransformedScreenRegion(): Rect2;
|
28
34
|
get preTransformedScreenRegionRotation(): number;
|
@@ -77,6 +77,17 @@ export default class Selection {
|
|
77
77
|
const scaleAndTranslateMat = this.transform.rightMul(rotationMatrix.inverse());
|
78
78
|
return this.originalRegion.transformedBoundingBox(scaleAndTranslateMat);
|
79
79
|
}
|
80
|
+
/**
|
81
|
+
* Computes and returns the bounding box of the selection without
|
82
|
+
* any additional padding. Computes directly from the elements that are selected.
|
83
|
+
* @internal
|
84
|
+
*/
|
85
|
+
computeTightBoundingBox() {
|
86
|
+
const bbox = this.selectedElems.reduce((accumulator, elem) => {
|
87
|
+
return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
|
88
|
+
}, null);
|
89
|
+
return bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
|
90
|
+
}
|
80
91
|
get regionRotation() {
|
81
92
|
return this.transform.transformVec3(Vec2.unitX).angle();
|
82
93
|
}
|
@@ -161,9 +172,7 @@ export default class Selection {
|
|
161
172
|
// Recompute this' region from the selected elements.
|
162
173
|
// Returns false if the selection is empty.
|
163
174
|
recomputeRegion() {
|
164
|
-
const newRegion = this.
|
165
|
-
return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
|
166
|
-
}, null);
|
175
|
+
const newRegion = this.computeTightBoundingBox();
|
167
176
|
if (!newRegion) {
|
168
177
|
this.cancelSelection();
|
169
178
|
return false;
|
@@ -47,10 +47,12 @@ export default class SelectionTool extends BaseTool {
|
|
47
47
|
snapSelectionToGrid() {
|
48
48
|
if (!this.selectionBox)
|
49
49
|
throw new Error('No selection to snap!');
|
50
|
-
|
51
|
-
const
|
50
|
+
// Snap the top left corner of what we have selected.
|
51
|
+
const topLeftOfBBox = this.selectionBox.computeTightBoundingBox().topLeft;
|
52
|
+
const snappedTopLeft = this.editor.viewport.snapToGrid(topLeftOfBBox);
|
53
|
+
const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
|
52
54
|
const oldTransform = this.selectionBox.getTransform();
|
53
|
-
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(
|
55
|
+
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDelta)));
|
54
56
|
this.selectionBox.finalizeTransform();
|
55
57
|
}
|
56
58
|
onPointerDown({ allPointers, current }) {
|
@@ -51,7 +51,7 @@ export class ResizeTransformer {
|
|
51
51
|
// Round: If this isn't done, scaling can create numbers with long decimal representations.
|
52
52
|
// long decimal representations => large file sizes.
|
53
53
|
scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
|
54
|
-
if (scale.x
|
54
|
+
if (scale.x !== 0 && scale.y !== 0) {
|
55
55
|
const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
|
56
56
|
this.selection.setTransform(Mat33.scaling2D(scale, origin));
|
57
57
|
}
|
package/package.json
CHANGED
package/src/Editor.ts
CHANGED
@@ -315,7 +315,7 @@ export class Editor {
|
|
315
315
|
|
316
316
|
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
317
317
|
// pinch-zooming.
|
318
|
-
if (!evt.ctrlKey) {
|
318
|
+
if (!evt.ctrlKey && !evt.metaKey) {
|
319
319
|
if (!this.settings.wheelEventsEnabled) {
|
320
320
|
return;
|
321
321
|
} else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
@@ -333,7 +333,7 @@ export class Editor {
|
|
333
333
|
delta = delta.times(100);
|
334
334
|
}
|
335
335
|
|
336
|
-
if (evt.ctrlKey) {
|
336
|
+
if (evt.ctrlKey || evt.metaKey) {
|
337
337
|
delta = Vec3.of(0, 0, evt.deltaY);
|
338
338
|
}
|
339
339
|
|
@@ -598,7 +598,7 @@ export class Editor {
|
|
598
598
|
} else if (this.toolController.dispatchInputEvent({
|
599
599
|
kind: InputEvtType.KeyPressEvent,
|
600
600
|
key: evt.key,
|
601
|
-
ctrlKey: evt.ctrlKey,
|
601
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
602
602
|
altKey: evt.altKey,
|
603
603
|
})) {
|
604
604
|
evt.preventDefault();
|
@@ -611,7 +611,7 @@ export class Editor {
|
|
611
611
|
if (this.toolController.dispatchInputEvent({
|
612
612
|
kind: InputEvtType.KeyUpEvent,
|
613
613
|
key: evt.key,
|
614
|
-
ctrlKey: evt.ctrlKey,
|
614
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
615
615
|
altKey: evt.altKey,
|
616
616
|
})) {
|
617
617
|
evt.preventDefault();
|
package/src/SVGLoader.ts
CHANGED
@@ -244,7 +244,14 @@ export default class SVGLoader implements ImageLoader {
|
|
244
244
|
|
245
245
|
// Compute styles.
|
246
246
|
const computedStyles = window.getComputedStyle(elem);
|
247
|
-
const
|
247
|
+
const fontSizeExp = /^([-0-9.e]+)px/i;
|
248
|
+
|
249
|
+
// In some environments, computedStyles.fontSize can be increased by the system.
|
250
|
+
// Thus, to prevent text from growing on load/save, prefer .style.fontSize.
|
251
|
+
let fontSizeMatch = fontSizeExp.exec(elem.style.fontSize);
|
252
|
+
if (!fontSizeMatch) {
|
253
|
+
fontSizeMatch = fontSizeExp.exec(computedStyles.fontSize);
|
254
|
+
}
|
248
255
|
|
249
256
|
const supportedStyleAttrs = [
|
250
257
|
'fontFamily',
|
@@ -455,7 +462,7 @@ export default class SVGLoader implements ImageLoader {
|
|
455
462
|
<meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
|
456
463
|
<meta charset='utf-8'/>
|
457
464
|
</head>
|
458
|
-
<body>
|
465
|
+
<body style='font-size: 12px;'>
|
459
466
|
<script>
|
460
467
|
console.error('JavaScript should not be able to run here!');
|
461
468
|
throw new Error(
|
package/src/Viewport.ts
CHANGED
@@ -157,14 +157,18 @@ export class Viewport {
|
|
157
157
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
158
158
|
*/
|
159
159
|
public getScaleFactorToNearestPowerOfTen() {
|
160
|
+
return this.getScaleFactorToNearestPowerOf(10);
|
161
|
+
}
|
162
|
+
|
163
|
+
private getScaleFactorToNearestPowerOf(powerOf: number) {
|
160
164
|
const scaleFactor = this.getScaleFactor();
|
161
|
-
return Math.pow(
|
165
|
+
return Math.pow(powerOf, Math.round(Math.log(scaleFactor) / Math.log(powerOf)));
|
162
166
|
}
|
163
167
|
|
164
168
|
public snapToGrid(canvasPos: Point2) {
|
165
169
|
const snapCoordinate = (coordinate: number) => {
|
166
|
-
const scaleFactor = this.
|
167
|
-
const roundFactor = scaleFactor /
|
170
|
+
const scaleFactor = this.getScaleFactorToNearestPowerOf(2);
|
171
|
+
const roundFactor = scaleFactor / 50;
|
168
172
|
const snapped = Math.round(coordinate * roundFactor) / roundFactor;
|
169
173
|
|
170
174
|
return snapped;
|
@@ -64,4 +64,14 @@ describe('Path.toString', () => {
|
|
64
64
|
|
65
65
|
expect(path.toString(true)).toBe(path1.toString(true));
|
66
66
|
});
|
67
|
+
|
68
|
+
it('should remove no-op move-tos', () => {
|
69
|
+
const path1 = Path.fromString('M50,75m0,0q0,12.5 0,50q0,6.3 25,0');
|
70
|
+
path1['cachedStringVersion'] = null;
|
71
|
+
const path2 = Path.fromString('M150,175M150,175q0,12.5 0,50q0,6.3 25,0');
|
72
|
+
path2['cachedStringVersion'] = null;
|
73
|
+
|
74
|
+
expect(path1.toString()).toBe('M50,75q0,12.5 0,50q0,6.3 25,0');
|
75
|
+
expect(path2.toString()).toBe('M150,175q0,12.5 0,50q0,6.3 25,0');
|
76
|
+
});
|
67
77
|
});
|
package/src/math/Path.ts
CHANGED
@@ -493,7 +493,7 @@ export default class Path {
|
|
493
493
|
}
|
494
494
|
|
495
495
|
// Don't add no-ops.
|
496
|
-
if (commandString === 'l0,0') {
|
496
|
+
if (commandString === 'l0,0' || commandString === 'm0,0') {
|
497
497
|
return;
|
498
498
|
}
|
499
499
|
result.push(commandString);
|
@@ -503,9 +503,17 @@ export default class Path {
|
|
503
503
|
}
|
504
504
|
};
|
505
505
|
|
506
|
-
|
506
|
+
// Don't add two moveTos in a row (this can happen if
|
507
|
+
// the start point corresponds to a moveTo _and_ the first command is
|
508
|
+
// also a moveTo)
|
509
|
+
if (parts[0]?.kind !== PathCommandType.MoveTo) {
|
510
|
+
addCommand('M', startPoint);
|
511
|
+
}
|
512
|
+
|
507
513
|
let exhaustivenessCheck: never;
|
508
|
-
for (
|
514
|
+
for (let i = 0; i < parts.length; i++) {
|
515
|
+
const part = parts[i];
|
516
|
+
|
509
517
|
switch (part.kind) {
|
510
518
|
case PathCommandType.MoveTo:
|
511
519
|
addCommand('M', part.point);
|
@@ -8,10 +8,7 @@ import Pen from '../tools/Pen';
|
|
8
8
|
import { StrokeDataPoint } from '../types';
|
9
9
|
import Viewport from '../Viewport';
|
10
10
|
|
11
|
-
|
12
|
-
// Many of the icons were created with Inkscape.
|
13
|
-
|
14
|
-
type IconType = SVGSVGElement|HTMLImageElement;
|
11
|
+
export type IconType = HTMLImageElement|SVGElement;
|
15
12
|
|
16
13
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
17
14
|
const iconColorFill = `
|
@@ -35,8 +32,33 @@ const checkerboardPatternDef = `
|
|
35
32
|
`;
|
36
33
|
const checkerboardPatternRef = 'url(#checkerboard)';
|
37
34
|
|
38
|
-
|
39
|
-
|
35
|
+
/**
|
36
|
+
* Provides icons that can be used in the toolbar, etc.
|
37
|
+
* Extend this class and override methods to customize icons.
|
38
|
+
*
|
39
|
+
* @example
|
40
|
+
* ```ts
|
41
|
+
* class CustomIconProvider extends jsdraw.IconProvider {
|
42
|
+
* // Use '☺' instead of the default dropdown symbol.
|
43
|
+
* public makeDropdownIcon() {
|
44
|
+
* const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
45
|
+
* icon.innerHTML = `
|
46
|
+
* <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
|
47
|
+
* `;
|
48
|
+
* icon.setAttribute('viewBox', '0 0 100 100');
|
49
|
+
* return icon;
|
50
|
+
* }
|
51
|
+
* }
|
52
|
+
*
|
53
|
+
* const icons = new CustomIconProvider();
|
54
|
+
* const editor = new jsdraw.Editor(document.body, {
|
55
|
+
* iconProvider: icons,
|
56
|
+
* });
|
57
|
+
*
|
58
|
+
* // Add a toolbar that uses these icons
|
59
|
+
* editor.addToolbar();
|
60
|
+
* ```
|
61
|
+
*/
|
40
62
|
export default class IconProvider {
|
41
63
|
|
42
64
|
public makeUndoIcon(): IconType {
|
@@ -19,7 +19,7 @@ export const makeColorInput = (
|
|
19
19
|
colorInputContainer.classList.add('color-input-container');
|
20
20
|
|
21
21
|
colorInputContainer.appendChild(colorInput);
|
22
|
-
addPipetteTool(editor, colorInputContainer, (color: Color4) => {
|
22
|
+
const pipetteController = addPipetteTool(editor, colorInputContainer, (color: Color4) => {
|
23
23
|
colorInput.value = color.toHexString();
|
24
24
|
onInputEnd();
|
25
25
|
|
@@ -58,6 +58,7 @@ export const makeColorInput = (
|
|
58
58
|
kind: EditorEventType.ColorPickerToggled,
|
59
59
|
open: true,
|
60
60
|
});
|
61
|
+
pipetteController.cancel();
|
61
62
|
});
|
62
63
|
colorInput.addEventListener('close', () => {
|
63
64
|
editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
|
@@ -132,6 +133,13 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
|
|
132
133
|
};
|
133
134
|
|
134
135
|
container.appendChild(pipetteButton);
|
136
|
+
|
137
|
+
return {
|
138
|
+
// Cancel a pipette color selection if one is in progress.
|
139
|
+
cancel: () => {
|
140
|
+
endColorSelectMode();
|
141
|
+
},
|
142
|
+
};
|
135
143
|
};
|
136
144
|
|
137
145
|
export default makeColorInput;
|
package/src/toolbar/toolbar.css
CHANGED
@@ -33,6 +33,7 @@
|
|
33
33
|
|
34
34
|
.toolbar-dropdown .toolbar-button > .toolbar-icon {
|
35
35
|
max-width: 50px;
|
36
|
+
width: 100%;
|
36
37
|
}
|
37
38
|
|
38
39
|
.toolbar-button.disabled {
|
@@ -89,6 +90,8 @@
|
|
89
90
|
|
90
91
|
.toolbar-root .toolbar-icon {
|
91
92
|
flex-shrink: 1;
|
93
|
+
|
94
|
+
width: 100%;
|
92
95
|
min-width: 30px;
|
93
96
|
min-height: 30px;
|
94
97
|
}
|