js-draw 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +21 -12
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +2 -1
- package/dist/src/Editor.js +24 -6
- package/dist/src/EditorImage.js +3 -0
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +11 -0
- package/dist/src/SVGLoader.js +113 -4
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +12 -2
- package/dist/src/components/AbstractComponent.d.ts +6 -0
- package/dist/src/components/AbstractComponent.js +11 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/Text.d.ts +30 -0
- package/dist/src/components/Text.js +111 -0
- package/dist/src/components/localization.d.ts +1 -0
- package/dist/src/components/localization.js +1 -0
- package/dist/src/geometry/Mat33.d.ts +1 -0
- package/dist/src/geometry/Mat33.js +30 -0
- package/dist/src/geometry/Path.js +105 -67
- package/dist/src/geometry/Rect2.d.ts +2 -0
- package/dist/src/geometry/Rect2.js +6 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
- package/dist/src/testing/loadExpectExtensions.js +1 -4
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +242 -154
- package/dist/src/toolbar/icons.d.ts +12 -0
- package/dist/src/toolbar/icons.js +198 -0
- package/dist/src/toolbar/localization.d.ts +5 -1
- package/dist/src/toolbar/localization.js +5 -1
- package/dist/src/toolbar/types.d.ts +4 -0
- package/dist/src/tools/PanZoom.d.ts +9 -6
- package/dist/src/tools/PanZoom.js +30 -21
- package/dist/src/tools/Pen.js +8 -3
- package/dist/src/tools/SelectionTool.js +1 -1
- package/dist/src/tools/TextTool.d.ts +30 -0
- package/dist/src/tools/TextTool.js +173 -0
- package/dist/src/tools/ToolController.d.ts +5 -5
- package/dist/src/tools/ToolController.js +10 -9
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist-test/test-dist-bundle.html +8 -1
- package/package.json +1 -1
- package/src/Editor.css +2 -0
- package/src/Editor.ts +26 -7
- package/src/EditorImage.ts +4 -0
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +146 -5
- package/src/Viewport.ts +15 -3
- package/src/components/AbstractComponent.ts +16 -1
- package/src/components/SVGGlobalAttributesObject.ts +0 -1
- package/src/components/Stroke.ts +1 -1
- package/src/components/Text.ts +140 -0
- package/src/components/localization.ts +2 -0
- package/src/geometry/Mat33.test.ts +44 -0
- package/src/geometry/Mat33.ts +41 -0
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.toString.test.ts +7 -3
- package/src/geometry/Path.ts +110 -68
- package/src/geometry/Rect2.ts +8 -0
- package/src/rendering/renderers/AbstractRenderer.ts +18 -1
- package/src/rendering/renderers/CanvasRenderer.ts +34 -10
- package/src/rendering/renderers/DummyRenderer.ts +8 -0
- package/src/rendering/renderers/SVGRenderer.ts +57 -10
- package/src/testing/loadExpectExtensions.ts +1 -4
- package/src/toolbar/HTMLToolbar.ts +294 -170
- package/src/toolbar/icons.ts +227 -0
- package/src/toolbar/localization.ts +11 -2
- package/src/toolbar/toolbar.css +27 -11
- package/src/toolbar/types.ts +5 -0
- package/src/tools/PanZoom.ts +37 -27
- package/src/tools/Pen.ts +7 -3
- package/src/tools/SelectionTool.ts +1 -1
- package/src/tools/TextTool.ts +225 -0
- package/src/tools/ToolController.ts +7 -5
- package/src/tools/localization.ts +7 -0
@@ -186,5 +186,35 @@ export default class Mat33 {
|
|
186
186
|
// Translate such that [center] goes to (0, 0)
|
187
187
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
188
188
|
}
|
189
|
+
// Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
|
190
|
+
static fromCSSMatrix(cssString) {
|
191
|
+
if (cssString === '' || cssString === 'none') {
|
192
|
+
return Mat33.identity;
|
193
|
+
}
|
194
|
+
const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
|
195
|
+
const numberSepExp = '[, \\t\\n]';
|
196
|
+
const regExpSource = `^\\s*matrix\\s*\\(${[
|
197
|
+
// According to MDN, matrix(a,b,c,d,e,f) has form:
|
198
|
+
// ⎡ a c e ⎤
|
199
|
+
// ⎢ b d f ⎥
|
200
|
+
// ⎣ 0 0 1 ⎦
|
201
|
+
numberExp, numberExp, numberExp,
|
202
|
+
numberExp, numberExp, numberExp, // b, d, f
|
203
|
+
].join(`${numberSepExp}+`)}${numberSepExp}*\\)\\s*$`;
|
204
|
+
const matrixExp = new RegExp(regExpSource, 'i');
|
205
|
+
const match = matrixExp.exec(cssString);
|
206
|
+
if (!match) {
|
207
|
+
throw new Error(`Unsupported transformation: ${cssString}`);
|
208
|
+
}
|
209
|
+
const matrixData = match.slice(1).map(entry => parseFloat(entry));
|
210
|
+
const a = matrixData[0];
|
211
|
+
const b = matrixData[1];
|
212
|
+
const c = matrixData[2];
|
213
|
+
const d = matrixData[3];
|
214
|
+
const e = matrixData[4];
|
215
|
+
const f = matrixData[5];
|
216
|
+
const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
|
217
|
+
return transform;
|
218
|
+
}
|
189
219
|
}
|
190
220
|
Mat33.identity = new Mat33(1, 0, 0, 0, 1, 0, 0, 0, 1);
|
@@ -220,6 +220,7 @@ export default class Path {
|
|
220
220
|
const lastDigit = parseInt(text.charAt(text.length - 1), 10);
|
221
221
|
const postDecimal = parseInt(roundingDownMatch[3], 10);
|
222
222
|
const preDecimal = parseInt(roundingDownMatch[2], 10);
|
223
|
+
const origPostDecimalString = roundingDownMatch[3];
|
223
224
|
let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
|
224
225
|
let carry = 0;
|
225
226
|
if (newPostDecimal.length > postDecimal.toString().length) {
|
@@ -227,11 +228,17 @@ export default class Path {
|
|
227
228
|
newPostDecimal = newPostDecimal.substring(1);
|
228
229
|
carry = 1;
|
229
230
|
}
|
231
|
+
// parseInt(...).toString() removes leading zeroes. Add them back.
|
232
|
+
while (newPostDecimal.length < origPostDecimalString.length) {
|
233
|
+
newPostDecimal = carry.toString(10) + newPostDecimal;
|
234
|
+
carry = 0;
|
235
|
+
}
|
230
236
|
text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
|
231
237
|
}
|
232
238
|
text = text.replace(fixRoundingUpExp, '$1');
|
233
239
|
// Remove trailing zeroes
|
234
|
-
text = text.replace(/([.][^0]
|
240
|
+
text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
|
241
|
+
text = text.replace(/[.]0+$/, '.');
|
235
242
|
// Remove trailing period
|
236
243
|
return text.replace(/[.]$/, '');
|
237
244
|
};
|
@@ -275,10 +282,12 @@ export default class Path {
|
|
275
282
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
|
276
283
|
// and
|
277
284
|
// https://www.w3.org/TR/SVG2/paths.html
|
285
|
+
var _a;
|
278
286
|
// Remove linebreaks
|
279
287
|
pathString = pathString.split('\n').join(' ');
|
280
288
|
let lastPos = Vec2.zero;
|
281
289
|
let firstPos = null;
|
290
|
+
let startPos = null;
|
282
291
|
let isFirstCommand = true;
|
283
292
|
const commands = [];
|
284
293
|
const moveTo = (point) => {
|
@@ -317,15 +326,61 @@ export default class Path {
|
|
317
326
|
endPoint,
|
318
327
|
});
|
319
328
|
};
|
329
|
+
const commandArgCounts = {
|
330
|
+
'm': 1,
|
331
|
+
'l': 1,
|
332
|
+
'c': 3,
|
333
|
+
'q': 2,
|
334
|
+
'z': 0,
|
335
|
+
'h': 1,
|
336
|
+
'v': 1,
|
337
|
+
};
|
320
338
|
// Each command: Command character followed by anything that isn't a command character
|
321
|
-
const commandExp = /([
|
339
|
+
const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
|
322
340
|
let current;
|
323
341
|
while ((current = commandExp.exec(pathString)) !== null) {
|
324
|
-
const argParts = current[2].trim().split(/[^0-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
342
|
+
const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(part => part.length > 0).reduce((accumualtor, current) => {
|
343
|
+
// As of 09/2022, iOS Safari doesn't support support lookbehind in regular
|
344
|
+
// expressions. As such, we need an alternative.
|
345
|
+
// Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
|
346
|
+
// we need special cases:
|
347
|
+
current = current.replace(/([^eE])[-]/g, '$1 -');
|
348
|
+
const parts = current.split(' -');
|
349
|
+
if (parts[0] !== '') {
|
350
|
+
accumualtor.push(parts[0]);
|
351
|
+
}
|
352
|
+
accumualtor.push(...parts.slice(1).map(part => `-${part}`));
|
353
|
+
return accumualtor;
|
354
|
+
}, []);
|
355
|
+
let numericArgs = argParts.map(arg => parseFloat(arg));
|
356
|
+
let commandChar = current[1].toLowerCase();
|
357
|
+
let uppercaseCommand = current[1] !== commandChar;
|
358
|
+
// Convert commands that don't take points into commands that do.
|
359
|
+
if (commandChar === 'v' || commandChar === 'h') {
|
360
|
+
numericArgs = numericArgs.reduce((accumulator, current) => {
|
361
|
+
if (commandChar === 'v') {
|
362
|
+
return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
|
363
|
+
}
|
364
|
+
else {
|
365
|
+
return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
|
366
|
+
}
|
367
|
+
}, []);
|
368
|
+
commandChar = 'l';
|
369
|
+
}
|
370
|
+
else if (commandChar === 'z') {
|
371
|
+
if (firstPos) {
|
372
|
+
numericArgs = [firstPos.x, firstPos.y];
|
373
|
+
firstPos = lastPos;
|
374
|
+
}
|
375
|
+
else {
|
376
|
+
continue;
|
377
|
+
}
|
378
|
+
// 'z' always acts like an uppercase lineTo(startPos)
|
379
|
+
uppercaseCommand = true;
|
380
|
+
commandChar = 'l';
|
381
|
+
}
|
382
|
+
const commandArgCount = (_a = commandArgCounts[commandChar]) !== null && _a !== void 0 ? _a : 0;
|
383
|
+
const allArgs = numericArgs.reduce((accumulator, current, index, parts) => {
|
329
384
|
if (index % 2 !== 0) {
|
330
385
|
const currentAsFloat = current;
|
331
386
|
const prevAsFloat = parts[index - 1];
|
@@ -334,76 +389,59 @@ export default class Path {
|
|
334
389
|
else {
|
335
390
|
return accumulator;
|
336
391
|
}
|
337
|
-
}, []).map((coordinate) => {
|
392
|
+
}, []).map((coordinate, index) => {
|
338
393
|
// Lowercase commands are relative, uppercase commands use absolute
|
339
394
|
// positioning
|
395
|
+
let newPos;
|
340
396
|
if (uppercaseCommand) {
|
341
|
-
|
342
|
-
return coordinate;
|
397
|
+
newPos = coordinate;
|
343
398
|
}
|
344
399
|
else {
|
345
|
-
|
346
|
-
|
400
|
+
newPos = lastPos.plus(coordinate);
|
401
|
+
}
|
402
|
+
if ((index + 1) % commandArgCount === 0) {
|
403
|
+
lastPos = newPos;
|
347
404
|
}
|
405
|
+
return newPos;
|
348
406
|
});
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
case 'l':
|
356
|
-
expectedPointArgCount = 1;
|
357
|
-
lineTo(args[0]);
|
358
|
-
break;
|
359
|
-
case 'z':
|
360
|
-
expectedPointArgCount = 0;
|
361
|
-
// firstPos can be null if the stroke data is just 'z'.
|
362
|
-
if (firstPos) {
|
363
|
-
lineTo(firstPos);
|
364
|
-
}
|
365
|
-
break;
|
366
|
-
case 'c':
|
367
|
-
expectedPointArgCount = 3;
|
368
|
-
cubicBezierTo(args[0], args[1], args[2]);
|
369
|
-
break;
|
370
|
-
case 'q':
|
371
|
-
expectedPointArgCount = 2;
|
372
|
-
quadraticBeierTo(args[0], args[1]);
|
373
|
-
break;
|
374
|
-
// Horizontal line
|
375
|
-
case 'h':
|
376
|
-
expectedPointArgCount = 0;
|
377
|
-
if (uppercaseCommand) {
|
378
|
-
lineTo(Vec2.of(numericArgs[0], lastPos.y));
|
379
|
-
}
|
380
|
-
else {
|
381
|
-
lineTo(lastPos.plus(Vec2.of(numericArgs[0], 0)));
|
382
|
-
}
|
383
|
-
break;
|
384
|
-
// Vertical line
|
385
|
-
case 'v':
|
386
|
-
expectedPointArgCount = 0;
|
387
|
-
if (uppercaseCommand) {
|
388
|
-
lineTo(Vec2.of(lastPos.x, numericArgs[1]));
|
389
|
-
}
|
390
|
-
else {
|
391
|
-
lineTo(lastPos.plus(Vec2.of(0, numericArgs[1])));
|
392
|
-
}
|
393
|
-
break;
|
394
|
-
default:
|
395
|
-
throw new Error(`Unknown path command ${commandChar}`);
|
407
|
+
if (allArgs.length % commandArgCount !== 0) {
|
408
|
+
throw new Error([
|
409
|
+
`Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
|
410
|
+
`The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
|
411
|
+
`Command: ${current[0]}`,
|
412
|
+
].join('\n'));
|
396
413
|
}
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
414
|
+
for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
|
415
|
+
const args = allArgs.slice(argPos, argPos + commandArgCount);
|
416
|
+
switch (commandChar.toLowerCase()) {
|
417
|
+
case 'm':
|
418
|
+
if (argPos === 0) {
|
419
|
+
moveTo(args[0]);
|
420
|
+
}
|
421
|
+
else {
|
422
|
+
lineTo(args[0]);
|
423
|
+
}
|
424
|
+
break;
|
425
|
+
case 'l':
|
426
|
+
lineTo(args[0]);
|
427
|
+
break;
|
428
|
+
case 'c':
|
429
|
+
cubicBezierTo(args[0], args[1], args[2]);
|
430
|
+
break;
|
431
|
+
case 'q':
|
432
|
+
quadraticBeierTo(args[0], args[1]);
|
433
|
+
break;
|
434
|
+
default:
|
435
|
+
throw new Error(`Unknown path command ${commandChar}`);
|
436
|
+
}
|
437
|
+
isFirstCommand = false;
|
401
438
|
}
|
402
|
-
if (
|
403
|
-
firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos =
|
439
|
+
if (allArgs.length > 0) {
|
440
|
+
firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = allArgs[0]);
|
441
|
+
startPos !== null && startPos !== void 0 ? startPos : (startPos = firstPos);
|
442
|
+
lastPos = allArgs[allArgs.length - 1];
|
404
443
|
}
|
405
|
-
isFirstCommand = false;
|
406
444
|
}
|
407
|
-
return new Path(
|
445
|
+
return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
|
408
446
|
}
|
409
447
|
}
|
@@ -34,6 +34,8 @@ export default class Rect2 {
|
|
34
34
|
get maxDimension(): number;
|
35
35
|
get topRight(): import("./Vec3").default;
|
36
36
|
get bottomLeft(): import("./Vec3").default;
|
37
|
+
get width(): number;
|
38
|
+
get height(): number;
|
37
39
|
getEdges(): LineSegment2[];
|
38
40
|
transformedBoundingBox(affineTransform: Mat33): Rect2;
|
39
41
|
/** @return true iff this is equal to [other] ± fuzz */
|
@@ -126,6 +126,12 @@ export default class Rect2 {
|
|
126
126
|
get bottomLeft() {
|
127
127
|
return this.topLeft.plus(Vec2.of(0, this.h));
|
128
128
|
}
|
129
|
+
get width() {
|
130
|
+
return this.w;
|
131
|
+
}
|
132
|
+
get height() {
|
133
|
+
return this.h;
|
134
|
+
}
|
129
135
|
// Returns edges in the order
|
130
136
|
// [ rightEdge, topEdge, leftEdge, bottomEdge ]
|
131
137
|
getEdges() {
|
@@ -1,4 +1,6 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
+
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
3
|
+
import { TextStyle } from '../../components/Text';
|
2
4
|
import Mat33 from '../../geometry/Mat33';
|
3
5
|
import { PathCommand } from '../../geometry/Path';
|
4
6
|
import Rect2 from '../../geometry/Rect2';
|
@@ -19,6 +21,7 @@ export interface RenderablePathSpec {
|
|
19
21
|
export default abstract class AbstractRenderer {
|
20
22
|
private viewport;
|
21
23
|
private selfTransform;
|
24
|
+
private transformStack;
|
22
25
|
protected constructor(viewport: Viewport);
|
23
26
|
protected getViewport(): Viewport;
|
24
27
|
abstract displaySize(): Vec2;
|
@@ -29,6 +32,7 @@ export default abstract class AbstractRenderer {
|
|
29
32
|
protected abstract moveTo(point: Point2): void;
|
30
33
|
protected abstract traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
31
34
|
protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
|
35
|
+
abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
|
32
36
|
abstract isTooSmallToRender(rect: Rect2): boolean;
|
33
37
|
setDraftMode(_draftMode: boolean): void;
|
34
38
|
protected objectLevel: number;
|
@@ -37,12 +41,14 @@ export default abstract class AbstractRenderer {
|
|
37
41
|
drawPath(path: RenderablePathSpec): void;
|
38
42
|
drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
|
39
43
|
startObject(_boundingBox: Rect2, _clip?: boolean): void;
|
40
|
-
endObject(): void;
|
44
|
+
endObject(_loaderData?: LoadSaveDataTable): void;
|
41
45
|
protected getNestingLevel(): number;
|
42
46
|
abstract drawPoints(...points: Point2[]): void;
|
43
47
|
canRenderFromWithoutDataLoss(_other: AbstractRenderer): boolean;
|
44
48
|
renderFromOtherOfSameType(_renderTo: Mat33, other: AbstractRenderer): void;
|
45
49
|
setTransform(transform: Mat33 | null): void;
|
50
|
+
pushTransform(transform: Mat33): void;
|
51
|
+
popTransform(): void;
|
46
52
|
getCanvasToScreenTransform(): Mat33;
|
47
53
|
canvasToScreen(vec: Vec2): Vec2;
|
48
54
|
getSizeOfCanvasPixelOnScreen(): number;
|
@@ -11,6 +11,7 @@ export default class AbstractRenderer {
|
|
11
11
|
this.viewport = viewport;
|
12
12
|
// If null, this' transformation is linked to the Viewport
|
13
13
|
this.selfTransform = null;
|
14
|
+
this.transformStack = [];
|
14
15
|
this.objectLevel = 0;
|
15
16
|
this.currentPaths = null;
|
16
17
|
}
|
@@ -79,7 +80,7 @@ export default class AbstractRenderer {
|
|
79
80
|
this.currentPaths = [];
|
80
81
|
this.objectLevel++;
|
81
82
|
}
|
82
|
-
endObject() {
|
83
|
+
endObject(_loaderData) {
|
83
84
|
// Render the paths all at once
|
84
85
|
this.flushPath();
|
85
86
|
this.currentPaths = null;
|
@@ -104,6 +105,17 @@ export default class AbstractRenderer {
|
|
104
105
|
setTransform(transform) {
|
105
106
|
this.selfTransform = transform;
|
106
107
|
}
|
108
|
+
pushTransform(transform) {
|
109
|
+
this.transformStack.push(this.selfTransform);
|
110
|
+
this.setTransform(this.getCanvasToScreenTransform().rightMul(transform));
|
111
|
+
}
|
112
|
+
popTransform() {
|
113
|
+
var _a;
|
114
|
+
if (this.transformStack.length === 0) {
|
115
|
+
throw new Error('Unable to pop more transforms than have been pushed!');
|
116
|
+
}
|
117
|
+
this.setTransform((_a = this.transformStack.pop()) !== null && _a !== void 0 ? _a : null);
|
118
|
+
}
|
107
119
|
// Get the matrix that transforms a vector on the canvas to a vector on this'
|
108
120
|
// rendering target.
|
109
121
|
getCanvasToScreenTransform() {
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { TextStyle } from '../../components/Text';
|
1
2
|
import Mat33 from '../../geometry/Mat33';
|
2
3
|
import Rect2 from '../../geometry/Rect2';
|
3
4
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
@@ -12,6 +13,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
12
13
|
private minRenderSizeAnyDimen;
|
13
14
|
private minRenderSizeBothDimens;
|
14
15
|
constructor(ctx: CanvasRenderingContext2D, viewport: Viewport);
|
16
|
+
private transformBy;
|
15
17
|
canRenderFromWithoutDataLoss(other: AbstractRenderer): boolean;
|
16
18
|
renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void;
|
17
19
|
setDraftMode(draftMode: boolean): void;
|
@@ -24,6 +26,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
24
26
|
protected traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
25
27
|
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
26
28
|
drawPath(path: RenderablePathSpec): void;
|
29
|
+
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
27
30
|
private clipLevels;
|
28
31
|
startObject(boundingBox: Rect2, clip: boolean): void;
|
29
32
|
endObject(): void;
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
+
import Text from '../../components/Text';
|
2
3
|
import { Vec2 } from '../../geometry/Vec2';
|
3
4
|
import AbstractRenderer from './AbstractRenderer';
|
4
5
|
export default class CanvasRenderer extends AbstractRenderer {
|
@@ -10,6 +11,16 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
10
11
|
this.clipLevels = [];
|
11
12
|
this.setDraftMode(false);
|
12
13
|
}
|
14
|
+
transformBy(transformBy) {
|
15
|
+
// From MDN, transform(a,b,c,d,e,f)
|
16
|
+
// takes input such that
|
17
|
+
// ⎡ a c e ⎤
|
18
|
+
// ⎢ b d f ⎥ transforms content drawn to [ctx].
|
19
|
+
// ⎣ 0 0 1 ⎦
|
20
|
+
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
|
21
|
+
transformBy.a2, transformBy.b2, // c, d
|
22
|
+
transformBy.a3, transformBy.b3);
|
23
|
+
}
|
13
24
|
canRenderFromWithoutDataLoss(other) {
|
14
25
|
return other instanceof CanvasRenderer;
|
15
26
|
}
|
@@ -19,14 +30,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
19
30
|
}
|
20
31
|
transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
|
21
32
|
this.ctx.save();
|
22
|
-
|
23
|
-
// takes input such that
|
24
|
-
// ⎡ a c e ⎤
|
25
|
-
// ⎢ b d f ⎥ transforms content drawn to [ctx].
|
26
|
-
// ⎣ 0 0 1 ⎦
|
27
|
-
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
|
28
|
-
transformBy.a2, transformBy.b2, // c, d
|
29
|
-
transformBy.a3, transformBy.b3);
|
33
|
+
this.transformBy(transformBy);
|
30
34
|
this.ctx.drawImage(other.ctx.canvas, 0, 0);
|
31
35
|
this.ctx.restore();
|
32
36
|
}
|
@@ -105,6 +109,22 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
105
109
|
}
|
106
110
|
super.drawPath(path);
|
107
111
|
}
|
112
|
+
drawText(text, transform, style) {
|
113
|
+
this.ctx.save();
|
114
|
+
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
115
|
+
this.transformBy(transform);
|
116
|
+
Text.applyTextStyles(this.ctx, style);
|
117
|
+
if (style.renderingStyle.fill.a !== 0) {
|
118
|
+
this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
|
119
|
+
this.ctx.fillText(text, 0, 0);
|
120
|
+
}
|
121
|
+
if (style.renderingStyle.stroke) {
|
122
|
+
this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
|
123
|
+
this.ctx.lineWidth = style.renderingStyle.stroke.width;
|
124
|
+
this.ctx.strokeText(text, 0, 0);
|
125
|
+
}
|
126
|
+
this.ctx.restore();
|
127
|
+
}
|
108
128
|
startObject(boundingBox, clip) {
|
109
129
|
if (this.isTooSmallToRender(boundingBox)) {
|
110
130
|
this.ignoreObjectsAboveLevel = this.getNestingLevel();
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { TextStyle } from '../../components/Text';
|
1
2
|
import Mat33 from '../../geometry/Mat33';
|
2
3
|
import Rect2 from '../../geometry/Rect2';
|
3
4
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
@@ -10,6 +11,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
10
11
|
lastFillStyle: RenderingStyle | null;
|
11
12
|
lastPoint: Point2 | null;
|
12
13
|
objectNestingLevel: number;
|
14
|
+
lastText: string | null;
|
13
15
|
pointBuffer: Point2[];
|
14
16
|
constructor(viewport: Viewport);
|
15
17
|
displaySize(): Vec2;
|
@@ -21,6 +23,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
21
23
|
protected traceCubicBezierCurve(p1: Vec3, p2: Vec3, p3: Vec3): void;
|
22
24
|
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
23
25
|
drawPoints(..._points: Vec3[]): void;
|
26
|
+
drawText(text: string, _transform: Mat33, _style: TextStyle): void;
|
24
27
|
startObject(boundingBox: Rect2, _clip: boolean): void;
|
25
28
|
endObject(): void;
|
26
29
|
isTooSmallToRender(_rect: Rect2): boolean;
|
@@ -10,6 +10,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
10
10
|
this.lastFillStyle = null;
|
11
11
|
this.lastPoint = null;
|
12
12
|
this.objectNestingLevel = 0;
|
13
|
+
this.lastText = null;
|
13
14
|
// List of points drawn since the last clear.
|
14
15
|
this.pointBuffer = [];
|
15
16
|
}
|
@@ -28,6 +29,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
28
29
|
this.clearedCount++;
|
29
30
|
this.renderedPathCount = 0;
|
30
31
|
this.pointBuffer = [];
|
32
|
+
this.lastText = null;
|
31
33
|
// Ensure all objects finished rendering
|
32
34
|
if (this.objectNestingLevel > 0) {
|
33
35
|
throw new Error(`Within an object while clearing! Nesting level: ${this.objectNestingLevel}`);
|
@@ -68,6 +70,9 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
68
70
|
// drawPoints is intended for debugging.
|
69
71
|
// As such, it is unlikely to be the target of automated tests.
|
70
72
|
}
|
73
|
+
drawText(text, _transform, _style) {
|
74
|
+
this.lastText = text;
|
75
|
+
}
|
71
76
|
startObject(boundingBox, _clip) {
|
72
77
|
super.startObject(boundingBox);
|
73
78
|
this.objectNestingLevel += 1;
|
@@ -1,3 +1,6 @@
|
|
1
|
+
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
2
|
+
import { TextStyle } from '../../components/Text';
|
3
|
+
import Mat33 from '../../geometry/Mat33';
|
1
4
|
import Rect2 from '../../geometry/Rect2';
|
2
5
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
3
6
|
import Viewport from '../../Viewport';
|
@@ -9,7 +12,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
9
12
|
private lastPathStyle;
|
10
13
|
private lastPath;
|
11
14
|
private lastPathStart;
|
12
|
-
private
|
15
|
+
private objectElems;
|
13
16
|
private overwrittenAttrs;
|
14
17
|
constructor(elem: SVGSVGElement, viewport: Viewport);
|
15
18
|
setRootSVGAttribute(name: string, value: string | null): void;
|
@@ -18,8 +21,9 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
18
21
|
protected beginPath(startPoint: Point2): void;
|
19
22
|
protected endPath(style: RenderingStyle): void;
|
20
23
|
private addPathToSVG;
|
24
|
+
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
21
25
|
startObject(boundingBox: Rect2): void;
|
22
|
-
endObject(): void;
|
26
|
+
endObject(loaderData?: LoadSaveDataTable): void;
|
23
27
|
protected lineTo(point: Point2): void;
|
24
28
|
protected moveTo(point: Point2): void;
|
25
29
|
protected traceCubicBezierCurve(controlPoint1: Point2, controlPoint2: Point2, endPoint: Point2): void;
|
@@ -1,11 +1,13 @@
|
|
1
1
|
import Path, { PathCommandType } from '../../geometry/Path';
|
2
2
|
import { Vec2 } from '../../geometry/Vec2';
|
3
|
+
import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader';
|
3
4
|
import AbstractRenderer from './AbstractRenderer';
|
4
5
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
5
6
|
export default class SVGRenderer extends AbstractRenderer {
|
6
7
|
constructor(elem, viewport) {
|
7
8
|
super(viewport);
|
8
9
|
this.elem = elem;
|
10
|
+
this.objectElems = null;
|
9
11
|
this.overwrittenAttrs = {};
|
10
12
|
this.clear();
|
11
13
|
}
|
@@ -26,7 +28,6 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
26
28
|
return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
|
27
29
|
}
|
28
30
|
clear() {
|
29
|
-
this.mainGroup = document.createElementNS(svgNameSpace, 'g');
|
30
31
|
// Restore all alltributes
|
31
32
|
for (const attrName in this.overwrittenAttrs) {
|
32
33
|
const value = this.overwrittenAttrs[attrName];
|
@@ -38,8 +39,6 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
38
39
|
}
|
39
40
|
}
|
40
41
|
this.overwrittenAttrs = {};
|
41
|
-
// Remove all children
|
42
|
-
this.elem.replaceChildren(this.mainGroup);
|
43
42
|
}
|
44
43
|
beginPath(startPoint) {
|
45
44
|
var _a;
|
@@ -72,6 +71,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
72
71
|
}
|
73
72
|
// Push [this.fullPath] to the SVG
|
74
73
|
addPathToSVG() {
|
74
|
+
var _a;
|
75
75
|
if (!this.lastPathStyle || !this.lastPath) {
|
76
76
|
return;
|
77
77
|
}
|
@@ -83,7 +83,31 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
83
83
|
pathElem.setAttribute('stroke', style.stroke.color.toHexString());
|
84
84
|
pathElem.setAttribute('stroke-width', style.stroke.width.toString());
|
85
85
|
}
|
86
|
-
this.
|
86
|
+
this.elem.appendChild(pathElem);
|
87
|
+
(_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(pathElem);
|
88
|
+
}
|
89
|
+
drawText(text, transform, style) {
|
90
|
+
var _a, _b, _c;
|
91
|
+
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
92
|
+
const textElem = document.createElementNS(svgNameSpace, 'text');
|
93
|
+
textElem.appendChild(document.createTextNode(text));
|
94
|
+
textElem.style.transform = `matrix(
|
95
|
+
${transform.a1}, ${transform.b1},
|
96
|
+
${transform.a2}, ${transform.b2},
|
97
|
+
${transform.a3}, ${transform.b3}
|
98
|
+
)`;
|
99
|
+
textElem.style.fontFamily = style.fontFamily;
|
100
|
+
textElem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
|
101
|
+
textElem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
|
102
|
+
textElem.style.fontSize = style.size + 'px';
|
103
|
+
textElem.style.fill = style.renderingStyle.fill.toHexString();
|
104
|
+
if (style.renderingStyle.stroke) {
|
105
|
+
const strokeStyle = style.renderingStyle.stroke;
|
106
|
+
textElem.style.stroke = strokeStyle.color.toHexString();
|
107
|
+
textElem.style.strokeWidth = strokeStyle.width + 'px';
|
108
|
+
}
|
109
|
+
this.elem.appendChild(textElem);
|
110
|
+
(_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
|
87
111
|
}
|
88
112
|
startObject(boundingBox) {
|
89
113
|
super.startObject(boundingBox);
|
@@ -91,11 +115,30 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
91
115
|
this.lastPath = null;
|
92
116
|
this.lastPathStart = null;
|
93
117
|
this.lastPathStyle = null;
|
118
|
+
this.objectElems = [];
|
94
119
|
}
|
95
|
-
endObject() {
|
96
|
-
|
120
|
+
endObject(loaderData) {
|
121
|
+
var _a;
|
122
|
+
super.endObject(loaderData);
|
97
123
|
// Don't extend paths across objects
|
98
124
|
this.addPathToSVG();
|
125
|
+
if (loaderData) {
|
126
|
+
// Restore any attributes unsupported by the app.
|
127
|
+
for (const elem of (_a = this.objectElems) !== null && _a !== void 0 ? _a : []) {
|
128
|
+
const attrs = loaderData[svgAttributesDataKey];
|
129
|
+
const styleAttrs = loaderData[svgStyleAttributesDataKey];
|
130
|
+
if (attrs) {
|
131
|
+
for (const [attr, value] of attrs) {
|
132
|
+
elem.setAttribute(attr, value);
|
133
|
+
}
|
134
|
+
}
|
135
|
+
if (styleAttrs) {
|
136
|
+
for (const attr of styleAttrs) {
|
137
|
+
elem.style.setProperty(attr.key, attr.value, attr.priority);
|
138
|
+
}
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
99
142
|
}
|
100
143
|
lineTo(point) {
|
101
144
|
point = this.canvasToScreen(point);
|
@@ -137,7 +180,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
137
180
|
elem.setAttribute('cx', `${point.x}`);
|
138
181
|
elem.setAttribute('cy', `${point.y}`);
|
139
182
|
elem.setAttribute('r', '15');
|
140
|
-
this.
|
183
|
+
this.elem.appendChild(elem);
|
141
184
|
});
|
142
185
|
}
|
143
186
|
// Renders a **copy** of the given element.
|
@@ -15,10 +15,7 @@ export const loadExpectExtensions = () => {
|
|
15
15
|
return {
|
16
16
|
pass,
|
17
17
|
message: () => {
|
18
|
-
|
19
|
-
return `Expected ${expected} not to .eq ${actual}. Options(${eqArgs})`;
|
20
|
-
}
|
21
|
-
return `Expected ${expected} to .eq ${actual}. Options(${eqArgs})`;
|
18
|
+
return `Expected ${pass ? '!' : ''}(${actual}).eq(${expected}). Options(${eqArgs})`;
|
22
19
|
},
|
23
20
|
};
|
24
21
|
},
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
2
|
import { ToolbarLocalization } from './localization';
|
3
|
+
import { ActionButtonIcon } from './types';
|
3
4
|
export default class HTMLToolbar {
|
4
5
|
private editor;
|
5
6
|
private localizationTable;
|
@@ -7,7 +8,7 @@ export default class HTMLToolbar {
|
|
7
8
|
private penTypes;
|
8
9
|
constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
|
9
10
|
setupColorPickers(): void;
|
10
|
-
addActionButton(
|
11
|
+
addActionButton(title: string | ActionButtonIcon, command: () => void, parent?: Element): HTMLButtonElement;
|
11
12
|
private addUndoRedoButtons;
|
12
13
|
addDefaultToolWidgets(): void;
|
13
14
|
addDefaultActionButtons(): void;
|