js-draw 0.0.1
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/.eslintrc.js +57 -0
- package/.husky/pre-commit +4 -0
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/__mocks__/coloris.ts +8 -0
- package/__mocks__/styleMock.js +1 -0
- package/dist/__mocks__/coloris.d.ts +2 -0
- package/dist/__mocks__/coloris.js +5 -0
- package/dist/build_tools/BundledFile.d.ts +12 -0
- package/dist/build_tools/BundledFile.js +153 -0
- package/dist/scripts/bundle.d.ts +1 -0
- package/dist/scripts/bundle.js +19 -0
- package/dist/scripts/watchBundle.d.ts +1 -0
- package/dist/scripts/watchBundle.js +9 -0
- package/dist/src/Color4.d.ts +23 -0
- package/dist/src/Color4.js +102 -0
- package/dist/src/Display.d.ts +22 -0
- package/dist/src/Display.js +93 -0
- package/dist/src/Editor.d.ts +55 -0
- package/dist/src/Editor.js +366 -0
- package/dist/src/EditorImage.d.ts +44 -0
- package/dist/src/EditorImage.js +243 -0
- package/dist/src/EventDispatcher.d.ts +11 -0
- package/dist/src/EventDispatcher.js +39 -0
- package/dist/src/Pointer.d.ts +22 -0
- package/dist/src/Pointer.js +57 -0
- package/dist/src/SVGLoader.d.ts +21 -0
- package/dist/src/SVGLoader.js +204 -0
- package/dist/src/StrokeBuilder.d.ts +35 -0
- package/dist/src/StrokeBuilder.js +275 -0
- package/dist/src/UndoRedoHistory.d.ts +17 -0
- package/dist/src/UndoRedoHistory.js +46 -0
- package/dist/src/Viewport.d.ts +39 -0
- package/dist/src/Viewport.js +134 -0
- package/dist/src/commands/Command.d.ts +15 -0
- package/dist/src/commands/Command.js +29 -0
- package/dist/src/commands/Erase.d.ts +11 -0
- package/dist/src/commands/Erase.js +37 -0
- package/dist/src/commands/localization.d.ts +19 -0
- package/dist/src/commands/localization.js +17 -0
- package/dist/src/components/AbstractComponent.d.ts +19 -0
- package/dist/src/components/AbstractComponent.js +46 -0
- package/dist/src/components/Stroke.d.ts +16 -0
- package/dist/src/components/Stroke.js +79 -0
- package/dist/src/components/UnknownSVGObject.d.ts +15 -0
- package/dist/src/components/UnknownSVGObject.js +25 -0
- package/dist/src/components/localization.d.ts +5 -0
- package/dist/src/components/localization.js +4 -0
- package/dist/src/geometry/LineSegment2.d.ts +19 -0
- package/dist/src/geometry/LineSegment2.js +100 -0
- package/dist/src/geometry/Mat33.d.ts +31 -0
- package/dist/src/geometry/Mat33.js +187 -0
- package/dist/src/geometry/Path.d.ts +55 -0
- package/dist/src/geometry/Path.js +364 -0
- package/dist/src/geometry/Rect2.d.ts +47 -0
- package/dist/src/geometry/Rect2.js +148 -0
- package/dist/src/geometry/Vec2.d.ts +13 -0
- package/dist/src/geometry/Vec2.js +13 -0
- package/dist/src/geometry/Vec3.d.ts +32 -0
- package/dist/src/geometry/Vec3.js +98 -0
- package/dist/src/localization.d.ts +12 -0
- package/dist/src/localization.js +5 -0
- package/dist/src/main.d.ts +3 -0
- package/dist/src/main.js +4 -0
- package/dist/src/rendering/AbstractRenderer.d.ts +38 -0
- package/dist/src/rendering/AbstractRenderer.js +108 -0
- package/dist/src/rendering/CanvasRenderer.d.ts +23 -0
- package/dist/src/rendering/CanvasRenderer.js +108 -0
- package/dist/src/rendering/DummyRenderer.d.ts +25 -0
- package/dist/src/rendering/DummyRenderer.js +65 -0
- package/dist/src/rendering/SVGRenderer.d.ts +27 -0
- package/dist/src/rendering/SVGRenderer.js +122 -0
- package/dist/src/testing/loadExpectExtensions.d.ts +17 -0
- package/dist/src/testing/loadExpectExtensions.js +27 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +12 -0
- package/dist/src/toolbar/HTMLToolbar.js +444 -0
- package/dist/src/toolbar/types.d.ts +17 -0
- package/dist/src/toolbar/types.js +5 -0
- package/dist/src/tools/BaseTool.d.ts +20 -0
- package/dist/src/tools/BaseTool.js +44 -0
- package/dist/src/tools/Eraser.d.ts +16 -0
- package/dist/src/tools/Eraser.js +53 -0
- package/dist/src/tools/PanZoom.d.ts +40 -0
- package/dist/src/tools/PanZoom.js +191 -0
- package/dist/src/tools/Pen.d.ts +25 -0
- package/dist/src/tools/Pen.js +97 -0
- package/dist/src/tools/SelectionTool.d.ts +49 -0
- package/dist/src/tools/SelectionTool.js +437 -0
- package/dist/src/tools/ToolController.d.ts +18 -0
- package/dist/src/tools/ToolController.js +110 -0
- package/dist/src/tools/ToolEnabledGroup.d.ts +6 -0
- package/dist/src/tools/ToolEnabledGroup.js +11 -0
- package/dist/src/tools/localization.d.ts +10 -0
- package/dist/src/tools/localization.js +9 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.js +20 -0
- package/jest.config.js +22 -0
- package/lint-staged.config.js +6 -0
- package/package.json +82 -0
- package/src/Color4.test.ts +12 -0
- package/src/Color4.ts +122 -0
- package/src/Display.ts +118 -0
- package/src/Editor.css +58 -0
- package/src/Editor.ts +469 -0
- package/src/EditorImage.test.ts +90 -0
- package/src/EditorImage.ts +297 -0
- package/src/EventDispatcher.test.ts +123 -0
- package/src/EventDispatcher.ts +53 -0
- package/src/Pointer.ts +93 -0
- package/src/SVGLoader.ts +230 -0
- package/src/StrokeBuilder.ts +362 -0
- package/src/UndoRedoHistory.ts +61 -0
- package/src/Viewport.ts +168 -0
- package/src/commands/Command.ts +43 -0
- package/src/commands/Erase.ts +52 -0
- package/src/commands/localization.ts +38 -0
- package/src/components/AbstractComponent.ts +73 -0
- package/src/components/Stroke.test.ts +18 -0
- package/src/components/Stroke.ts +102 -0
- package/src/components/UnknownSVGObject.ts +36 -0
- package/src/components/localization.ts +9 -0
- package/src/editorStyles.js +3 -0
- package/src/geometry/LineSegment2.test.ts +77 -0
- package/src/geometry/LineSegment2.ts +127 -0
- package/src/geometry/Mat33.test.ts +144 -0
- package/src/geometry/Mat33.ts +268 -0
- package/src/geometry/Path.fromString.test.ts +146 -0
- package/src/geometry/Path.test.ts +96 -0
- package/src/geometry/Path.toString.test.ts +31 -0
- package/src/geometry/Path.ts +456 -0
- package/src/geometry/Rect2.test.ts +121 -0
- package/src/geometry/Rect2.ts +215 -0
- package/src/geometry/Vec2.test.ts +32 -0
- package/src/geometry/Vec2.ts +18 -0
- package/src/geometry/Vec3.test.ts +29 -0
- package/src/geometry/Vec3.ts +133 -0
- package/src/localization.ts +27 -0
- package/src/rendering/AbstractRenderer.ts +164 -0
- package/src/rendering/CanvasRenderer.ts +141 -0
- package/src/rendering/DummyRenderer.ts +80 -0
- package/src/rendering/SVGRenderer.ts +159 -0
- package/src/testing/loadExpectExtensions.ts +43 -0
- package/src/toolbar/HTMLToolbar.ts +551 -0
- package/src/toolbar/toolbar.css +110 -0
- package/src/toolbar/types.ts +20 -0
- package/src/tools/BaseTool.ts +58 -0
- package/src/tools/Eraser.ts +67 -0
- package/src/tools/PanZoom.ts +253 -0
- package/src/tools/Pen.ts +121 -0
- package/src/tools/SelectionTool.test.ts +85 -0
- package/src/tools/SelectionTool.ts +545 -0
- package/src/tools/ToolController.ts +126 -0
- package/src/tools/ToolEnabledGroup.ts +14 -0
- package/src/tools/localization.ts +22 -0
- package/src/types.ts +133 -0
- package/tsconfig.json +28 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
// A vector with three components. Can also be used to represent a two-component vector
|
2
|
+
export default class Vec3 {
|
3
|
+
constructor(x, y, z) {
|
4
|
+
this.x = x;
|
5
|
+
this.y = y;
|
6
|
+
this.z = z;
|
7
|
+
}
|
8
|
+
// Returns the x, y components of this
|
9
|
+
get xy() {
|
10
|
+
// Useful for APIs that behave differently if .z is present.
|
11
|
+
return {
|
12
|
+
x: this.x,
|
13
|
+
y: this.y,
|
14
|
+
};
|
15
|
+
}
|
16
|
+
static of(x, y, z) {
|
17
|
+
return new Vec3(x, y, z);
|
18
|
+
}
|
19
|
+
// Returns this' [idx]th component
|
20
|
+
at(idx) {
|
21
|
+
if (idx === 0)
|
22
|
+
return this.x;
|
23
|
+
if (idx === 1)
|
24
|
+
return this.y;
|
25
|
+
if (idx === 2)
|
26
|
+
return this.z;
|
27
|
+
throw new Error(`${idx} out of bounds!`);
|
28
|
+
}
|
29
|
+
magnitude() {
|
30
|
+
return Math.sqrt(this.dot(this));
|
31
|
+
}
|
32
|
+
magnitudeSquared() {
|
33
|
+
return this.dot(this);
|
34
|
+
}
|
35
|
+
// Return this' angle in the XY plane (treats this as a Vec2)
|
36
|
+
angle() {
|
37
|
+
return Math.atan2(this.y, this.x);
|
38
|
+
}
|
39
|
+
normalized() {
|
40
|
+
const norm = this.magnitude();
|
41
|
+
return Vec3.of(this.x / norm, this.y / norm, this.z / norm);
|
42
|
+
}
|
43
|
+
times(c) {
|
44
|
+
return Vec3.of(this.x * c, this.y * c, this.z * c);
|
45
|
+
}
|
46
|
+
plus(v) {
|
47
|
+
return Vec3.of(this.x + v.x, this.y + v.y, this.z + v.z);
|
48
|
+
}
|
49
|
+
minus(v) {
|
50
|
+
return this.plus(v.times(-1));
|
51
|
+
}
|
52
|
+
dot(other) {
|
53
|
+
return this.x * other.x + this.y * other.y + this.z * other.z;
|
54
|
+
}
|
55
|
+
cross(other) {
|
56
|
+
// | i j k |
|
57
|
+
// | x1 y1 z1| = (i)(y1z2 - y2z1) - (j)(x1z2 - x2z1) + (k)(x1y2 - x2y1)
|
58
|
+
// | x2 y2 z2|
|
59
|
+
return Vec3.of(this.y * other.z - other.y * this.z, other.x * this.z - this.x * other.z, this.x * other.y - other.x * this.y);
|
60
|
+
}
|
61
|
+
// Returns this plus a vector of length [distance] in [direction]
|
62
|
+
extend(distance, direction) {
|
63
|
+
return this.plus(direction.normalized().times(distance));
|
64
|
+
}
|
65
|
+
// Returns a vector [fractionTo] of the way to target from this.
|
66
|
+
lerp(target, fractionTo) {
|
67
|
+
return this.times(1 - fractionTo).plus(target.times(fractionTo));
|
68
|
+
}
|
69
|
+
// [zip] Maps a component of this and a corresponding component of
|
70
|
+
// [other] to a component of the output vector.
|
71
|
+
zip(other, zip) {
|
72
|
+
return Vec3.of(zip(other.x, this.x), zip(other.y, this.y), zip(other.z, this.z));
|
73
|
+
}
|
74
|
+
// Returns a vector with each component acted on by [fn]
|
75
|
+
map(fn) {
|
76
|
+
return Vec3.of(fn(this.x), fn(this.y), fn(this.z));
|
77
|
+
}
|
78
|
+
asArray() {
|
79
|
+
return [this.x, this.y, this.z];
|
80
|
+
}
|
81
|
+
// [fuzz] The maximum difference between two components for this and [other]
|
82
|
+
// to be considered equal.
|
83
|
+
eq(other, fuzz) {
|
84
|
+
for (let i = 0; i < 3; i++) {
|
85
|
+
if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
|
86
|
+
return false;
|
87
|
+
}
|
88
|
+
}
|
89
|
+
return true;
|
90
|
+
}
|
91
|
+
toString() {
|
92
|
+
return `Vec(${this.x}, ${this.y}, ${this.z})`;
|
93
|
+
}
|
94
|
+
}
|
95
|
+
Vec3.unitX = Vec3.of(1, 0, 0);
|
96
|
+
Vec3.unitY = Vec3.of(0, 1, 0);
|
97
|
+
Vec3.unitZ = Vec3.of(0, 0, 1);
|
98
|
+
Vec3.zero = Vec3.of(0, 0, 0);
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { CommandLocalization } from './commands/localization';
|
2
|
+
import { ImageComponentLocalization } from './components/localization';
|
3
|
+
import { ToolbarLocalization } from './toolbar/types';
|
4
|
+
import { ToolLocalization } from './tools/localization';
|
5
|
+
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
|
6
|
+
undoAnnouncement: (actionDescription: string) => string;
|
7
|
+
redoAnnouncement: (actionDescription: string) => string;
|
8
|
+
doneLoading: string;
|
9
|
+
loading: (percentage: number) => string;
|
10
|
+
imageEditor: string;
|
11
|
+
}
|
12
|
+
export declare const defaultEditorLocalization: EditorLocalization;
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { defaultCommandLocalization } from './commands/localization';
|
2
|
+
import { defaultComponentLocalization } from './components/localization';
|
3
|
+
import HTMLToolbar from './toolbar/HTMLToolbar';
|
4
|
+
import { defaultToolLocalization } from './tools/localization';
|
5
|
+
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, HTMLToolbar.defaultLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
|
package/dist/src/main.js
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import Color4 from '../Color4';
|
2
|
+
import { PathCommand } from '../geometry/Path';
|
3
|
+
import Rect2 from '../geometry/Rect2';
|
4
|
+
import { Point2, Vec2 } from '../geometry/Vec2';
|
5
|
+
import Viewport from '../Viewport';
|
6
|
+
export interface RenderingStyle {
|
7
|
+
fill: Color4;
|
8
|
+
stroke?: {
|
9
|
+
color: Color4;
|
10
|
+
width: number;
|
11
|
+
};
|
12
|
+
}
|
13
|
+
export interface RenderablePathSpec {
|
14
|
+
startPoint: Point2;
|
15
|
+
commands: PathCommand[];
|
16
|
+
style: RenderingStyle;
|
17
|
+
}
|
18
|
+
export default abstract class AbstractRenderer {
|
19
|
+
protected viewport: Viewport;
|
20
|
+
protected constructor(viewport: Viewport);
|
21
|
+
abstract displaySize(): Vec2;
|
22
|
+
abstract clear(): void;
|
23
|
+
protected abstract beginPath(startPoint: Point2): void;
|
24
|
+
protected abstract endPath(style: RenderingStyle): void;
|
25
|
+
protected abstract lineTo(point: Point2): void;
|
26
|
+
protected abstract moveTo(point: Point2): void;
|
27
|
+
protected abstract traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
28
|
+
protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
|
29
|
+
private objectLevel;
|
30
|
+
private currentPaths;
|
31
|
+
private flushPath;
|
32
|
+
drawPath(path: RenderablePathSpec): void;
|
33
|
+
drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
|
34
|
+
startObject(_boundingBox: Rect2): void;
|
35
|
+
endObject(): void;
|
36
|
+
protected getNestingLevel(): number;
|
37
|
+
abstract drawPoints(...points: Point2[]): void;
|
38
|
+
}
|
@@ -0,0 +1,108 @@
|
|
1
|
+
import { PathCommandType } from '../geometry/Path';
|
2
|
+
import Rect2 from '../geometry/Rect2';
|
3
|
+
import { Vec2 } from '../geometry/Vec2';
|
4
|
+
const stylesEqual = (a, b) => {
|
5
|
+
var _a, _b, _c, _d, _e;
|
6
|
+
return a === b || (a.fill.eq(b.fill)
|
7
|
+
&& ((_b = (_a = a.stroke) === null || _a === void 0 ? void 0 : _a.color) === null || _b === void 0 ? void 0 : _b.eq((_c = b.stroke) === null || _c === void 0 ? void 0 : _c.color))
|
8
|
+
&& ((_d = a.stroke) === null || _d === void 0 ? void 0 : _d.width) === ((_e = b.stroke) === null || _e === void 0 ? void 0 : _e.width));
|
9
|
+
};
|
10
|
+
export default class AbstractRenderer {
|
11
|
+
constructor(viewport) {
|
12
|
+
this.viewport = viewport;
|
13
|
+
this.objectLevel = 0;
|
14
|
+
this.currentPaths = null;
|
15
|
+
}
|
16
|
+
flushPath() {
|
17
|
+
if (!this.currentPaths) {
|
18
|
+
return;
|
19
|
+
}
|
20
|
+
let lastStyle = null;
|
21
|
+
for (const path of this.currentPaths) {
|
22
|
+
const { startPoint, commands, style } = path;
|
23
|
+
if (!lastStyle || !stylesEqual(lastStyle, style)) {
|
24
|
+
if (lastStyle) {
|
25
|
+
this.endPath(lastStyle);
|
26
|
+
}
|
27
|
+
this.beginPath(startPoint);
|
28
|
+
lastStyle = style;
|
29
|
+
}
|
30
|
+
else {
|
31
|
+
this.moveTo(startPoint);
|
32
|
+
}
|
33
|
+
for (const command of commands) {
|
34
|
+
if (command.kind === PathCommandType.LineTo) {
|
35
|
+
this.lineTo(command.point);
|
36
|
+
}
|
37
|
+
else if (command.kind === PathCommandType.MoveTo) {
|
38
|
+
this.moveTo(command.point);
|
39
|
+
}
|
40
|
+
else if (command.kind === PathCommandType.CubicBezierTo) {
|
41
|
+
this.traceCubicBezierCurve(command.controlPoint1, command.controlPoint2, command.endPoint);
|
42
|
+
}
|
43
|
+
else if (command.kind === PathCommandType.QuadraticBezierTo) {
|
44
|
+
this.traceQuadraticBezierCurve(command.controlPoint, command.endPoint);
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
if (lastStyle) {
|
49
|
+
this.endPath(lastStyle);
|
50
|
+
}
|
51
|
+
}
|
52
|
+
drawPath(path) {
|
53
|
+
// If we're being called outside of an object,
|
54
|
+
// we can't delay rendering
|
55
|
+
if (this.objectLevel === 0) {
|
56
|
+
this.currentPaths = [path];
|
57
|
+
this.flushPath();
|
58
|
+
this.currentPaths = null;
|
59
|
+
}
|
60
|
+
else {
|
61
|
+
// Otherwise, don't render paths all at once. This prevents faint lines between
|
62
|
+
// segments of the same stroke from being visible.
|
63
|
+
this.currentPaths.push(path);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]
|
67
|
+
drawRect(rect, lineWidth, lineFill) {
|
68
|
+
const commands = [];
|
69
|
+
// Vector from the top left corner or bottom right corner to the edge of the
|
70
|
+
// stroked region.
|
71
|
+
const cornerToEdge = Vec2.of(lineWidth, lineWidth).times(0.5);
|
72
|
+
const innerRect = Rect2.fromCorners(rect.topLeft.plus(cornerToEdge), rect.bottomRight.minus(cornerToEdge));
|
73
|
+
const outerRect = Rect2.fromCorners(rect.topLeft.minus(cornerToEdge), rect.bottomRight.plus(cornerToEdge));
|
74
|
+
const corners = [
|
75
|
+
innerRect.corners[3],
|
76
|
+
...innerRect.corners,
|
77
|
+
...outerRect.corners.reverse(),
|
78
|
+
];
|
79
|
+
for (const corner of corners) {
|
80
|
+
commands.push({
|
81
|
+
kind: PathCommandType.LineTo,
|
82
|
+
point: corner,
|
83
|
+
});
|
84
|
+
}
|
85
|
+
this.drawPath({
|
86
|
+
startPoint: outerRect.corners[3],
|
87
|
+
commands,
|
88
|
+
style: lineFill,
|
89
|
+
});
|
90
|
+
}
|
91
|
+
// Note the start/end of an object with the given bounding box.
|
92
|
+
startObject(_boundingBox) {
|
93
|
+
this.currentPaths = [];
|
94
|
+
this.objectLevel++;
|
95
|
+
}
|
96
|
+
endObject() {
|
97
|
+
// Render the paths all at once
|
98
|
+
this.flushPath();
|
99
|
+
this.currentPaths = null;
|
100
|
+
this.objectLevel--;
|
101
|
+
if (this.objectLevel < 0) {
|
102
|
+
throw new Error('More objects have ended than have been started (negative object nesting level)!');
|
103
|
+
}
|
104
|
+
}
|
105
|
+
getNestingLevel() {
|
106
|
+
return this.objectLevel;
|
107
|
+
}
|
108
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import Rect2 from '../geometry/Rect2';
|
2
|
+
import { Point2, Vec2 } from '../geometry/Vec2';
|
3
|
+
import Vec3 from '../geometry/Vec3';
|
4
|
+
import Viewport from '../Viewport';
|
5
|
+
import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from './AbstractRenderer';
|
6
|
+
export default class CanvasRenderer extends AbstractRenderer {
|
7
|
+
private ctx;
|
8
|
+
private ignoreObjectsAboveLevel;
|
9
|
+
private ignoringObject;
|
10
|
+
constructor(ctx: CanvasRenderingContext2D, viewport: Viewport);
|
11
|
+
displaySize(): Vec2;
|
12
|
+
clear(): void;
|
13
|
+
protected beginPath(startPoint: Point2): void;
|
14
|
+
protected endPath(style: RenderingStyle): void;
|
15
|
+
protected lineTo(point: Point2): void;
|
16
|
+
protected moveTo(point: Point2): void;
|
17
|
+
protected traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
18
|
+
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
19
|
+
drawPath(path: RenderablePathSpec): void;
|
20
|
+
startObject(boundingBox: Rect2): void;
|
21
|
+
endObject(): void;
|
22
|
+
drawPoints(...points: Point2[]): void;
|
23
|
+
}
|
@@ -0,0 +1,108 @@
|
|
1
|
+
import Color4 from '../Color4';
|
2
|
+
import { Vec2 } from '../geometry/Vec2';
|
3
|
+
import AbstractRenderer from './AbstractRenderer';
|
4
|
+
const minSquareCurveApproxDist = 25;
|
5
|
+
export default class CanvasRenderer extends AbstractRenderer {
|
6
|
+
constructor(ctx, viewport) {
|
7
|
+
super(viewport);
|
8
|
+
this.ctx = ctx;
|
9
|
+
this.ignoreObjectsAboveLevel = null;
|
10
|
+
this.ignoringObject = false;
|
11
|
+
}
|
12
|
+
displaySize() {
|
13
|
+
return Vec2.of(this.ctx.canvas.clientWidth, this.ctx.canvas.clientHeight);
|
14
|
+
}
|
15
|
+
clear() {
|
16
|
+
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
17
|
+
}
|
18
|
+
beginPath(startPoint) {
|
19
|
+
startPoint = this.viewport.canvasToScreen(startPoint);
|
20
|
+
this.ctx.beginPath();
|
21
|
+
this.ctx.moveTo(startPoint.x, startPoint.y);
|
22
|
+
}
|
23
|
+
endPath(style) {
|
24
|
+
this.ctx.fillStyle = style.fill.toHexString();
|
25
|
+
this.ctx.fill();
|
26
|
+
if (style.stroke) {
|
27
|
+
this.ctx.strokeStyle = style.stroke.color.toHexString();
|
28
|
+
this.ctx.lineWidth = this.viewport.getScaleFactor() * style.stroke.width;
|
29
|
+
this.ctx.stroke();
|
30
|
+
}
|
31
|
+
this.ctx.closePath();
|
32
|
+
}
|
33
|
+
lineTo(point) {
|
34
|
+
point = this.viewport.canvasToScreen(point);
|
35
|
+
this.ctx.lineTo(point.x, point.y);
|
36
|
+
}
|
37
|
+
moveTo(point) {
|
38
|
+
point = this.viewport.canvasToScreen(point);
|
39
|
+
this.ctx.moveTo(point.x, point.y);
|
40
|
+
}
|
41
|
+
traceCubicBezierCurve(p1, p2, p3) {
|
42
|
+
p1 = this.viewport.canvasToScreen(p1);
|
43
|
+
p2 = this.viewport.canvasToScreen(p2);
|
44
|
+
p3 = this.viewport.canvasToScreen(p3);
|
45
|
+
// Approximate the curve if small enough.
|
46
|
+
const delta1 = p2.minus(p1);
|
47
|
+
const delta2 = p3.minus(p2);
|
48
|
+
if (delta1.magnitudeSquared() < minSquareCurveApproxDist
|
49
|
+
&& delta2.magnitudeSquared() < minSquareCurveApproxDist) {
|
50
|
+
this.ctx.lineTo(p3.x, p3.y);
|
51
|
+
}
|
52
|
+
else {
|
53
|
+
this.ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
|
54
|
+
}
|
55
|
+
}
|
56
|
+
traceQuadraticBezierCurve(controlPoint, endPoint) {
|
57
|
+
controlPoint = this.viewport.canvasToScreen(controlPoint);
|
58
|
+
endPoint = this.viewport.canvasToScreen(endPoint);
|
59
|
+
// Approximate the curve with a line if small enough
|
60
|
+
const delta = controlPoint.minus(endPoint);
|
61
|
+
if (delta.magnitudeSquared() < minSquareCurveApproxDist) {
|
62
|
+
this.ctx.lineTo(endPoint.x, endPoint.y);
|
63
|
+
}
|
64
|
+
else {
|
65
|
+
this.ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
drawPath(path) {
|
69
|
+
if (this.ignoringObject) {
|
70
|
+
return;
|
71
|
+
}
|
72
|
+
super.drawPath(path);
|
73
|
+
}
|
74
|
+
startObject(boundingBox) {
|
75
|
+
// Should we ignore all objects within this object's bbox?
|
76
|
+
const diagonal = this.viewport.canvasToScreenTransform.transformVec3(boundingBox.size);
|
77
|
+
const minRenderSize = 4;
|
78
|
+
if (Math.abs(diagonal.x) < minRenderSize && Math.abs(diagonal.y) < minRenderSize) {
|
79
|
+
this.ignoreObjectsAboveLevel = this.getNestingLevel();
|
80
|
+
this.ignoringObject = true;
|
81
|
+
}
|
82
|
+
super.startObject(boundingBox);
|
83
|
+
}
|
84
|
+
endObject() {
|
85
|
+
super.endObject();
|
86
|
+
// If exiting an object with a too-small-to-draw bounding box,
|
87
|
+
if (this.ignoreObjectsAboveLevel !== null && this.getNestingLevel() <= this.ignoreObjectsAboveLevel) {
|
88
|
+
this.ignoreObjectsAboveLevel = null;
|
89
|
+
this.ignoringObject = false;
|
90
|
+
}
|
91
|
+
}
|
92
|
+
drawPoints(...points) {
|
93
|
+
const pointRadius = 10;
|
94
|
+
for (let i = 0; i < points.length; i++) {
|
95
|
+
const point = this.viewport.canvasToScreen(points[i]);
|
96
|
+
this.ctx.beginPath();
|
97
|
+
this.ctx.arc(point.x, point.y, pointRadius, 0, Math.PI * 2);
|
98
|
+
this.ctx.fillStyle = Color4.ofRGBA(0.5 + Math.sin(i) / 2, 1.0, 0.5 + Math.cos(i * 0.2) / 4, 0.5).toHexString();
|
99
|
+
this.ctx.fill();
|
100
|
+
this.ctx.stroke();
|
101
|
+
this.ctx.closePath();
|
102
|
+
this.ctx.textAlign = 'center';
|
103
|
+
this.ctx.textBaseline = 'middle';
|
104
|
+
this.ctx.fillStyle = 'black';
|
105
|
+
this.ctx.fillText(`${i}`, point.x, point.y, pointRadius * 2);
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import Rect2 from '../geometry/Rect2';
|
2
|
+
import { Point2, Vec2 } from '../geometry/Vec2';
|
3
|
+
import Vec3 from '../geometry/Vec3';
|
4
|
+
import Viewport from '../Viewport';
|
5
|
+
import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
|
6
|
+
export default class DummyRenderer extends AbstractRenderer {
|
7
|
+
clearedCount: number;
|
8
|
+
renderedPathCount: number;
|
9
|
+
lastFillStyle: RenderingStyle | null;
|
10
|
+
lastPoint: Point2 | null;
|
11
|
+
objectNestingLevel: number;
|
12
|
+
pointBuffer: Point2[];
|
13
|
+
constructor(viewport: Viewport);
|
14
|
+
displaySize(): Vec2;
|
15
|
+
clear(): void;
|
16
|
+
protected beginPath(startPoint: Vec3): void;
|
17
|
+
protected endPath(style: RenderingStyle): void;
|
18
|
+
protected lineTo(point: Vec3): void;
|
19
|
+
protected moveTo(point: Point2): void;
|
20
|
+
protected traceCubicBezierCurve(p1: Vec3, p2: Vec3, p3: Vec3): void;
|
21
|
+
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
22
|
+
drawPoints(..._points: Vec3[]): void;
|
23
|
+
startObject(boundingBox: Rect2): void;
|
24
|
+
endObject(): void;
|
25
|
+
}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
// Renderer that outputs nothing. Useful for automated tests.
|
2
|
+
import { Vec2 } from '../geometry/Vec2';
|
3
|
+
import AbstractRenderer from './AbstractRenderer';
|
4
|
+
export default class DummyRenderer extends AbstractRenderer {
|
5
|
+
constructor(viewport) {
|
6
|
+
super(viewport);
|
7
|
+
// Variables that track the state of what's been rendered
|
8
|
+
this.clearedCount = 0;
|
9
|
+
this.renderedPathCount = 0;
|
10
|
+
this.lastFillStyle = null;
|
11
|
+
this.lastPoint = null;
|
12
|
+
this.objectNestingLevel = 0;
|
13
|
+
// List of points drawn since the last clear.
|
14
|
+
this.pointBuffer = [];
|
15
|
+
}
|
16
|
+
displaySize() {
|
17
|
+
// Return a dummy
|
18
|
+
return Vec2.of(640, 480);
|
19
|
+
}
|
20
|
+
clear() {
|
21
|
+
this.clearedCount++;
|
22
|
+
this.renderedPathCount = 0;
|
23
|
+
this.pointBuffer = [];
|
24
|
+
// Ensure all objects finished rendering
|
25
|
+
if (this.objectNestingLevel > 0) {
|
26
|
+
throw new Error(`Within an object while clearing! Nesting level: ${this.objectNestingLevel}`);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
beginPath(startPoint) {
|
30
|
+
this.lastPoint = startPoint;
|
31
|
+
this.pointBuffer.push(startPoint);
|
32
|
+
}
|
33
|
+
endPath(style) {
|
34
|
+
this.renderedPathCount++;
|
35
|
+
this.lastFillStyle = style;
|
36
|
+
}
|
37
|
+
lineTo(point) {
|
38
|
+
this.lastPoint = point;
|
39
|
+
this.pointBuffer.push(point);
|
40
|
+
}
|
41
|
+
moveTo(point) {
|
42
|
+
this.lastPoint = point;
|
43
|
+
this.pointBuffer.push(point);
|
44
|
+
}
|
45
|
+
traceCubicBezierCurve(p1, p2, p3) {
|
46
|
+
this.lastPoint = p3;
|
47
|
+
this.pointBuffer.push(p1, p2, p3);
|
48
|
+
}
|
49
|
+
traceQuadraticBezierCurve(controlPoint, endPoint) {
|
50
|
+
this.lastPoint = endPoint;
|
51
|
+
this.pointBuffer.push(controlPoint, endPoint);
|
52
|
+
}
|
53
|
+
drawPoints(..._points) {
|
54
|
+
// drawPoints is intended for debugging.
|
55
|
+
// As such, it is unlikely to be the target of automated tests.
|
56
|
+
}
|
57
|
+
startObject(boundingBox) {
|
58
|
+
super.startObject(boundingBox);
|
59
|
+
this.objectNestingLevel += 1;
|
60
|
+
}
|
61
|
+
endObject() {
|
62
|
+
super.endObject();
|
63
|
+
this.objectNestingLevel -= 1;
|
64
|
+
}
|
65
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import Rect2 from '../geometry/Rect2';
|
2
|
+
import { Point2, Vec2 } from '../geometry/Vec2';
|
3
|
+
import Viewport from '../Viewport';
|
4
|
+
import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
|
5
|
+
export default class SVGRenderer extends AbstractRenderer {
|
6
|
+
private elem;
|
7
|
+
private currentPath;
|
8
|
+
private pathStart;
|
9
|
+
private lastPathStyle;
|
10
|
+
private lastPath;
|
11
|
+
private lastPathStart;
|
12
|
+
private mainGroup;
|
13
|
+
constructor(elem: SVGSVGElement, viewport: Viewport);
|
14
|
+
displaySize(): Vec2;
|
15
|
+
clear(): void;
|
16
|
+
protected beginPath(startPoint: Point2): void;
|
17
|
+
protected endPath(style: RenderingStyle): void;
|
18
|
+
private addPathToSVG;
|
19
|
+
startObject(boundingBox: Rect2): void;
|
20
|
+
endObject(): void;
|
21
|
+
protected lineTo(point: Point2): void;
|
22
|
+
protected moveTo(point: Point2): void;
|
23
|
+
protected traceCubicBezierCurve(controlPoint1: Point2, controlPoint2: Point2, endPoint: Point2): void;
|
24
|
+
protected traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
|
25
|
+
drawPoints(...points: Point2[]): void;
|
26
|
+
drawSVGElem(elem: SVGElement): void;
|
27
|
+
}
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import Path, { PathCommandType } from '../geometry/Path';
|
2
|
+
import { Vec2 } from '../geometry/Vec2';
|
3
|
+
import AbstractRenderer from './AbstractRenderer';
|
4
|
+
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
5
|
+
export default class SVGRenderer extends AbstractRenderer {
|
6
|
+
constructor(elem, viewport) {
|
7
|
+
super(viewport);
|
8
|
+
this.elem = elem;
|
9
|
+
this.clear();
|
10
|
+
}
|
11
|
+
displaySize() {
|
12
|
+
return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
|
13
|
+
}
|
14
|
+
clear() {
|
15
|
+
this.mainGroup = document.createElementNS(svgNameSpace, 'g');
|
16
|
+
// Remove all children
|
17
|
+
this.elem.replaceChildren(this.mainGroup);
|
18
|
+
}
|
19
|
+
beginPath(startPoint) {
|
20
|
+
var _a;
|
21
|
+
this.currentPath = [];
|
22
|
+
this.pathStart = this.viewport.canvasToScreen(startPoint);
|
23
|
+
(_a = this.lastPathStart) !== null && _a !== void 0 ? _a : (this.lastPathStart = this.pathStart);
|
24
|
+
}
|
25
|
+
endPath(style) {
|
26
|
+
var _a;
|
27
|
+
if (this.currentPath == null) {
|
28
|
+
throw new Error('No path exists to end! Make sure beginPath was called!');
|
29
|
+
}
|
30
|
+
// Try to extend the previous path, if possible
|
31
|
+
if (style.fill.eq((_a = this.lastPathStyle) === null || _a === void 0 ? void 0 : _a.fill) && this.lastPath != null) {
|
32
|
+
this.lastPath.push({
|
33
|
+
kind: PathCommandType.MoveTo,
|
34
|
+
point: this.pathStart,
|
35
|
+
}, ...this.currentPath);
|
36
|
+
this.pathStart = null;
|
37
|
+
this.currentPath = null;
|
38
|
+
}
|
39
|
+
else {
|
40
|
+
this.addPathToSVG();
|
41
|
+
this.lastPathStart = this.pathStart;
|
42
|
+
this.lastPathStyle = style;
|
43
|
+
this.lastPath = this.currentPath;
|
44
|
+
this.pathStart = null;
|
45
|
+
this.currentPath = null;
|
46
|
+
}
|
47
|
+
}
|
48
|
+
// Push [this.fullPath] to the SVG
|
49
|
+
addPathToSVG() {
|
50
|
+
if (!this.lastPathStyle || !this.lastPath) {
|
51
|
+
return;
|
52
|
+
}
|
53
|
+
const pathElem = document.createElementNS(svgNameSpace, 'path');
|
54
|
+
pathElem.setAttribute('d', Path.toString(this.lastPathStart, this.lastPath));
|
55
|
+
const style = this.lastPathStyle;
|
56
|
+
pathElem.setAttribute('fill', style.fill.toHexString());
|
57
|
+
if (style.stroke) {
|
58
|
+
pathElem.setAttribute('stroke', style.stroke.color.toHexString());
|
59
|
+
pathElem.setAttribute('stroke-width', style.stroke.width.toString());
|
60
|
+
}
|
61
|
+
this.mainGroup.appendChild(pathElem);
|
62
|
+
}
|
63
|
+
startObject(boundingBox) {
|
64
|
+
super.startObject(boundingBox);
|
65
|
+
// Only accumulate a path within an object
|
66
|
+
this.lastPath = null;
|
67
|
+
this.lastPathStart = null;
|
68
|
+
this.lastPathStyle = null;
|
69
|
+
}
|
70
|
+
endObject() {
|
71
|
+
super.endObject();
|
72
|
+
// Don't extend paths across objects
|
73
|
+
this.addPathToSVG();
|
74
|
+
}
|
75
|
+
lineTo(point) {
|
76
|
+
point = this.viewport.canvasToScreen(point);
|
77
|
+
this.currentPath.push({
|
78
|
+
kind: PathCommandType.LineTo,
|
79
|
+
point,
|
80
|
+
});
|
81
|
+
}
|
82
|
+
moveTo(point) {
|
83
|
+
point = this.viewport.canvasToScreen(point);
|
84
|
+
this.currentPath.push({
|
85
|
+
kind: PathCommandType.MoveTo,
|
86
|
+
point,
|
87
|
+
});
|
88
|
+
}
|
89
|
+
traceCubicBezierCurve(controlPoint1, controlPoint2, endPoint) {
|
90
|
+
controlPoint1 = this.viewport.canvasToScreen(controlPoint1);
|
91
|
+
controlPoint2 = this.viewport.canvasToScreen(controlPoint2);
|
92
|
+
endPoint = this.viewport.canvasToScreen(endPoint);
|
93
|
+
this.currentPath.push({
|
94
|
+
kind: PathCommandType.CubicBezierTo,
|
95
|
+
controlPoint1,
|
96
|
+
controlPoint2,
|
97
|
+
endPoint,
|
98
|
+
});
|
99
|
+
}
|
100
|
+
traceQuadraticBezierCurve(controlPoint, endPoint) {
|
101
|
+
controlPoint = this.viewport.canvasToScreen(controlPoint);
|
102
|
+
endPoint = this.viewport.canvasToScreen(endPoint);
|
103
|
+
this.currentPath.push({
|
104
|
+
kind: PathCommandType.QuadraticBezierTo,
|
105
|
+
controlPoint,
|
106
|
+
endPoint,
|
107
|
+
});
|
108
|
+
}
|
109
|
+
drawPoints(...points) {
|
110
|
+
points.map(point => {
|
111
|
+
const elem = document.createElementNS(svgNameSpace, 'circle');
|
112
|
+
elem.setAttribute('cx', `${point.x}`);
|
113
|
+
elem.setAttribute('cy', `${point.y}`);
|
114
|
+
elem.setAttribute('r', '15');
|
115
|
+
this.mainGroup.appendChild(elem);
|
116
|
+
});
|
117
|
+
}
|
118
|
+
// Renders a copy of the given element.
|
119
|
+
drawSVGElem(elem) {
|
120
|
+
this.elem.appendChild(elem.cloneNode(true));
|
121
|
+
}
|
122
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
export declare const loadExpectExtensions: () => void;
|
2
|
+
export interface CustomMatchers<R = unknown> {
|
3
|
+
objEq(expected: {
|
4
|
+
eq: (other: any, ...args: any) => boolean;
|
5
|
+
}, ...opts: any): R;
|
6
|
+
}
|
7
|
+
declare global {
|
8
|
+
export namespace jest {
|
9
|
+
interface Expect extends CustomMatchers {
|
10
|
+
}
|
11
|
+
interface Matchers<R> extends CustomMatchers<R> {
|
12
|
+
}
|
13
|
+
interface AsyncAsymmetricMatchers extends CustomMatchers {
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
export default loadExpectExtensions;
|