js-draw 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.firebase/hosting.ZG9jcw.cache +338 -0
- package/.github/ISSUE_TEMPLATE/translation.md +1 -1
- package/CHANGELOG.md +8 -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 +73 -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 +82 -17
package/src/components/Text.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import LineSegment2 from '../math/LineSegment2';
|
2
2
|
import Mat33, { Mat33Array } from '../math/Mat33';
|
3
3
|
import Rect2 from '../math/Rect2';
|
4
|
+
import { Vec2 } from '../math/Vec2';
|
4
5
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
6
|
import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
6
7
|
import AbstractComponent from './AbstractComponent';
|
@@ -15,16 +16,23 @@ export interface TextStyle {
|
|
15
16
|
}
|
16
17
|
|
17
18
|
const componentTypeId = 'text';
|
18
|
-
export default class
|
19
|
+
export default class TextComponent extends AbstractComponent {
|
19
20
|
protected contentBBox: Rect2;
|
20
21
|
|
21
22
|
public constructor(
|
22
|
-
protected readonly textObjects: Array<string|
|
23
|
+
protected readonly textObjects: Array<string|TextComponent>,
|
23
24
|
private transform: Mat33,
|
24
|
-
private
|
25
|
+
private style: TextStyle,
|
25
26
|
) {
|
26
27
|
super(componentTypeId);
|
27
28
|
this.recomputeBBox();
|
29
|
+
|
30
|
+
// If this has no direct children, choose a style representative of this' content
|
31
|
+
// (useful for estimating the style of the TextComponent).
|
32
|
+
const hasDirectContent = textObjects.some(obj => typeof obj === 'string');
|
33
|
+
if (!hasDirectContent && textObjects.length > 0) {
|
34
|
+
this.style = (textObjects[0] as TextComponent).getTextStyle();
|
35
|
+
}
|
28
36
|
}
|
29
37
|
|
30
38
|
public static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle) {
|
@@ -55,13 +63,13 @@ export default class Text extends AbstractComponent {
|
|
55
63
|
|
56
64
|
// Returns the bounding box of `text`. This is approximate if no Canvas is available.
|
57
65
|
private static getTextDimens(text: string, style: TextStyle): Rect2 {
|
58
|
-
|
59
|
-
if (!
|
66
|
+
TextComponent.textMeasuringCtx ??= document.createElement('canvas').getContext('2d') ?? null;
|
67
|
+
if (!TextComponent.textMeasuringCtx) {
|
60
68
|
return this.estimateTextDimens(text, style);
|
61
69
|
}
|
62
70
|
|
63
|
-
const ctx =
|
64
|
-
|
71
|
+
const ctx = TextComponent.textMeasuringCtx;
|
72
|
+
TextComponent.applyTextStyles(ctx, style);
|
65
73
|
|
66
74
|
const measure = ctx.measureText(text);
|
67
75
|
|
@@ -71,9 +79,9 @@ export default class Text extends AbstractComponent {
|
|
71
79
|
return new Rect2(0, textY, measure.width, textHeight);
|
72
80
|
}
|
73
81
|
|
74
|
-
private computeBBoxOfPart(part: string|
|
82
|
+
private computeBBoxOfPart(part: string|TextComponent) {
|
75
83
|
if (typeof part === 'string') {
|
76
|
-
const textBBox =
|
84
|
+
const textBBox = TextComponent.getTextDimens(part, this.style);
|
77
85
|
return textBBox.transformedBoundingBox(this.transform);
|
78
86
|
} else {
|
79
87
|
const bbox = part.contentBBox.transformedBoundingBox(this.transform);
|
@@ -93,19 +101,23 @@ export default class Text extends AbstractComponent {
|
|
93
101
|
this.contentBBox = bbox ?? Rect2.empty;
|
94
102
|
}
|
95
103
|
|
96
|
-
|
104
|
+
private renderInternal(canvas: AbstractRenderer) {
|
97
105
|
const cursor = this.transform;
|
98
106
|
|
99
|
-
canvas.startObject(this.contentBBox);
|
100
107
|
for (const textObject of this.textObjects) {
|
101
108
|
if (typeof textObject === 'string') {
|
102
109
|
canvas.drawText(textObject, cursor, this.style);
|
103
110
|
} else {
|
104
111
|
canvas.pushTransform(cursor);
|
105
|
-
textObject.
|
112
|
+
textObject.renderInternal(canvas);
|
106
113
|
canvas.popTransform();
|
107
114
|
}
|
108
115
|
}
|
116
|
+
}
|
117
|
+
|
118
|
+
public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
|
119
|
+
canvas.startObject(this.contentBBox);
|
120
|
+
this.renderInternal(canvas);
|
109
121
|
canvas.endObject(this.getLoadSaveData());
|
110
122
|
}
|
111
123
|
|
@@ -119,7 +131,7 @@ export default class Text extends AbstractComponent {
|
|
119
131
|
|
120
132
|
for (const subObject of this.textObjects) {
|
121
133
|
if (typeof subObject === 'string') {
|
122
|
-
const textBBox =
|
134
|
+
const textBBox = TextComponent.getTextDimens(subObject, this.style);
|
123
135
|
|
124
136
|
// TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and
|
125
137
|
// use pixel-testing to check for intersection with its contour.
|
@@ -136,13 +148,25 @@ export default class Text extends AbstractComponent {
|
|
136
148
|
return false;
|
137
149
|
}
|
138
150
|
|
151
|
+
public getBaselinePos() {
|
152
|
+
return this.transform.transformVec2(Vec2.zero);
|
153
|
+
}
|
154
|
+
|
155
|
+
public getTextStyle() {
|
156
|
+
return this.style;
|
157
|
+
}
|
158
|
+
|
159
|
+
public getTransform(): Mat33 {
|
160
|
+
return this.transform;
|
161
|
+
}
|
162
|
+
|
139
163
|
protected applyTransformation(affineTransfm: Mat33): void {
|
140
164
|
this.transform = affineTransfm.rightMul(this.transform);
|
141
165
|
this.recomputeBBox();
|
142
166
|
}
|
143
167
|
|
144
168
|
protected createClone(): AbstractComponent {
|
145
|
-
return new
|
169
|
+
return new TextComponent(this.textObjects, this.transform, this.style);
|
146
170
|
}
|
147
171
|
|
148
172
|
public getText() {
|
@@ -188,7 +212,7 @@ export default class Text extends AbstractComponent {
|
|
188
212
|
};
|
189
213
|
}
|
190
214
|
|
191
|
-
public static deserializeFromString(json: any):
|
215
|
+
public static deserializeFromString(json: any): TextComponent {
|
192
216
|
const style: TextStyle = {
|
193
217
|
renderingStyle: styleFromJSON(json.style.renderingStyle),
|
194
218
|
size: json.style.size,
|
@@ -197,12 +221,12 @@ export default class Text extends AbstractComponent {
|
|
197
221
|
fontFamily: json.style.fontFamily,
|
198
222
|
};
|
199
223
|
|
200
|
-
const textObjects: Array<string|
|
224
|
+
const textObjects: Array<string|TextComponent> = json.textObjects.map((data: any) => {
|
201
225
|
if ((data.text ?? null) !== null) {
|
202
226
|
return data.text;
|
203
227
|
}
|
204
228
|
|
205
|
-
return
|
229
|
+
return TextComponent.deserializeFromString(data.json);
|
206
230
|
});
|
207
231
|
|
208
232
|
json.transform = json.transform.filter((elem: any) => typeof elem === 'number');
|
@@ -213,8 +237,27 @@ export default class Text extends AbstractComponent {
|
|
213
237
|
const transformData = json.transform as Mat33Array;
|
214
238
|
const transform = new Mat33(...transformData);
|
215
239
|
|
216
|
-
return new
|
240
|
+
return new TextComponent(textObjects, transform, style);
|
241
|
+
}
|
242
|
+
|
243
|
+
public static fromLines(lines: string[], transform: Mat33, style: TextStyle): AbstractComponent {
|
244
|
+
let lastComponent: TextComponent|null = null;
|
245
|
+
const components: TextComponent[] = [];
|
246
|
+
|
247
|
+
for (const line of lines) {
|
248
|
+
let position = Vec2.zero;
|
249
|
+
if (lastComponent) {
|
250
|
+
const lineMargin = Math.floor(style.size);
|
251
|
+
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
|
252
|
+
}
|
253
|
+
|
254
|
+
const component = new TextComponent([ line ], Mat33.translation(position), style);
|
255
|
+
components.push(component);
|
256
|
+
lastComponent = component;
|
257
|
+
}
|
258
|
+
|
259
|
+
return new TextComponent(components, transform, style);
|
217
260
|
}
|
218
261
|
}
|
219
262
|
|
220
|
-
AbstractComponent.registerComponent(componentTypeId, (data: string) =>
|
263
|
+
AbstractComponent.registerComponent(componentTypeId, (data: string) => TextComponent.deserializeFromString(data));
|
@@ -11,21 +11,24 @@ import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
|
11
11
|
import RenderingStyle from '../../rendering/RenderingStyle';
|
12
12
|
|
13
13
|
export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
|
14
|
-
// Don't smooth if input is more than ±
|
14
|
+
// Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
|
15
15
|
// less than ±1 px from the curve.
|
16
|
-
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() *
|
16
|
+
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
|
17
17
|
const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
|
18
18
|
|
19
19
|
return new FreehandLineBuilder(
|
20
|
-
initialPoint, minSmoothingDist, maxSmoothingDist
|
20
|
+
initialPoint, minSmoothingDist, maxSmoothingDist, viewport
|
21
21
|
);
|
22
22
|
};
|
23
23
|
|
24
24
|
type CurrentSegmentToPathResult = {
|
25
|
-
|
25
|
+
upperCurveCommand: QuadraticBezierPathCommand,
|
26
26
|
lowerToUpperConnector: PathCommand,
|
27
27
|
upperToLowerConnector: PathCommand,
|
28
|
-
|
28
|
+
lowerCurveCommand: QuadraticBezierPathCommand,
|
29
|
+
|
30
|
+
upperCurve: Bezier,
|
31
|
+
lowerCurve: Bezier,
|
29
32
|
};
|
30
33
|
|
31
34
|
// Handles stroke smoothing and creates Strokes from user/stylus input.
|
@@ -47,8 +50,11 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
47
50
|
// least recent edge.
|
48
51
|
// The lowerSegments form a path that goes from the least recent edge to the most
|
49
52
|
// recent edge.
|
50
|
-
private upperSegments:
|
51
|
-
private lowerSegments:
|
53
|
+
private upperSegments: PathCommand[];
|
54
|
+
private lowerSegments: PathCommand[];
|
55
|
+
private lastUpperBezier: Bezier|null = null;
|
56
|
+
private lastLowerBezier: Bezier|null = null;
|
57
|
+
private parts: RenderablePathSpec[] = [];
|
52
58
|
|
53
59
|
private buffer: Point2[];
|
54
60
|
private lastPoint: StrokeDataPoint;
|
@@ -69,7 +75,9 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
69
75
|
// Note that the maximum will be smaller if the stroke width is less than
|
70
76
|
// [maxFitAllowed].
|
71
77
|
private minFitAllowed: number,
|
72
|
-
private maxFitAllowed: number
|
78
|
+
private maxFitAllowed: number,
|
79
|
+
|
80
|
+
private viewport: Viewport,
|
73
81
|
) {
|
74
82
|
this.lastPoint = this.startPoint;
|
75
83
|
this.upperSegments = [];
|
@@ -93,15 +101,19 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
93
101
|
};
|
94
102
|
}
|
95
103
|
|
96
|
-
private
|
97
|
-
|
98
|
-
|
104
|
+
private previewCurrentPath(): RenderablePathSpec|null {
|
105
|
+
const upperPath = this.upperSegments.slice();
|
106
|
+
const lowerPath = this.lowerSegments.slice();
|
99
107
|
let lowerToUpperCap: PathCommand;
|
100
108
|
let pathStartConnector: PathCommand;
|
101
109
|
if (this.currentCurve) {
|
102
|
-
const {
|
103
|
-
|
104
|
-
|
110
|
+
const {
|
111
|
+
upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand
|
112
|
+
} = this.currentSegmentToPath();
|
113
|
+
|
114
|
+
upperPath.push(upperCurveCommand);
|
115
|
+
lowerPath.push(lowerCurveCommand);
|
116
|
+
|
105
117
|
lowerToUpperCap = lowerToUpperConnector;
|
106
118
|
pathStartConnector = this.pathStartConnector ?? upperToLowerConnector;
|
107
119
|
} else {
|
@@ -109,13 +121,17 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
109
121
|
return null;
|
110
122
|
}
|
111
123
|
|
112
|
-
upperPath = this.upperSegments.slice();
|
113
|
-
lowerPath = this.lowerSegments.slice();
|
114
124
|
lowerToUpperCap = this.mostRecentConnector;
|
115
125
|
pathStartConnector = this.pathStartConnector;
|
116
126
|
}
|
117
|
-
const startPoint = lowerPath[lowerPath.length - 1].endPoint;
|
118
127
|
|
128
|
+
let startPoint: Point2;
|
129
|
+
const lastLowerSegment = lowerPath[lowerPath.length - 1];
|
130
|
+
if (lastLowerSegment.kind === PathCommandType.LineTo || lastLowerSegment.kind === PathCommandType.MoveTo) {
|
131
|
+
startPoint = lastLowerSegment.point;
|
132
|
+
} else {
|
133
|
+
startPoint = lastLowerSegment.endPoint;
|
134
|
+
}
|
119
135
|
|
120
136
|
return {
|
121
137
|
// Start at the end of the lower curve:
|
@@ -156,24 +172,37 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
156
172
|
};
|
157
173
|
}
|
158
174
|
|
175
|
+
private previewFullPath(): RenderablePathSpec[]|null {
|
176
|
+
const preview = this.previewCurrentPath();
|
177
|
+
if (preview) {
|
178
|
+
return [ ...this.parts, preview ];
|
179
|
+
}
|
180
|
+
return null;
|
181
|
+
}
|
182
|
+
|
159
183
|
private previewStroke(): Stroke|null {
|
160
|
-
const pathPreview = this.
|
184
|
+
const pathPreview = this.previewFullPath();
|
161
185
|
|
162
186
|
if (pathPreview) {
|
163
|
-
return new Stroke(
|
187
|
+
return new Stroke(pathPreview);
|
164
188
|
}
|
165
189
|
return null;
|
166
190
|
}
|
167
191
|
|
168
192
|
public preview(renderer: AbstractRenderer) {
|
169
|
-
const
|
170
|
-
if (
|
171
|
-
|
193
|
+
const paths = this.previewFullPath();
|
194
|
+
if (paths) {
|
195
|
+
const approxBBox = this.viewport.visibleRect;
|
196
|
+
renderer.startObject(approxBBox);
|
197
|
+
for (const path of paths) {
|
198
|
+
renderer.drawPath(path);
|
199
|
+
}
|
200
|
+
renderer.endObject();
|
172
201
|
}
|
173
202
|
}
|
174
203
|
|
175
204
|
public build(): Stroke {
|
176
|
-
if (this.lastPoint
|
205
|
+
if (this.lastPoint) {
|
177
206
|
this.finalizeCurrentCurve();
|
178
207
|
}
|
179
208
|
return this.previewStroke()!;
|
@@ -189,6 +218,74 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
189
218
|
return Viewport.roundPoint(point, minFit);
|
190
219
|
}
|
191
220
|
|
221
|
+
// Returns true if, due to overlap with previous segments, a new RenderablePathSpec should be created.
|
222
|
+
private shouldStartNewSegment(lowerCurve: Bezier, upperCurve: Bezier): boolean {
|
223
|
+
if (!this.lastLowerBezier || !this.lastUpperBezier) {
|
224
|
+
return false;
|
225
|
+
}
|
226
|
+
|
227
|
+
const getIntersection = (curve1: Bezier, curve2: Bezier): Point2|null => {
|
228
|
+
const intersection = curve1.intersects(curve2) as (string[] | null | undefined);
|
229
|
+
if (!intersection || intersection.length === 0) {
|
230
|
+
return null;
|
231
|
+
}
|
232
|
+
|
233
|
+
// From http://pomax.github.io/bezierjs/#intersect-curve,
|
234
|
+
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
|
235
|
+
const firstTPair = intersection[0];
|
236
|
+
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
|
237
|
+
|
238
|
+
if (!match) {
|
239
|
+
throw new Error(
|
240
|
+
`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`
|
241
|
+
);
|
242
|
+
}
|
243
|
+
|
244
|
+
const t = parseFloat(match[1]);
|
245
|
+
return Vec2.ofXY(curve1.get(t));
|
246
|
+
};
|
247
|
+
|
248
|
+
const getExitDirection = (curve: Bezier): Vec2 => {
|
249
|
+
return Vec2.ofXY(curve.points[2]).minus(Vec2.ofXY(curve.points[1])).normalized();
|
250
|
+
};
|
251
|
+
|
252
|
+
const getEnterDirection = (curve: Bezier): Vec2 => {
|
253
|
+
return Vec2.ofXY(curve.points[1]).minus(Vec2.ofXY(curve.points[0])).normalized();
|
254
|
+
};
|
255
|
+
|
256
|
+
// Prevent
|
257
|
+
// /
|
258
|
+
// / /
|
259
|
+
// / / /|
|
260
|
+
// / / |
|
261
|
+
// / |
|
262
|
+
// where the next stroke and the previous stroke are in different directions.
|
263
|
+
//
|
264
|
+
// Are the exit/enter directions of the previous and current curves in different enough directions?
|
265
|
+
if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.3
|
266
|
+
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.3
|
267
|
+
|
268
|
+
// Also handle if the curves exit/enter directions differ
|
269
|
+
|| getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
|
270
|
+
|| getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
|
271
|
+
return true;
|
272
|
+
}
|
273
|
+
|
274
|
+
// Check whether the lower curve intersects the other wall:
|
275
|
+
// / / ← lower
|
276
|
+
// / / /
|
277
|
+
// / / /
|
278
|
+
// //
|
279
|
+
// / /
|
280
|
+
const lowerIntersection = getIntersection(lowerCurve, this.lastUpperBezier);
|
281
|
+
const upperIntersection = getIntersection(upperCurve, this.lastLowerBezier);
|
282
|
+
if (lowerIntersection || upperIntersection) {
|
283
|
+
return true;
|
284
|
+
}
|
285
|
+
|
286
|
+
return false;
|
287
|
+
}
|
288
|
+
|
192
289
|
// Returns the distance between the start, control, and end points of the curve.
|
193
290
|
private approxCurrentCurveLength() {
|
194
291
|
if (!this.currentCurve) {
|
@@ -257,9 +354,23 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
257
354
|
return;
|
258
355
|
}
|
259
356
|
|
260
|
-
const {
|
357
|
+
const {
|
358
|
+
upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand,
|
359
|
+
lowerCurve, upperCurve,
|
360
|
+
} = this.currentSegmentToPath();
|
261
361
|
|
262
|
-
|
362
|
+
const shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
|
363
|
+
if (shouldStartNew) {
|
364
|
+
const part = this.previewCurrentPath();
|
365
|
+
|
366
|
+
if (part) {
|
367
|
+
this.parts.push(part);
|
368
|
+
this.upperSegments = [];
|
369
|
+
this.lowerSegments = [];
|
370
|
+
}
|
371
|
+
}
|
372
|
+
|
373
|
+
if (this.isFirstSegment || shouldStartNew) {
|
263
374
|
// We draw the upper path (reversed), then the lower path, so we need the
|
264
375
|
// upperToLowerConnector to join the two paths.
|
265
376
|
this.pathStartConnector = upperToLowerConnector;
|
@@ -269,8 +380,11 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
269
380
|
// upperPath:
|
270
381
|
this.mostRecentConnector = lowerToUpperConnector;
|
271
382
|
|
272
|
-
this.
|
273
|
-
this.
|
383
|
+
this.lowerSegments.push(lowerCurveCommand);
|
384
|
+
this.upperSegments.push(upperCurveCommand);
|
385
|
+
|
386
|
+
this.lastLowerBezier = lowerCurve;
|
387
|
+
this.lastUpperBezier = upperCurve;
|
274
388
|
|
275
389
|
const lastPoint = this.buffer[this.buffer.length - 1];
|
276
390
|
this.lastExitingVec = Vec2.ofXY(
|
@@ -325,12 +439,14 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
325
439
|
);
|
326
440
|
|
327
441
|
// Each starts at startPt ± startVec
|
442
|
+
const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
|
328
443
|
const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
|
329
444
|
const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
|
330
445
|
const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
|
331
446
|
const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
|
447
|
+
const upperCurveEndPoint = this.roundPoint(startPt.minus(startVec));
|
332
448
|
|
333
|
-
const
|
449
|
+
const lowerCurveCommand: QuadraticBezierPathCommand = {
|
334
450
|
kind: PathCommandType.QuadraticBezierTo,
|
335
451
|
controlPoint: lowerCurveControlPoint,
|
336
452
|
endPoint: lowerCurveEndPoint,
|
@@ -339,7 +455,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
339
455
|
// From the end of the upperCurve to the start of the lowerCurve:
|
340
456
|
const upperToLowerConnector: LinePathCommand = {
|
341
457
|
kind: PathCommandType.LineTo,
|
342
|
-
point:
|
458
|
+
point: lowerCurveStartPoint,
|
343
459
|
};
|
344
460
|
|
345
461
|
// From the end of lowerCurve to the start of upperCurve:
|
@@ -348,13 +464,19 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
348
464
|
point: upperCurveStartPoint,
|
349
465
|
};
|
350
466
|
|
351
|
-
const
|
467
|
+
const upperCurveCommand: QuadraticBezierPathCommand = {
|
352
468
|
kind: PathCommandType.QuadraticBezierTo,
|
353
469
|
controlPoint: upperCurveControlPoint,
|
354
|
-
endPoint:
|
470
|
+
endPoint: upperCurveEndPoint,
|
355
471
|
};
|
356
472
|
|
357
|
-
|
473
|
+
const upperCurve = new Bezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
|
474
|
+
const lowerCurve = new Bezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
|
475
|
+
|
476
|
+
return {
|
477
|
+
upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
|
478
|
+
upperCurve, lowerCurve,
|
479
|
+
};
|
358
480
|
}
|
359
481
|
|
360
482
|
// Compute the direction of the velocity at the end of this.buffer
|
@@ -397,6 +519,11 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
397
519
|
const prevEndWidth = this.curveEndWidth;
|
398
520
|
this.curveEndWidth = pointRadius;
|
399
521
|
|
522
|
+
if (this.isFirstSegment) {
|
523
|
+
// The start of a curve often lacks accurate pressure information. Update it.
|
524
|
+
this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
|
525
|
+
}
|
526
|
+
|
400
527
|
// recompute bbox
|
401
528
|
this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
|
402
529
|
|
@@ -413,6 +540,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
413
540
|
console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
|
414
541
|
}
|
415
542
|
|
543
|
+
// If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
|
416
544
|
let enteringVec = this.lastExitingVec;
|
417
545
|
if (!enteringVec) {
|
418
546
|
let sampleIdx = Math.ceil(this.buffer.length / 2);
|
package/src/components/lib.ts
CHANGED
@@ -4,14 +4,14 @@ export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
|
|
4
4
|
export * from './AbstractComponent';
|
5
5
|
export { default as AbstractComponent } from './AbstractComponent';
|
6
6
|
import Stroke from './Stroke';
|
7
|
-
import
|
7
|
+
import TextComponent from './Text';
|
8
8
|
import ImageComponent from './ImageComponent';
|
9
9
|
|
10
10
|
export {
|
11
11
|
Stroke,
|
12
|
-
Text,
|
12
|
+
TextComponent as Text,
|
13
13
|
|
14
|
-
|
14
|
+
TextComponent as TextComponent,
|
15
15
|
Stroke as StrokeComponent,
|
16
16
|
ImageComponent,
|
17
17
|
};
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
-
import
|
2
|
+
import TextComponent, { TextStyle } from '../../components/Text';
|
3
3
|
import Mat33 from '../../math/Mat33';
|
4
4
|
import Rect2 from '../../math/Rect2';
|
5
5
|
import { Point2, Vec2 } from '../../math/Vec2';
|
@@ -153,7 +153,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
153
153
|
this.ctx.save();
|
154
154
|
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
155
155
|
this.transformBy(transform);
|
156
|
-
|
156
|
+
TextComponent.applyTextStyles(this.ctx, style);
|
157
157
|
|
158
158
|
if (style.renderingStyle.fill.a !== 0) {
|
159
159
|
this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
|
@@ -100,39 +100,64 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
100
100
|
}
|
101
101
|
|
102
102
|
// Apply [elemTransform] to [elem].
|
103
|
-
private transformFrom(elemTransform: Mat33, elem: SVGElement) {
|
104
|
-
let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
|
103
|
+
private transformFrom(elemTransform: Mat33, elem: SVGElement, inCanvasSpace: boolean = false) {
|
104
|
+
let transform = !inCanvasSpace ? this.getCanvasToScreenTransform().rightMul(elemTransform) : elemTransform;
|
105
105
|
const translation = transform.transformVec2(Vec2.zero);
|
106
106
|
transform = transform.rightMul(Mat33.translation(translation.times(-1)));
|
107
107
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
108
|
+
if (!transform.eq(Mat33.identity)) {
|
109
|
+
elem.style.transform = `matrix(
|
110
|
+
${transform.a1}, ${transform.b1},
|
111
|
+
${transform.a2}, ${transform.b2},
|
112
|
+
${transform.a3}, ${transform.b3}
|
113
|
+
)`;
|
114
|
+
} else {
|
115
|
+
elem.style.transform = '';
|
116
|
+
}
|
117
|
+
|
113
118
|
elem.setAttribute('x', `${toRoundedString(translation.x)}`);
|
114
119
|
elem.setAttribute('y', `${toRoundedString(translation.y)}`);
|
115
120
|
}
|
116
121
|
|
122
|
+
private textContainer: SVGTextElement|null = null;
|
123
|
+
private textContainerTransform: Mat33|null = null;
|
117
124
|
public drawText(text: string, transform: Mat33, style: TextStyle): void {
|
118
|
-
const
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
125
|
+
const applyTextStyles = (elem: SVGTextElement|SVGTSpanElement, style: TextStyle) => {
|
126
|
+
elem.style.fontFamily = style.fontFamily;
|
127
|
+
elem.style.fontVariant = style.fontVariant ?? '';
|
128
|
+
elem.style.fontWeight = style.fontWeight ?? '';
|
129
|
+
elem.style.fontSize = style.size + 'px';
|
130
|
+
elem.style.fill = style.renderingStyle.fill.toHexString();
|
131
|
+
|
132
|
+
if (style.renderingStyle.stroke) {
|
133
|
+
const strokeStyle = style.renderingStyle.stroke;
|
134
|
+
elem.style.stroke = strokeStyle.color.toHexString();
|
135
|
+
elem.style.strokeWidth = strokeStyle.width + 'px';
|
136
|
+
}
|
137
|
+
};
|
138
|
+
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
139
|
+
|
140
|
+
if (!this.textContainer) {
|
141
|
+
const container = document.createElementNS(svgNameSpace, 'text');
|
142
|
+
container.appendChild(document.createTextNode(text));
|
143
|
+
this.transformFrom(transform, container, true);
|
144
|
+
applyTextStyles(container, style);
|
145
|
+
|
146
|
+
this.elem.appendChild(container);
|
147
|
+
this.objectElems?.push(container);
|
148
|
+
if (this.objectLevel > 0) {
|
149
|
+
this.textContainer = container;
|
150
|
+
this.textContainerTransform = transform;
|
151
|
+
}
|
152
|
+
} else {
|
153
|
+
const elem = document.createElementNS(svgNameSpace, 'tspan');
|
154
|
+
elem.appendChild(document.createTextNode(text));
|
155
|
+
this.textContainer.appendChild(elem);
|
133
156
|
|
134
|
-
|
135
|
-
|
157
|
+
transform = this.textContainerTransform!.inverse().rightMul(transform);
|
158
|
+
this.transformFrom(transform, elem, true);
|
159
|
+
applyTextStyles(elem, style);
|
160
|
+
}
|
136
161
|
}
|
137
162
|
|
138
163
|
public drawImage(image: RenderableImage) {
|
@@ -153,6 +178,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
153
178
|
// Only accumulate a path within an object
|
154
179
|
this.lastPathString = [];
|
155
180
|
this.lastPathStyle = null;
|
181
|
+
this.textContainer = null;
|
156
182
|
this.objectElems = [];
|
157
183
|
}
|
158
184
|
|