js-draw 0.22.0 → 0.23.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 +14 -0
- package/dist/bundle.js +3 -3
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/src/Pointer.d.ts +3 -1
- package/dist/cjs/src/Pointer.js +27 -2
- package/dist/cjs/src/commands/Duplicate.d.ts +24 -0
- package/dist/cjs/src/commands/Duplicate.js +26 -0
- package/dist/cjs/src/commands/Erase.d.ts +20 -0
- package/dist/cjs/src/commands/Erase.js +20 -0
- package/dist/cjs/src/commands/invertCommand.js +6 -0
- package/dist/cjs/src/commands/uniteCommands.js +14 -13
- package/dist/cjs/src/components/BackgroundComponent.js +9 -2
- package/dist/cjs/src/components/ImageComponent.d.ts +6 -6
- package/dist/cjs/src/components/ImageComponent.js +17 -12
- package/dist/cjs/src/components/lib.d.ts +1 -1
- package/dist/cjs/src/components/lib.js +2 -2
- package/dist/cjs/src/components/localization.d.ts +1 -0
- package/dist/cjs/src/components/localization.js +1 -0
- package/dist/cjs/src/math/Vec2.d.ts +20 -0
- package/dist/cjs/src/math/Vec2.js +20 -0
- package/dist/cjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
- package/dist/cjs/src/rendering/renderers/AbstractRenderer.js +21 -3
- package/dist/cjs/src/rendering/renderers/CanvasRenderer.js +12 -7
- package/dist/cjs/src/toolbar/localization.d.ts +1 -0
- package/dist/cjs/src/toolbar/localization.js +1 -0
- package/dist/cjs/src/toolbar/makeColorInput.js +8 -0
- package/dist/cjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
- package/dist/cjs/src/toolbar/widgets/BaseWidget.js +29 -6
- package/dist/cjs/src/tools/Pen.d.ts +4 -0
- package/dist/cjs/src/tools/Pen.js +24 -1
- package/dist/cjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
- package/dist/cjs/src/tools/SelectionTool/SelectionTool.js +8 -0
- package/dist/cjs/src/util/waitForAll.d.ts +6 -0
- package/dist/cjs/src/util/waitForAll.js +17 -0
- package/dist/mjs/src/Pointer.d.ts +3 -1
- package/dist/mjs/src/Pointer.mjs +27 -2
- package/dist/mjs/src/commands/Duplicate.d.ts +24 -0
- package/dist/mjs/src/commands/Duplicate.mjs +26 -0
- package/dist/mjs/src/commands/Erase.d.ts +20 -0
- package/dist/mjs/src/commands/Erase.mjs +20 -0
- package/dist/mjs/src/commands/invertCommand.mjs +6 -0
- package/dist/mjs/src/commands/uniteCommands.mjs +14 -13
- package/dist/mjs/src/components/BackgroundComponent.mjs +9 -2
- package/dist/mjs/src/components/ImageComponent.d.ts +6 -6
- package/dist/mjs/src/components/ImageComponent.mjs +17 -12
- package/dist/mjs/src/components/lib.d.ts +1 -1
- package/dist/mjs/src/components/lib.mjs +3 -1
- package/dist/mjs/src/components/localization.d.ts +1 -0
- package/dist/mjs/src/components/localization.mjs +1 -0
- package/dist/mjs/src/math/Vec2.d.ts +20 -0
- package/dist/mjs/src/math/Vec2.mjs +20 -0
- package/dist/mjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
- package/dist/mjs/src/rendering/renderers/AbstractRenderer.mjs +21 -3
- package/dist/mjs/src/rendering/renderers/CanvasRenderer.mjs +12 -7
- package/dist/mjs/src/toolbar/localization.d.ts +1 -0
- package/dist/mjs/src/toolbar/localization.mjs +1 -0
- package/dist/mjs/src/toolbar/makeColorInput.mjs +8 -0
- package/dist/mjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
- package/dist/mjs/src/toolbar/widgets/BaseWidget.mjs +29 -6
- package/dist/mjs/src/tools/Pen.d.ts +4 -0
- package/dist/mjs/src/tools/Pen.mjs +24 -1
- package/dist/mjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
- package/dist/mjs/src/tools/SelectionTool/SelectionTool.mjs +8 -0
- package/dist/mjs/src/util/waitForAll.d.ts +6 -0
- package/dist/mjs/src/util/waitForAll.mjs +15 -0
- package/package.json +12 -12
- package/src/toolbar/toolbar.css +35 -1
@@ -1,3 +1,4 @@
|
|
1
|
+
import waitForAll from '../util/waitForAll.mjs';
|
1
2
|
import Command from './Command.mjs';
|
2
3
|
import SerializableCommand from './SerializableCommand.mjs';
|
3
4
|
class NonSerializableUnion extends Command {
|
@@ -6,21 +7,10 @@ class NonSerializableUnion extends Command {
|
|
6
7
|
this.commands = commands;
|
7
8
|
this.applyChunkSize = applyChunkSize;
|
8
9
|
}
|
9
|
-
static waitForAll(commands) {
|
10
|
-
// If any are Promises...
|
11
|
-
if (commands.some(command => command && command['then'])) {
|
12
|
-
console.log('waiting...');
|
13
|
-
// Wait for all commands to finish.
|
14
|
-
return Promise.all(commands)
|
15
|
-
// Ensure we return a Promise<void> and not a Promise<void[]>
|
16
|
-
.then(() => { });
|
17
|
-
}
|
18
|
-
return;
|
19
|
-
}
|
20
10
|
apply(editor) {
|
21
11
|
if (this.applyChunkSize === undefined) {
|
22
12
|
const results = this.commands.map(cmd => cmd.apply(editor));
|
23
|
-
return
|
13
|
+
return waitForAll(results);
|
24
14
|
}
|
25
15
|
else {
|
26
16
|
return editor.asyncApplyCommands(this.commands, this.applyChunkSize);
|
@@ -31,12 +21,15 @@ class NonSerializableUnion extends Command {
|
|
31
21
|
commands.reverse();
|
32
22
|
if (this.applyChunkSize === undefined) {
|
33
23
|
const results = commands.map(cmd => cmd.unapply(editor));
|
34
|
-
return
|
24
|
+
return waitForAll(results);
|
35
25
|
}
|
36
26
|
else {
|
37
27
|
return editor.asyncUnapplyCommands(commands, this.applyChunkSize, false);
|
38
28
|
}
|
39
29
|
}
|
30
|
+
onDrop(editor) {
|
31
|
+
this.commands.forEach(command => command.onDrop(editor));
|
32
|
+
}
|
40
33
|
description(editor, localizationTable) {
|
41
34
|
const descriptions = [];
|
42
35
|
let lastDescription = null;
|
@@ -68,17 +61,25 @@ class SerializableUnion extends SerializableCommand {
|
|
68
61
|
this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize);
|
69
62
|
}
|
70
63
|
serializeToJSON() {
|
64
|
+
if (this.serializedData) {
|
65
|
+
return this.serializedData;
|
66
|
+
}
|
71
67
|
return {
|
72
68
|
applyChunkSize: this.applyChunkSize,
|
73
69
|
data: this.commands.map(command => command.serialize()),
|
74
70
|
};
|
75
71
|
}
|
76
72
|
apply(editor) {
|
73
|
+
// Cache this' serialized form -- applying this may change how commands serialize.
|
74
|
+
this.serializedData = this.serializeToJSON();
|
77
75
|
return this.nonserializableCommand.apply(editor);
|
78
76
|
}
|
79
77
|
unapply(editor) {
|
80
78
|
return this.nonserializableCommand.unapply(editor);
|
81
79
|
}
|
80
|
+
onDrop(editor) {
|
81
|
+
this.nonserializableCommand.onDrop(editor);
|
82
|
+
}
|
82
83
|
description(editor, localizationTable) {
|
83
84
|
return this.nonserializableCommand.description(editor, localizationTable);
|
84
85
|
}
|
@@ -167,7 +167,7 @@ export default class BackgroundComponent extends AbstractComponent {
|
|
167
167
|
if (this.backgroundType === BackgroundType.None) {
|
168
168
|
return;
|
169
169
|
}
|
170
|
-
const clip =
|
170
|
+
const clip = this.backgroundType === BackgroundType.Grid;
|
171
171
|
canvas.startObject(this.contentBBox, clip);
|
172
172
|
if (this.backgroundType === BackgroundType.SolidColor || this.backgroundType === BackgroundType.Grid) {
|
173
173
|
// If the rectangle for this region contains the visible rect,
|
@@ -235,9 +235,16 @@ export default class BackgroundComponent extends AbstractComponent {
|
|
235
235
|
if (this.backgroundType === BackgroundType.SolidColor) {
|
236
236
|
return localizationTable.filledBackgroundWithColor(this.mainColor.toString());
|
237
237
|
}
|
238
|
-
else {
|
238
|
+
else if (this.backgroundType === BackgroundType.None) {
|
239
239
|
return localizationTable.emptyBackground;
|
240
240
|
}
|
241
|
+
else if (this.backgroundType === BackgroundType.Grid) {
|
242
|
+
return localizationTable.gridBackground;
|
243
|
+
}
|
244
|
+
else {
|
245
|
+
const exhaustivenessCheck = this.backgroundType;
|
246
|
+
return exhaustivenessCheck;
|
247
|
+
}
|
241
248
|
}
|
242
249
|
createClone() {
|
243
250
|
return new BackgroundComponent(this.backgroundType, this.mainColor);
|
@@ -14,6 +14,12 @@ export default class ImageComponent extends AbstractComponent {
|
|
14
14
|
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
15
15
|
getProportionalRenderingTime(): number;
|
16
16
|
intersects(lineSegment: LineSegment2): boolean;
|
17
|
+
protected applyTransformation(affineTransfm: Mat33): void;
|
18
|
+
description(localizationTable: ImageComponentLocalization): string;
|
19
|
+
getAltText(): string | undefined;
|
20
|
+
getURL(): string;
|
21
|
+
getTransformation(): Mat33;
|
22
|
+
protected createClone(): AbstractComponent;
|
17
23
|
protected serializeToJSON(): {
|
18
24
|
src: string;
|
19
25
|
label: string | undefined;
|
@@ -21,11 +27,5 @@ export default class ImageComponent extends AbstractComponent {
|
|
21
27
|
height: number;
|
22
28
|
transform: Mat33Array;
|
23
29
|
};
|
24
|
-
protected applyTransformation(affineTransfm: Mat33): void;
|
25
|
-
description(localizationTable: ImageComponentLocalization): string;
|
26
|
-
getAltText(): string | undefined;
|
27
|
-
getURL(): string;
|
28
|
-
getTransformation(): Mat33;
|
29
|
-
protected createClone(): AbstractComponent;
|
30
30
|
static deserializeFromJSON(data: any): ImageComponent;
|
31
31
|
}
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
9
9
|
};
|
10
10
|
import Mat33 from '../math/Mat33.mjs';
|
11
11
|
import Rect2 from '../math/Rect2.mjs';
|
12
|
+
import { assertIsNumber, assertIsNumberArray } from '../util/assertions.mjs';
|
12
13
|
import AbstractComponent from './AbstractComponent.mjs';
|
13
14
|
// Represents a raster image.
|
14
15
|
export default class ImageComponent extends AbstractComponent {
|
@@ -98,16 +99,6 @@ export default class ImageComponent extends AbstractComponent {
|
|
98
99
|
}
|
99
100
|
return false;
|
100
101
|
}
|
101
|
-
serializeToJSON() {
|
102
|
-
return {
|
103
|
-
src: this.image.base64Url,
|
104
|
-
label: this.image.label,
|
105
|
-
// Store the width and height for bounding box computations while the image is loading.
|
106
|
-
width: this.image.image.width,
|
107
|
-
height: this.image.image.height,
|
108
|
-
transform: this.image.transform.toArray(),
|
109
|
-
};
|
110
|
-
}
|
111
102
|
applyTransformation(affineTransfm) {
|
112
103
|
this.image.transform = affineTransfm.rightMul(this.image.transform);
|
113
104
|
this.recomputeBBox();
|
@@ -127,19 +118,33 @@ export default class ImageComponent extends AbstractComponent {
|
|
127
118
|
createClone() {
|
128
119
|
return new ImageComponent(Object.assign({}, this.image));
|
129
120
|
}
|
121
|
+
serializeToJSON() {
|
122
|
+
return {
|
123
|
+
src: this.image.base64Url,
|
124
|
+
label: this.image.label,
|
125
|
+
// Store the width and height for bounding box computations while the image is loading.
|
126
|
+
width: this.image.image.width,
|
127
|
+
height: this.image.image.height,
|
128
|
+
transform: this.image.transform.toArray(),
|
129
|
+
};
|
130
|
+
}
|
130
131
|
static deserializeFromJSON(data) {
|
131
132
|
if (!(typeof data.src === 'string')) {
|
132
133
|
throw new Error(`${data} has invalid format! Expected src property.`);
|
133
134
|
}
|
135
|
+
assertIsNumberArray(data.transform);
|
136
|
+
assertIsNumber(data.width);
|
137
|
+
assertIsNumber(data.height);
|
134
138
|
const image = new Image();
|
135
139
|
image.src = data.src;
|
136
140
|
image.width = data.width;
|
137
141
|
image.height = data.height;
|
142
|
+
const transform = new Mat33(...data.transform);
|
138
143
|
return new ImageComponent({
|
139
144
|
image: image,
|
140
|
-
base64Url:
|
145
|
+
base64Url: data.src,
|
141
146
|
label: data.label,
|
142
|
-
transform
|
147
|
+
transform,
|
143
148
|
});
|
144
149
|
}
|
145
150
|
}
|
@@ -10,4 +10,4 @@ import ImageComponent from './ImageComponent';
|
|
10
10
|
import RestyleableComponent from './RestylableComponent';
|
11
11
|
import { createRestyleComponentCommand, isRestylableComponent, ComponentStyle as RestyleableComponentStyle } from './RestylableComponent';
|
12
12
|
import BackgroundComponent from './BackgroundComponent';
|
13
|
-
export { Stroke,
|
13
|
+
export { Stroke, RestyleableComponent, createRestyleComponentCommand, isRestylableComponent, RestyleableComponentStyle, TextComponent, TextComponent as Text, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
|
@@ -9,4 +9,6 @@ import TextComponent from './TextComponent.mjs';
|
|
9
9
|
import ImageComponent from './ImageComponent.mjs';
|
10
10
|
import { createRestyleComponentCommand, isRestylableComponent } from './RestylableComponent.mjs';
|
11
11
|
import BackgroundComponent from './BackgroundComponent.mjs';
|
12
|
-
export { Stroke,
|
12
|
+
export { Stroke, createRestyleComponentCommand, isRestylableComponent, TextComponent,
|
13
|
+
// @deprecated
|
14
|
+
TextComponent as Text, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
|
@@ -3,6 +3,7 @@ export const defaultComponentLocalization = {
|
|
3
3
|
stroke: 'Stroke',
|
4
4
|
svgObject: 'SVG Object',
|
5
5
|
emptyBackground: 'Empty background',
|
6
|
+
gridBackground: 'Grid background',
|
6
7
|
filledBackgroundWithColor: (color) => `Filled background (${color})`,
|
7
8
|
text: (text) => `Text object: ${text}`,
|
8
9
|
imageNode: (description) => `Image: ${description}`,
|
@@ -1,12 +1,32 @@
|
|
1
1
|
import Vec3 from './Vec3';
|
2
2
|
export declare namespace Vec2 {
|
3
|
+
/**
|
4
|
+
* Creates a `Vec2` from an x and y coordinate.
|
5
|
+
*
|
6
|
+
* For example,
|
7
|
+
* ```ts
|
8
|
+
* const v = Vec2.of(3, 4); // x=3, y=4.
|
9
|
+
* ```
|
10
|
+
*/
|
3
11
|
const of: (x: number, y: number) => Vec2;
|
12
|
+
/**
|
13
|
+
* Creates a `Vec2` from an object containing x and y coordinates.
|
14
|
+
*
|
15
|
+
* For example,
|
16
|
+
* ```ts
|
17
|
+
* const v1 = Vec2.ofXY({ x: 3, y: 4.5 });
|
18
|
+
* const v2 = Vec2.ofXY({ x: -123.4, y: 1 });
|
19
|
+
* ```
|
20
|
+
*/
|
4
21
|
const ofXY: ({ x, y }: {
|
5
22
|
x: number;
|
6
23
|
y: number;
|
7
24
|
}) => Vec2;
|
25
|
+
/** A vector of length 1 in the X direction (→). */
|
8
26
|
const unitX: Vec3;
|
27
|
+
/** A vector of length 1 in the Y direction (↑). */
|
9
28
|
const unitY: Vec3;
|
29
|
+
/** The zero vector: A vector with x=0, y=0. */
|
10
30
|
const zero: Vec3;
|
11
31
|
}
|
12
32
|
export type Point2 = Vec3;
|
@@ -1,13 +1,33 @@
|
|
1
1
|
import Vec3 from './Vec3.mjs';
|
2
2
|
export var Vec2;
|
3
3
|
(function (Vec2) {
|
4
|
+
/**
|
5
|
+
* Creates a `Vec2` from an x and y coordinate.
|
6
|
+
*
|
7
|
+
* For example,
|
8
|
+
* ```ts
|
9
|
+
* const v = Vec2.of(3, 4); // x=3, y=4.
|
10
|
+
* ```
|
11
|
+
*/
|
4
12
|
Vec2.of = (x, y) => {
|
5
13
|
return Vec3.of(x, y, 0);
|
6
14
|
};
|
15
|
+
/**
|
16
|
+
* Creates a `Vec2` from an object containing x and y coordinates.
|
17
|
+
*
|
18
|
+
* For example,
|
19
|
+
* ```ts
|
20
|
+
* const v1 = Vec2.ofXY({ x: 3, y: 4.5 });
|
21
|
+
* const v2 = Vec2.ofXY({ x: -123.4, y: 1 });
|
22
|
+
* ```
|
23
|
+
*/
|
7
24
|
Vec2.ofXY = ({ x, y }) => {
|
8
25
|
return Vec3.of(x, y, 0);
|
9
26
|
};
|
27
|
+
/** A vector of length 1 in the X direction (→). */
|
10
28
|
Vec2.unitX = Vec2.of(1, 0);
|
29
|
+
/** A vector of length 1 in the Y direction (↑). */
|
11
30
|
Vec2.unitY = Vec2.of(0, 1);
|
31
|
+
/** The zero vector: A vector with x=0, y=0. */
|
12
32
|
Vec2.zero = Vec2.of(0, 0);
|
13
33
|
})(Vec2 || (Vec2 = {}));
|
@@ -19,6 +19,11 @@ export interface RenderableImage {
|
|
19
19
|
base64Url: string;
|
20
20
|
label?: string;
|
21
21
|
}
|
22
|
+
/**
|
23
|
+
* Abstract base class for renderers.
|
24
|
+
*
|
25
|
+
* @see {@link EditorImage.render}
|
26
|
+
*/
|
22
27
|
export default abstract class AbstractRenderer {
|
23
28
|
private viewport;
|
24
29
|
private selfTransform;
|
@@ -40,9 +45,21 @@ export default abstract class AbstractRenderer {
|
|
40
45
|
protected objectLevel: number;
|
41
46
|
private currentPaths;
|
42
47
|
private flushPath;
|
48
|
+
/**
|
49
|
+
* Draws a styled path. If within an object started by {@link startObject},
|
50
|
+
* the resultant path may not be visible until {@link endObject} is called.
|
51
|
+
*/
|
43
52
|
drawPath(path: RenderablePathSpec): void;
|
44
53
|
drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
|
54
|
+
/** Draws a filled rectangle. */
|
45
55
|
fillRect(rect: Rect2, fill: Color4): void;
|
56
|
+
/**
|
57
|
+
* This should be called whenever a new object is being drawn.
|
58
|
+
*
|
59
|
+
* @param _boundingBox The bounding box of the object to be drawn.
|
60
|
+
* @param _clip Whether content outside `_boundingBox` should be drawn. Renderers
|
61
|
+
* that override this method are not required to support `_clip`.
|
62
|
+
*/
|
46
63
|
startObject(_boundingBox: Rect2, _clip?: boolean): void;
|
47
64
|
/**
|
48
65
|
* Notes the end of an object.
|
@@ -1,6 +1,11 @@
|
|
1
1
|
import Path, { PathCommandType } from '../../math/Path.mjs';
|
2
2
|
import { Vec2 } from '../../math/Vec2.mjs';
|
3
3
|
import { stylesEqual } from '../RenderingStyle.mjs';
|
4
|
+
/**
|
5
|
+
* Abstract base class for renderers.
|
6
|
+
*
|
7
|
+
* @see {@link EditorImage.render}
|
8
|
+
*/
|
4
9
|
export default class AbstractRenderer {
|
5
10
|
constructor(viewport) {
|
6
11
|
this.viewport = viewport;
|
@@ -49,7 +54,12 @@ export default class AbstractRenderer {
|
|
49
54
|
if (lastStyle) {
|
50
55
|
this.endPath(lastStyle);
|
51
56
|
}
|
57
|
+
this.currentPaths = [];
|
52
58
|
}
|
59
|
+
/**
|
60
|
+
* Draws a styled path. If within an object started by {@link startObject},
|
61
|
+
* the resultant path may not be visible until {@link endObject} is called.
|
62
|
+
*/
|
53
63
|
drawPath(path) {
|
54
64
|
// If we're being called outside of an object,
|
55
65
|
// we can't delay rendering
|
@@ -70,14 +80,22 @@ export default class AbstractRenderer {
|
|
70
80
|
const path = Path.fromRect(rect, lineWidth);
|
71
81
|
this.drawPath(path.toRenderable(lineFill));
|
72
82
|
}
|
73
|
-
|
83
|
+
/** Draws a filled rectangle. */
|
74
84
|
fillRect(rect, fill) {
|
75
85
|
const path = Path.fromRect(rect);
|
76
86
|
this.drawPath(path.toRenderable({ fill }));
|
77
87
|
}
|
78
|
-
|
79
|
-
|
88
|
+
/**
|
89
|
+
* This should be called whenever a new object is being drawn.
|
90
|
+
*
|
91
|
+
* @param _boundingBox The bounding box of the object to be drawn.
|
92
|
+
* @param _clip Whether content outside `_boundingBox` should be drawn. Renderers
|
93
|
+
* that override this method are not required to support `_clip`.
|
94
|
+
*/
|
80
95
|
startObject(_boundingBox, _clip) {
|
96
|
+
if (this.objectLevel > 0) {
|
97
|
+
this.flushPath();
|
98
|
+
}
|
81
99
|
this.currentPaths = [];
|
82
100
|
this.objectLevel++;
|
83
101
|
}
|
@@ -175,14 +175,19 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
175
175
|
super.startObject(boundingBox);
|
176
176
|
this.currentObjectBBox = boundingBox;
|
177
177
|
if (!this.ignoringObject && clip) {
|
178
|
-
|
179
|
-
|
180
|
-
this.
|
181
|
-
|
182
|
-
|
183
|
-
this.ctx.
|
178
|
+
// Don't clip if it would only remove content already trimmed by
|
179
|
+
// the edge of the screen.
|
180
|
+
const clippedIsOutsideScreen = boundingBox.containsRect(this.getViewport().visibleRect);
|
181
|
+
if (!clippedIsOutsideScreen) {
|
182
|
+
this.clipLevels.push(this.objectLevel);
|
183
|
+
this.ctx.save();
|
184
|
+
this.ctx.beginPath();
|
185
|
+
for (const corner of boundingBox.corners) {
|
186
|
+
const screenCorner = this.canvasToScreen(corner);
|
187
|
+
this.ctx.lineTo(screenCorner.x, screenCorner.y);
|
188
|
+
}
|
189
|
+
this.ctx.clip();
|
184
190
|
}
|
185
|
-
this.ctx.clip();
|
186
191
|
}
|
187
192
|
}
|
188
193
|
endObject() {
|
@@ -23,6 +23,7 @@ export const defaultToolbarLocalization = {
|
|
23
23
|
selectPenType: 'Pen type: ',
|
24
24
|
pickColorFromScreen: 'Pick color from screen',
|
25
25
|
clickToPickColorAnnouncement: 'Click on the screen to pick a color',
|
26
|
+
colorSelectionCanceledAnnouncement: 'Color selection canceled',
|
26
27
|
selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
|
27
28
|
documentProperties: 'Page',
|
28
29
|
backgroundColor: 'Background Color: ',
|
@@ -42,6 +42,11 @@ export const makeColorInput = (editor, onColorChange) => {
|
|
42
42
|
open: true,
|
43
43
|
});
|
44
44
|
pipetteController.cancel();
|
45
|
+
// Focus the Coloris color picker, if it exists.
|
46
|
+
// Don't focus the text input within the color picker, however,
|
47
|
+
// as this displays a keyboard on mobile devices.
|
48
|
+
const colorPickerElem = document.querySelector('#clr-picker #clr-hue-slider');
|
49
|
+
colorPickerElem === null || colorPickerElem === void 0 ? void 0 : colorPickerElem.focus();
|
45
50
|
});
|
46
51
|
colorInput.addEventListener('close', () => {
|
47
52
|
editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
|
@@ -49,6 +54,8 @@ export const makeColorInput = (editor, onColorChange) => {
|
|
49
54
|
open: false,
|
50
55
|
});
|
51
56
|
onInputEnd();
|
57
|
+
// Restore focus to the input that opened the color picker
|
58
|
+
colorInput.focus();
|
52
59
|
});
|
53
60
|
const setColorInputValue = (color) => {
|
54
61
|
if (typeof color === 'object') {
|
@@ -94,6 +101,7 @@ const addPipetteTool = (editor, container, onColorChange) => {
|
|
94
101
|
// If already picking, cancel it.
|
95
102
|
if (pipetteButton.classList.contains('active')) {
|
96
103
|
endColorSelectMode();
|
104
|
+
editor.announceForAccessibility(editor.localization.colorSelectionCanceledAnnouncement);
|
97
105
|
return;
|
98
106
|
}
|
99
107
|
pipetteTool === null || pipetteTool === void 0 ? void 0 : pipetteTool.setColorListener(pipetteColorPreview, pipetteColorSelect);
|
@@ -46,6 +46,7 @@ export default abstract class BaseWidget {
|
|
46
46
|
protected updateIcon(): void;
|
47
47
|
setDisabled(disabled: boolean): void;
|
48
48
|
setSelected(selected: boolean): void;
|
49
|
+
private hideDropdownTimeout;
|
49
50
|
protected setDropdownVisible(visible: boolean): void;
|
50
51
|
canBeInOverflowMenu(): boolean;
|
51
52
|
getButtonWidth(): number;
|
@@ -23,6 +23,7 @@ export default class BaseWidget {
|
|
23
23
|
this.subWidgets = {};
|
24
24
|
this.toplevel = true;
|
25
25
|
this.toolbarWidgetToggleListener = null;
|
26
|
+
this.hideDropdownTimeout = null;
|
26
27
|
this.localizationTable = localizationTable !== null && localizationTable !== void 0 ? localizationTable : editor.localization;
|
27
28
|
this.icon = null;
|
28
29
|
this.container = document.createElement('div');
|
@@ -211,13 +212,18 @@ export default class BaseWidget {
|
|
211
212
|
if (currentlySelected === selected) {
|
212
213
|
return;
|
213
214
|
}
|
215
|
+
// Ensure that accessibility tools check and read the value of
|
216
|
+
// aria-checked.
|
217
|
+
// TODO: Ensure that 'role' is set to 'switch' by default for selectable
|
218
|
+
// buttons.
|
219
|
+
this.button.setAttribute('role', 'switch');
|
214
220
|
if (selected) {
|
215
221
|
this.container.classList.add('selected');
|
216
|
-
this.button.
|
222
|
+
this.button.setAttribute('aria-checked', 'true');
|
217
223
|
}
|
218
224
|
else {
|
219
225
|
this.container.classList.remove('selected');
|
220
|
-
this.button.
|
226
|
+
this.button.setAttribute('aria-checked', 'false');
|
221
227
|
}
|
222
228
|
}
|
223
229
|
setDropdownVisible(visible) {
|
@@ -225,6 +231,13 @@ export default class BaseWidget {
|
|
225
231
|
if (currentlyVisible === visible) {
|
226
232
|
return;
|
227
233
|
}
|
234
|
+
// If waiting to hide the dropdown, cancel it.
|
235
|
+
if (this.hideDropdownTimeout) {
|
236
|
+
clearTimeout(this.hideDropdownTimeout);
|
237
|
+
this.hideDropdownTimeout = null;
|
238
|
+
this.dropdownContainer.classList.remove('hiding');
|
239
|
+
}
|
240
|
+
const animationDuration = 150; // ms
|
228
241
|
if (visible) {
|
229
242
|
this.dropdownContainer.classList.remove('hidden');
|
230
243
|
this.container.classList.add('dropdownVisible');
|
@@ -235,10 +248,20 @@ export default class BaseWidget {
|
|
235
248
|
});
|
236
249
|
}
|
237
250
|
else {
|
238
|
-
this.dropdownContainer.classList.add('hidden');
|
239
251
|
this.container.classList.remove('dropdownVisible');
|
240
252
|
this.editor.announceForAccessibility(this.localizationTable.dropdownHidden(this.getTitle()));
|
253
|
+
this.dropdownContainer.classList.add('hiding');
|
254
|
+
// Hide the dropdown *slightly* before the animation finishes. This
|
255
|
+
// prevents flickering in some browsers.
|
256
|
+
const hideDelay = animationDuration * 0.95;
|
257
|
+
this.hideDropdownTimeout = setTimeout(() => {
|
258
|
+
this.dropdownContainer.classList.add('hidden');
|
259
|
+
this.dropdownContainer.classList.remove('hiding');
|
260
|
+
}, hideDelay);
|
241
261
|
}
|
262
|
+
// Animate
|
263
|
+
const animationName = `var(--dropdown-${visible ? 'show' : 'hide'}-animation)`;
|
264
|
+
this.dropdownContainer.style.animation = `${animationDuration}ms ease ${animationName}`;
|
242
265
|
this.repositionDropdown();
|
243
266
|
}
|
244
267
|
canBeInOverflowMenu() {
|
@@ -257,11 +280,11 @@ export default class BaseWidget {
|
|
257
280
|
const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
|
258
281
|
const screenWidth = document.body.clientWidth;
|
259
282
|
if (dropdownBBox.left > screenWidth / 2) {
|
260
|
-
|
261
|
-
|
283
|
+
// Use .translate so as not to conflict with CSS animating the
|
284
|
+
// transform property.
|
285
|
+
this.dropdownContainer.style.translate = `calc(${this.button.clientWidth + 'px'} - 100%) 0`;
|
262
286
|
}
|
263
287
|
else {
|
264
|
-
this.dropdownContainer.style.marginLeft = '';
|
265
288
|
this.dropdownContainer.style.transform = '';
|
266
289
|
}
|
267
290
|
}
|
@@ -14,9 +14,12 @@ export default class Pen extends BaseTool {
|
|
14
14
|
private builderFactory;
|
15
15
|
protected builder: ComponentBuilder | null;
|
16
16
|
private lastPoint;
|
17
|
+
private startPoint;
|
17
18
|
private ctrlKeyPressed;
|
19
|
+
private shiftKeyPressed;
|
18
20
|
constructor(editor: Editor, description: string, style: PenStyle, builderFactory?: ComponentBuilderFactory);
|
19
21
|
private getPressureMultiplier;
|
22
|
+
private xyAxesSnap;
|
20
23
|
protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
|
21
24
|
protected previewStroke(): void;
|
22
25
|
protected addPointToStroke(point: StrokeDataPoint): void;
|
@@ -34,6 +37,7 @@ export default class Pen extends BaseTool {
|
|
34
37
|
getStrokeFactory(): ComponentBuilderFactory;
|
35
38
|
setEnabled(enabled: boolean): void;
|
36
39
|
private isSnappingToGrid;
|
40
|
+
private isAngleLocked;
|
37
41
|
onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
|
38
42
|
onKeyUp({ key }: KeyUpEvent): boolean;
|
39
43
|
}
|
@@ -11,14 +11,26 @@ export default class Pen extends BaseTool {
|
|
11
11
|
this.builderFactory = builderFactory;
|
12
12
|
this.builder = null;
|
13
13
|
this.lastPoint = null;
|
14
|
+
this.startPoint = null;
|
14
15
|
this.ctrlKeyPressed = false;
|
16
|
+
this.shiftKeyPressed = false;
|
15
17
|
}
|
16
18
|
getPressureMultiplier() {
|
17
19
|
return 1 / this.editor.viewport.getScaleFactor() * this.style.thickness;
|
18
20
|
}
|
21
|
+
// Snap the given pointer to the nearer of the x/y axes.
|
22
|
+
xyAxesSnap(pointer) {
|
23
|
+
if (!this.startPoint) {
|
24
|
+
return pointer;
|
25
|
+
}
|
26
|
+
return pointer.lockedToXYAxes(this.startPoint.pos, this.editor.viewport);
|
27
|
+
}
|
19
28
|
// Converts a `pointer` to a `StrokeDataPoint`.
|
20
29
|
toStrokePoint(pointer) {
|
21
30
|
var _a;
|
31
|
+
if (this.isAngleLocked() && this.lastPoint) {
|
32
|
+
pointer = this.xyAxesSnap(pointer);
|
33
|
+
}
|
22
34
|
if (this.isSnappingToGrid()) {
|
23
35
|
pointer = pointer.snappedToGrid(this.editor.viewport);
|
24
36
|
}
|
@@ -64,7 +76,8 @@ export default class Pen extends BaseTool {
|
|
64
76
|
}
|
65
77
|
}
|
66
78
|
if ((allPointers.length === 1 && !isEraser) || anyDeviceIsStylus) {
|
67
|
-
this.
|
79
|
+
this.startPoint = this.toStrokePoint(current);
|
80
|
+
this.builder = this.builderFactory(this.startPoint, this.editor.viewport);
|
68
81
|
return true;
|
69
82
|
}
|
70
83
|
return false;
|
@@ -104,6 +117,7 @@ export default class Pen extends BaseTool {
|
|
104
117
|
}
|
105
118
|
}
|
106
119
|
this.builder = null;
|
120
|
+
this.lastPoint = null;
|
107
121
|
this.editor.clearWetInk();
|
108
122
|
}
|
109
123
|
noteUpdated() {
|
@@ -138,6 +152,7 @@ export default class Pen extends BaseTool {
|
|
138
152
|
this.ctrlKeyPressed = false;
|
139
153
|
}
|
140
154
|
isSnappingToGrid() { return this.ctrlKeyPressed; }
|
155
|
+
isAngleLocked() { return this.shiftKeyPressed; }
|
141
156
|
onKeyPress({ key, ctrlKey }) {
|
142
157
|
key = key.toLowerCase();
|
143
158
|
let newThickness;
|
@@ -156,6 +171,10 @@ export default class Pen extends BaseTool {
|
|
156
171
|
this.ctrlKeyPressed = true;
|
157
172
|
return true;
|
158
173
|
}
|
174
|
+
if (key === 'shift') {
|
175
|
+
this.shiftKeyPressed = true;
|
176
|
+
return true;
|
177
|
+
}
|
159
178
|
// Ctrl+Z: End the stroke so that it can be undone/redone.
|
160
179
|
if (key === 'z' && ctrlKey && this.builder) {
|
161
180
|
this.finalizeStroke();
|
@@ -168,6 +187,10 @@ export default class Pen extends BaseTool {
|
|
168
187
|
this.ctrlKeyPressed = false;
|
169
188
|
return true;
|
170
189
|
}
|
190
|
+
if (key === 'shift') {
|
191
|
+
this.shiftKeyPressed = false;
|
192
|
+
return true;
|
193
|
+
}
|
171
194
|
return false;
|
172
195
|
}
|
173
196
|
}
|