js-draw 0.6.0 → 0.7.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/.firebase/hosting.ZG9jcw.cache +338 -0
- package/.github/ISSUE_TEMPLATE/translation.md +1 -1
- package/CHANGELOG.md +11 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +0 -1
- package/dist/src/Editor.js +4 -3
- package/dist/src/SVGLoader.js +2 -2
- package/dist/src/components/Stroke.js +1 -0
- package/dist/src/components/Text.d.ts +10 -5
- package/dist/src/components/Text.js +49 -15
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +9 -2
- package/dist/src/components/builders/FreehandLineBuilder.js +127 -28
- package/dist/src/components/lib.d.ts +2 -2
- package/dist/src/components/lib.js +2 -2
- package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
- package/dist/src/rendering/renderers/SVGRenderer.js +49 -22
- package/dist/src/toolbar/IconProvider.d.ts +24 -18
- package/dist/src/toolbar/IconProvider.js +23 -21
- package/dist/src/toolbar/widgets/PenToolWidget.js +8 -5
- package/dist/src/tools/PasteHandler.js +2 -22
- package/dist/src/tools/TextTool.d.ts +4 -0
- package/dist/src/tools/TextTool.js +76 -15
- package/package.json +1 -1
- package/src/Editor.toSVG.test.ts +27 -0
- package/src/Editor.ts +4 -4
- package/src/SVGLoader.test.ts +20 -0
- package/src/SVGLoader.ts +4 -4
- package/src/components/Stroke.ts +1 -0
- package/src/components/Text.test.ts +3 -3
- package/src/components/Text.ts +62 -19
- package/src/components/builders/FreehandLineBuilder.ts +160 -32
- package/src/components/lib.ts +3 -3
- package/src/rendering/renderers/CanvasRenderer.ts +2 -2
- package/src/rendering/renderers/SVGRenderer.ts +50 -24
- package/src/toolbar/IconProvider.ts +24 -20
- package/src/toolbar/widgets/PenToolWidget.ts +9 -5
- package/src/tools/PasteHandler.ts +2 -24
- package/src/tools/TextTool.ts +86 -17
package/dist/src/Editor.d.ts
CHANGED
package/dist/src/Editor.js
CHANGED
@@ -42,6 +42,7 @@ import Pointer from './Pointer';
|
|
42
42
|
import Mat33 from './math/Mat33';
|
43
43
|
import getLocalizationTable from './localizations/getLocalizationTable';
|
44
44
|
import IconProvider from './toolbar/IconProvider';
|
45
|
+
import { toRoundedString } from './math/rounding';
|
45
46
|
// { @inheritDoc Editor! }
|
46
47
|
export class Editor {
|
47
48
|
/**
|
@@ -638,9 +639,9 @@ export class Editor {
|
|
638
639
|
importExportViewport.resetTransform(origTransform);
|
639
640
|
// Just show the main region
|
640
641
|
const rect = importExportViewport.visibleRect;
|
641
|
-
result.setAttribute('viewBox',
|
642
|
-
result.setAttribute('width',
|
643
|
-
result.setAttribute('height',
|
642
|
+
result.setAttribute('viewBox', [rect.x, rect.y, rect.w, rect.h].map(part => toRoundedString(part)).join(' '));
|
643
|
+
result.setAttribute('width', toRoundedString(rect.w));
|
644
|
+
result.setAttribute('height', toRoundedString(rect.h));
|
644
645
|
// Ensure the image can be identified as an SVG if downloaded.
|
645
646
|
// See https://jwatt.org/svg/authoring/
|
646
647
|
result.setAttribute('version', '1.1');
|
package/dist/src/SVGLoader.js
CHANGED
@@ -11,7 +11,7 @@ import Color4 from './Color4';
|
|
11
11
|
import ImageComponent from './components/ImageComponent';
|
12
12
|
import Stroke from './components/Stroke';
|
13
13
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
14
|
-
import
|
14
|
+
import TextComponent from './components/Text';
|
15
15
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
16
16
|
import Mat33 from './math/Mat33';
|
17
17
|
import Path from './math/Path';
|
@@ -209,7 +209,7 @@ export default class SVGLoader {
|
|
209
209
|
};
|
210
210
|
const supportedAttrs = [];
|
211
211
|
const transform = this.getTransform(elem, supportedAttrs, computedStyles);
|
212
|
-
const result = new
|
212
|
+
const result = new TextComponent(contentList, transform, style);
|
213
213
|
this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
|
214
214
|
return result;
|
215
215
|
}
|
@@ -3,6 +3,7 @@ import Rect2 from '../math/Rect2';
|
|
3
3
|
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
4
4
|
import AbstractComponent from './AbstractComponent';
|
5
5
|
export default class Stroke extends AbstractComponent {
|
6
|
+
// Creates a `Stroke` from the given `parts`.
|
6
7
|
constructor(parts) {
|
7
8
|
var _a;
|
8
9
|
super('stroke');
|
@@ -12,24 +12,29 @@ export interface TextStyle {
|
|
12
12
|
fontVariant?: string;
|
13
13
|
renderingStyle: RenderingStyle;
|
14
14
|
}
|
15
|
-
export default class
|
16
|
-
protected readonly textObjects: Array<string |
|
15
|
+
export default class TextComponent extends AbstractComponent {
|
16
|
+
protected readonly textObjects: Array<string | TextComponent>;
|
17
17
|
private transform;
|
18
|
-
private
|
18
|
+
private style;
|
19
19
|
protected contentBBox: Rect2;
|
20
|
-
constructor(textObjects: Array<string |
|
20
|
+
constructor(textObjects: Array<string | TextComponent>, transform: Mat33, style: TextStyle);
|
21
21
|
static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
|
22
22
|
private static textMeasuringCtx;
|
23
23
|
private static estimateTextDimens;
|
24
24
|
private static getTextDimens;
|
25
25
|
private computeBBoxOfPart;
|
26
26
|
private recomputeBBox;
|
27
|
+
private renderInternal;
|
27
28
|
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
28
29
|
intersects(lineSegment: LineSegment2): boolean;
|
30
|
+
getBaselinePos(): import("../lib").Vec3;
|
31
|
+
getTextStyle(): TextStyle;
|
32
|
+
getTransform(): Mat33;
|
29
33
|
protected applyTransformation(affineTransfm: Mat33): void;
|
30
34
|
protected createClone(): AbstractComponent;
|
31
35
|
getText(): string;
|
32
36
|
description(localizationTable: ImageComponentLocalization): string;
|
33
37
|
protected serializeToJSON(): Record<string, any>;
|
34
|
-
static deserializeFromString(json: any):
|
38
|
+
static deserializeFromString(json: any): TextComponent;
|
39
|
+
static fromLines(lines: string[], transform: Mat33, style: TextStyle): AbstractComponent;
|
35
40
|
}
|
@@ -1,16 +1,23 @@
|
|
1
1
|
import LineSegment2 from '../math/LineSegment2';
|
2
2
|
import Mat33 from '../math/Mat33';
|
3
3
|
import Rect2 from '../math/Rect2';
|
4
|
+
import { Vec2 } from '../math/Vec2';
|
4
5
|
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
5
6
|
import AbstractComponent from './AbstractComponent';
|
6
7
|
const componentTypeId = 'text';
|
7
|
-
export default class
|
8
|
+
export default class TextComponent extends AbstractComponent {
|
8
9
|
constructor(textObjects, transform, style) {
|
9
10
|
super(componentTypeId);
|
10
11
|
this.textObjects = textObjects;
|
11
12
|
this.transform = transform;
|
12
13
|
this.style = style;
|
13
14
|
this.recomputeBBox();
|
15
|
+
// If this has no direct children, choose a style representative of this' content
|
16
|
+
// (useful for estimating the style of the TextComponent).
|
17
|
+
const hasDirectContent = textObjects.some(obj => typeof obj === 'string');
|
18
|
+
if (!hasDirectContent && textObjects.length > 0) {
|
19
|
+
this.style = textObjects[0].getTextStyle();
|
20
|
+
}
|
14
21
|
}
|
15
22
|
static applyTextStyles(ctx, style) {
|
16
23
|
var _a, _b;
|
@@ -35,12 +42,12 @@ export default class Text extends AbstractComponent {
|
|
35
42
|
// Returns the bounding box of `text`. This is approximate if no Canvas is available.
|
36
43
|
static getTextDimens(text, style) {
|
37
44
|
var _a, _b;
|
38
|
-
(_a =
|
39
|
-
if (!
|
45
|
+
(_a = TextComponent.textMeasuringCtx) !== null && _a !== void 0 ? _a : (TextComponent.textMeasuringCtx = (_b = document.createElement('canvas').getContext('2d')) !== null && _b !== void 0 ? _b : null);
|
46
|
+
if (!TextComponent.textMeasuringCtx) {
|
40
47
|
return this.estimateTextDimens(text, style);
|
41
48
|
}
|
42
|
-
const ctx =
|
43
|
-
|
49
|
+
const ctx = TextComponent.textMeasuringCtx;
|
50
|
+
TextComponent.applyTextStyles(ctx, style);
|
44
51
|
const measure = ctx.measureText(text);
|
45
52
|
// Text is drawn with (0,0) at the bottom left of the baseline.
|
46
53
|
const textY = -measure.actualBoundingBoxAscent;
|
@@ -49,7 +56,7 @@ export default class Text extends AbstractComponent {
|
|
49
56
|
}
|
50
57
|
computeBBoxOfPart(part) {
|
51
58
|
if (typeof part === 'string') {
|
52
|
-
const textBBox =
|
59
|
+
const textBBox = TextComponent.getTextDimens(part, this.style);
|
53
60
|
return textBBox.transformedBoundingBox(this.transform);
|
54
61
|
}
|
55
62
|
else {
|
@@ -66,19 +73,22 @@ export default class Text extends AbstractComponent {
|
|
66
73
|
}
|
67
74
|
this.contentBBox = bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
|
68
75
|
}
|
69
|
-
|
76
|
+
renderInternal(canvas) {
|
70
77
|
const cursor = this.transform;
|
71
|
-
canvas.startObject(this.contentBBox);
|
72
78
|
for (const textObject of this.textObjects) {
|
73
79
|
if (typeof textObject === 'string') {
|
74
80
|
canvas.drawText(textObject, cursor, this.style);
|
75
81
|
}
|
76
82
|
else {
|
77
83
|
canvas.pushTransform(cursor);
|
78
|
-
textObject.
|
84
|
+
textObject.renderInternal(canvas);
|
79
85
|
canvas.popTransform();
|
80
86
|
}
|
81
87
|
}
|
88
|
+
}
|
89
|
+
render(canvas, _visibleRect) {
|
90
|
+
canvas.startObject(this.contentBBox);
|
91
|
+
this.renderInternal(canvas);
|
82
92
|
canvas.endObject(this.getLoadSaveData());
|
83
93
|
}
|
84
94
|
intersects(lineSegment) {
|
@@ -89,7 +99,7 @@ export default class Text extends AbstractComponent {
|
|
89
99
|
lineSegment = new LineSegment2(p1InThisSpace, p2InThisSpace);
|
90
100
|
for (const subObject of this.textObjects) {
|
91
101
|
if (typeof subObject === 'string') {
|
92
|
-
const textBBox =
|
102
|
+
const textBBox = TextComponent.getTextDimens(subObject, this.style);
|
93
103
|
// TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and
|
94
104
|
// use pixel-testing to check for intersection with its contour.
|
95
105
|
if (textBBox.getEdges().some(edge => lineSegment.intersection(edge) !== null)) {
|
@@ -104,12 +114,21 @@ export default class Text extends AbstractComponent {
|
|
104
114
|
}
|
105
115
|
return false;
|
106
116
|
}
|
117
|
+
getBaselinePos() {
|
118
|
+
return this.transform.transformVec2(Vec2.zero);
|
119
|
+
}
|
120
|
+
getTextStyle() {
|
121
|
+
return this.style;
|
122
|
+
}
|
123
|
+
getTransform() {
|
124
|
+
return this.transform;
|
125
|
+
}
|
107
126
|
applyTransformation(affineTransfm) {
|
108
127
|
this.transform = affineTransfm.rightMul(this.transform);
|
109
128
|
this.recomputeBBox();
|
110
129
|
}
|
111
130
|
createClone() {
|
112
|
-
return new
|
131
|
+
return new TextComponent(this.textObjects, this.transform, this.style);
|
113
132
|
}
|
114
133
|
getText() {
|
115
134
|
const result = [];
|
@@ -159,7 +178,7 @@ export default class Text extends AbstractComponent {
|
|
159
178
|
if (((_a = data.text) !== null && _a !== void 0 ? _a : null) !== null) {
|
160
179
|
return data.text;
|
161
180
|
}
|
162
|
-
return
|
181
|
+
return TextComponent.deserializeFromString(data.json);
|
163
182
|
});
|
164
183
|
json.transform = json.transform.filter((elem) => typeof elem === 'number');
|
165
184
|
if (json.transform.length !== 9) {
|
@@ -167,8 +186,23 @@ export default class Text extends AbstractComponent {
|
|
167
186
|
}
|
168
187
|
const transformData = json.transform;
|
169
188
|
const transform = new Mat33(...transformData);
|
170
|
-
return new
|
189
|
+
return new TextComponent(textObjects, transform, style);
|
190
|
+
}
|
191
|
+
static fromLines(lines, transform, style) {
|
192
|
+
let lastComponent = null;
|
193
|
+
const components = [];
|
194
|
+
for (const line of lines) {
|
195
|
+
let position = Vec2.zero;
|
196
|
+
if (lastComponent) {
|
197
|
+
const lineMargin = Math.floor(style.size);
|
198
|
+
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
|
199
|
+
}
|
200
|
+
const component = new TextComponent([line], Mat33.translation(position), style);
|
201
|
+
components.push(component);
|
202
|
+
lastComponent = component;
|
203
|
+
}
|
204
|
+
return new TextComponent(components, transform, style);
|
171
205
|
}
|
172
206
|
}
|
173
|
-
|
174
|
-
AbstractComponent.registerComponent(componentTypeId, (data) =>
|
207
|
+
TextComponent.textMeasuringCtx = null;
|
208
|
+
AbstractComponent.registerComponent(componentTypeId, (data) => TextComponent.deserializeFromString(data));
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
2
2
|
import Rect2 from '../../math/Rect2';
|
3
3
|
import Stroke from '../Stroke';
|
4
|
+
import Viewport from '../../Viewport';
|
4
5
|
import { StrokeDataPoint } from '../../types';
|
5
6
|
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
6
7
|
export declare const makeFreehandLineBuilder: ComponentBuilderFactory;
|
@@ -8,11 +9,15 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
8
9
|
private startPoint;
|
9
10
|
private minFitAllowed;
|
10
11
|
private maxFitAllowed;
|
12
|
+
private viewport;
|
11
13
|
private isFirstSegment;
|
12
14
|
private pathStartConnector;
|
13
15
|
private mostRecentConnector;
|
14
16
|
private upperSegments;
|
15
17
|
private lowerSegments;
|
18
|
+
private lastUpperBezier;
|
19
|
+
private lastLowerBezier;
|
20
|
+
private parts;
|
16
21
|
private buffer;
|
17
22
|
private lastPoint;
|
18
23
|
private lastExitingVec;
|
@@ -21,14 +26,16 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
21
26
|
private curveEndWidth;
|
22
27
|
private momentum;
|
23
28
|
private bbox;
|
24
|
-
constructor(startPoint: StrokeDataPoint, minFitAllowed: number, maxFitAllowed: number);
|
29
|
+
constructor(startPoint: StrokeDataPoint, minFitAllowed: number, maxFitAllowed: number, viewport: Viewport);
|
25
30
|
getBBox(): Rect2;
|
26
31
|
private getRenderingStyle;
|
27
|
-
private
|
32
|
+
private previewCurrentPath;
|
33
|
+
private previewFullPath;
|
28
34
|
private previewStroke;
|
29
35
|
preview(renderer: AbstractRenderer): void;
|
30
36
|
build(): Stroke;
|
31
37
|
private roundPoint;
|
38
|
+
private shouldStartNewSegment;
|
32
39
|
private approxCurrentCurveLength;
|
33
40
|
private finalizeCurrentCurve;
|
34
41
|
private currentSegmentToPath;
|
@@ -6,11 +6,11 @@ import LineSegment2 from '../../math/LineSegment2';
|
|
6
6
|
import Stroke from '../Stroke';
|
7
7
|
import Viewport from '../../Viewport';
|
8
8
|
export const makeFreehandLineBuilder = (initialPoint, viewport) => {
|
9
|
-
// Don't smooth if input is more than ±
|
9
|
+
// Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
|
10
10
|
// less than ±1 px from the curve.
|
11
|
-
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() *
|
11
|
+
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
|
12
12
|
const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
|
13
|
-
return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist);
|
13
|
+
return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport);
|
14
14
|
};
|
15
15
|
// Handles stroke smoothing and creates Strokes from user/stylus input.
|
16
16
|
export default class FreehandLineBuilder {
|
@@ -19,13 +19,17 @@ export default class FreehandLineBuilder {
|
|
19
19
|
// for which a point is considered 'part of the curve'.
|
20
20
|
// Note that the maximum will be smaller if the stroke width is less than
|
21
21
|
// [maxFitAllowed].
|
22
|
-
minFitAllowed, maxFitAllowed) {
|
22
|
+
minFitAllowed, maxFitAllowed, viewport) {
|
23
23
|
this.startPoint = startPoint;
|
24
24
|
this.minFitAllowed = minFitAllowed;
|
25
25
|
this.maxFitAllowed = maxFitAllowed;
|
26
|
+
this.viewport = viewport;
|
26
27
|
this.isFirstSegment = true;
|
27
28
|
this.pathStartConnector = null;
|
28
29
|
this.mostRecentConnector = null;
|
30
|
+
this.lastUpperBezier = null;
|
31
|
+
this.lastLowerBezier = null;
|
32
|
+
this.parts = [];
|
29
33
|
this.lastExitingVec = null;
|
30
34
|
this.currentCurve = null;
|
31
35
|
this.lastPoint = this.startPoint;
|
@@ -46,16 +50,16 @@ export default class FreehandLineBuilder {
|
|
46
50
|
fill: (_a = this.lastPoint.color) !== null && _a !== void 0 ? _a : null,
|
47
51
|
};
|
48
52
|
}
|
49
|
-
|
53
|
+
previewCurrentPath() {
|
50
54
|
var _a;
|
51
|
-
|
52
|
-
|
55
|
+
const upperPath = this.upperSegments.slice();
|
56
|
+
const lowerPath = this.lowerSegments.slice();
|
53
57
|
let lowerToUpperCap;
|
54
58
|
let pathStartConnector;
|
55
59
|
if (this.currentCurve) {
|
56
|
-
const {
|
57
|
-
upperPath
|
58
|
-
lowerPath
|
60
|
+
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand } = this.currentSegmentToPath();
|
61
|
+
upperPath.push(upperCurveCommand);
|
62
|
+
lowerPath.push(lowerCurveCommand);
|
59
63
|
lowerToUpperCap = lowerToUpperConnector;
|
60
64
|
pathStartConnector = (_a = this.pathStartConnector) !== null && _a !== void 0 ? _a : upperToLowerConnector;
|
61
65
|
}
|
@@ -63,12 +67,17 @@ export default class FreehandLineBuilder {
|
|
63
67
|
if (this.mostRecentConnector === null || this.pathStartConnector === null) {
|
64
68
|
return null;
|
65
69
|
}
|
66
|
-
upperPath = this.upperSegments.slice();
|
67
|
-
lowerPath = this.lowerSegments.slice();
|
68
70
|
lowerToUpperCap = this.mostRecentConnector;
|
69
71
|
pathStartConnector = this.pathStartConnector;
|
70
72
|
}
|
71
|
-
|
73
|
+
let startPoint;
|
74
|
+
const lastLowerSegment = lowerPath[lowerPath.length - 1];
|
75
|
+
if (lastLowerSegment.kind === PathCommandType.LineTo || lastLowerSegment.kind === PathCommandType.MoveTo) {
|
76
|
+
startPoint = lastLowerSegment.point;
|
77
|
+
}
|
78
|
+
else {
|
79
|
+
startPoint = lastLowerSegment.endPoint;
|
80
|
+
}
|
72
81
|
return {
|
73
82
|
// Start at the end of the lower curve:
|
74
83
|
// Start point
|
@@ -103,21 +112,33 @@ export default class FreehandLineBuilder {
|
|
103
112
|
style: this.getRenderingStyle(),
|
104
113
|
};
|
105
114
|
}
|
115
|
+
previewFullPath() {
|
116
|
+
const preview = this.previewCurrentPath();
|
117
|
+
if (preview) {
|
118
|
+
return [...this.parts, preview];
|
119
|
+
}
|
120
|
+
return null;
|
121
|
+
}
|
106
122
|
previewStroke() {
|
107
|
-
const pathPreview = this.
|
123
|
+
const pathPreview = this.previewFullPath();
|
108
124
|
if (pathPreview) {
|
109
|
-
return new Stroke(
|
125
|
+
return new Stroke(pathPreview);
|
110
126
|
}
|
111
127
|
return null;
|
112
128
|
}
|
113
129
|
preview(renderer) {
|
114
|
-
const
|
115
|
-
if (
|
116
|
-
|
130
|
+
const paths = this.previewFullPath();
|
131
|
+
if (paths) {
|
132
|
+
const approxBBox = this.viewport.visibleRect;
|
133
|
+
renderer.startObject(approxBBox);
|
134
|
+
for (const path of paths) {
|
135
|
+
renderer.drawPath(path);
|
136
|
+
}
|
137
|
+
renderer.endObject();
|
117
138
|
}
|
118
139
|
}
|
119
140
|
build() {
|
120
|
-
if (this.lastPoint
|
141
|
+
if (this.lastPoint) {
|
121
142
|
this.finalizeCurrentCurve();
|
122
143
|
}
|
123
144
|
return this.previewStroke();
|
@@ -129,6 +150,61 @@ export default class FreehandLineBuilder {
|
|
129
150
|
}
|
130
151
|
return Viewport.roundPoint(point, minFit);
|
131
152
|
}
|
153
|
+
// Returns true if, due to overlap with previous segments, a new RenderablePathSpec should be created.
|
154
|
+
shouldStartNewSegment(lowerCurve, upperCurve) {
|
155
|
+
if (!this.lastLowerBezier || !this.lastUpperBezier) {
|
156
|
+
return false;
|
157
|
+
}
|
158
|
+
const getIntersection = (curve1, curve2) => {
|
159
|
+
const intersection = curve1.intersects(curve2);
|
160
|
+
if (!intersection || intersection.length === 0) {
|
161
|
+
return null;
|
162
|
+
}
|
163
|
+
// From http://pomax.github.io/bezierjs/#intersect-curve,
|
164
|
+
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
|
165
|
+
const firstTPair = intersection[0];
|
166
|
+
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
|
167
|
+
if (!match) {
|
168
|
+
throw new Error(`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`);
|
169
|
+
}
|
170
|
+
const t = parseFloat(match[1]);
|
171
|
+
return Vec2.ofXY(curve1.get(t));
|
172
|
+
};
|
173
|
+
const getExitDirection = (curve) => {
|
174
|
+
return Vec2.ofXY(curve.points[2]).minus(Vec2.ofXY(curve.points[1])).normalized();
|
175
|
+
};
|
176
|
+
const getEnterDirection = (curve) => {
|
177
|
+
return Vec2.ofXY(curve.points[1]).minus(Vec2.ofXY(curve.points[0])).normalized();
|
178
|
+
};
|
179
|
+
// Prevent
|
180
|
+
// /
|
181
|
+
// / /
|
182
|
+
// / / /|
|
183
|
+
// / / |
|
184
|
+
// / |
|
185
|
+
// where the next stroke and the previous stroke are in different directions.
|
186
|
+
//
|
187
|
+
// Are the exit/enter directions of the previous and current curves in different enough directions?
|
188
|
+
if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.3
|
189
|
+
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.3
|
190
|
+
// Also handle if the curves exit/enter directions differ
|
191
|
+
|| getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
|
192
|
+
|| getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
|
193
|
+
return true;
|
194
|
+
}
|
195
|
+
// Check whether the lower curve intersects the other wall:
|
196
|
+
// / / ← lower
|
197
|
+
// / / /
|
198
|
+
// / / /
|
199
|
+
// //
|
200
|
+
// / /
|
201
|
+
const lowerIntersection = getIntersection(lowerCurve, this.lastUpperBezier);
|
202
|
+
const upperIntersection = getIntersection(upperCurve, this.lastLowerBezier);
|
203
|
+
if (lowerIntersection || upperIntersection) {
|
204
|
+
return true;
|
205
|
+
}
|
206
|
+
return false;
|
207
|
+
}
|
132
208
|
// Returns the distance between the start, control, and end points of the curve.
|
133
209
|
approxCurrentCurveLength() {
|
134
210
|
if (!this.currentCurve) {
|
@@ -185,8 +261,17 @@ export default class FreehandLineBuilder {
|
|
185
261
|
this.mostRecentConnector = this.pathStartConnector;
|
186
262
|
return;
|
187
263
|
}
|
188
|
-
const {
|
189
|
-
|
264
|
+
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, } = this.currentSegmentToPath();
|
265
|
+
const shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
|
266
|
+
if (shouldStartNew) {
|
267
|
+
const part = this.previewCurrentPath();
|
268
|
+
if (part) {
|
269
|
+
this.parts.push(part);
|
270
|
+
this.upperSegments = [];
|
271
|
+
this.lowerSegments = [];
|
272
|
+
}
|
273
|
+
}
|
274
|
+
if (this.isFirstSegment || shouldStartNew) {
|
190
275
|
// We draw the upper path (reversed), then the lower path, so we need the
|
191
276
|
// upperToLowerConnector to join the two paths.
|
192
277
|
this.pathStartConnector = upperToLowerConnector;
|
@@ -195,8 +280,10 @@ export default class FreehandLineBuilder {
|
|
195
280
|
// With the most recent connector, we're joining the end of the lowerPath to the most recent
|
196
281
|
// upperPath:
|
197
282
|
this.mostRecentConnector = lowerToUpperConnector;
|
198
|
-
this.
|
199
|
-
this.
|
283
|
+
this.lowerSegments.push(lowerCurveCommand);
|
284
|
+
this.upperSegments.push(upperCurveCommand);
|
285
|
+
this.lastLowerBezier = lowerCurve;
|
286
|
+
this.lastUpperBezier = upperCurve;
|
200
287
|
const lastPoint = this.buffer[this.buffer.length - 1];
|
201
288
|
this.lastExitingVec = Vec2.ofXY(this.currentCurve.points[2]).minus(Vec2.ofXY(this.currentCurve.points[1]));
|
202
289
|
console.assert(this.lastExitingVec.magnitude() !== 0, 'lastExitingVec has zero length!');
|
@@ -238,11 +325,13 @@ export default class FreehandLineBuilder {
|
|
238
325
|
.normalized().times(this.curveStartWidth / 2 * halfVecT
|
239
326
|
+ this.curveEndWidth / 2 * (1 - halfVecT));
|
240
327
|
// Each starts at startPt ± startVec
|
328
|
+
const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
|
241
329
|
const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
|
242
330
|
const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
|
243
331
|
const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
|
244
332
|
const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
|
245
|
-
const
|
333
|
+
const upperCurveEndPoint = this.roundPoint(startPt.minus(startVec));
|
334
|
+
const lowerCurveCommand = {
|
246
335
|
kind: PathCommandType.QuadraticBezierTo,
|
247
336
|
controlPoint: lowerCurveControlPoint,
|
248
337
|
endPoint: lowerCurveEndPoint,
|
@@ -250,19 +339,24 @@ export default class FreehandLineBuilder {
|
|
250
339
|
// From the end of the upperCurve to the start of the lowerCurve:
|
251
340
|
const upperToLowerConnector = {
|
252
341
|
kind: PathCommandType.LineTo,
|
253
|
-
point:
|
342
|
+
point: lowerCurveStartPoint,
|
254
343
|
};
|
255
344
|
// From the end of lowerCurve to the start of upperCurve:
|
256
345
|
const lowerToUpperConnector = {
|
257
346
|
kind: PathCommandType.LineTo,
|
258
347
|
point: upperCurveStartPoint,
|
259
348
|
};
|
260
|
-
const
|
349
|
+
const upperCurveCommand = {
|
261
350
|
kind: PathCommandType.QuadraticBezierTo,
|
262
351
|
controlPoint: upperCurveControlPoint,
|
263
|
-
endPoint:
|
352
|
+
endPoint: upperCurveEndPoint,
|
353
|
+
};
|
354
|
+
const upperCurve = new Bezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
|
355
|
+
const lowerCurve = new Bezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
|
356
|
+
return {
|
357
|
+
upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
|
358
|
+
upperCurve, lowerCurve,
|
264
359
|
};
|
265
|
-
return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
|
266
360
|
}
|
267
361
|
// Compute the direction of the velocity at the end of this.buffer
|
268
362
|
computeExitingVec() {
|
@@ -299,6 +393,10 @@ export default class FreehandLineBuilder {
|
|
299
393
|
const pointRadius = newPoint.width / 2;
|
300
394
|
const prevEndWidth = this.curveEndWidth;
|
301
395
|
this.curveEndWidth = pointRadius;
|
396
|
+
if (this.isFirstSegment) {
|
397
|
+
// The start of a curve often lacks accurate pressure information. Update it.
|
398
|
+
this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
|
399
|
+
}
|
302
400
|
// recompute bbox
|
303
401
|
this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
|
304
402
|
if (this.currentCurve === null) {
|
@@ -310,6 +408,7 @@ export default class FreehandLineBuilder {
|
|
310
408
|
this.curveStartWidth = lastPoint.width / 2;
|
311
409
|
console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
|
312
410
|
}
|
411
|
+
// If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
|
313
412
|
let enteringVec = this.lastExitingVec;
|
314
413
|
if (!enteringVec) {
|
315
414
|
let sampleIdx = Math.ceil(this.buffer.length / 2);
|
@@ -3,6 +3,6 @@ export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
|
|
3
3
|
export * from './AbstractComponent';
|
4
4
|
export { default as AbstractComponent } from './AbstractComponent';
|
5
5
|
import Stroke from './Stroke';
|
6
|
-
import
|
6
|
+
import TextComponent from './Text';
|
7
7
|
import ImageComponent from './ImageComponent';
|
8
|
-
export { Stroke, Text,
|
8
|
+
export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
|
@@ -3,6 +3,6 @@ export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
|
|
3
3
|
export * from './AbstractComponent';
|
4
4
|
export { default as AbstractComponent } from './AbstractComponent';
|
5
5
|
import Stroke from './Stroke';
|
6
|
-
import
|
6
|
+
import TextComponent from './Text';
|
7
7
|
import ImageComponent from './ImageComponent';
|
8
|
-
export { Stroke, Text,
|
8
|
+
export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
-
import
|
2
|
+
import TextComponent from '../../components/Text';
|
3
3
|
import { Vec2 } from '../../math/Vec2';
|
4
4
|
import AbstractRenderer from './AbstractRenderer';
|
5
5
|
export default class CanvasRenderer extends AbstractRenderer {
|
@@ -113,7 +113,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
113
113
|
this.ctx.save();
|
114
114
|
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
115
115
|
this.transformBy(transform);
|
116
|
-
|
116
|
+
TextComponent.applyTextStyles(this.ctx, style);
|
117
117
|
if (style.renderingStyle.fill.a !== 0) {
|
118
118
|
this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
|
119
119
|
this.ctx.fillText(text, 0, 0);
|
@@ -20,6 +20,8 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
20
20
|
private addPathToSVG;
|
21
21
|
drawPath(pathSpec: RenderablePathSpec): void;
|
22
22
|
private transformFrom;
|
23
|
+
private textContainer;
|
24
|
+
private textContainerTransform;
|
23
25
|
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
24
26
|
drawImage(image: RenderableImage): void;
|
25
27
|
startObject(boundingBox: Rect2): void;
|