js-draw 0.1.1 → 0.1.2
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 +3 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +4 -0
- 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 +3 -0
- package/dist/src/SVGLoader.js +11 -1
- package/dist/src/Viewport.js +10 -0
- package/dist/src/components/AbstractComponent.d.ts +6 -0
- package/dist/src/components/AbstractComponent.js +11 -0
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/geometry/Path.js +97 -66
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +21 -7
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +165 -154
- package/dist/src/toolbar/icons.d.ts +10 -0
- package/dist/src/toolbar/icons.js +180 -0
- package/dist/src/toolbar/localization.d.ts +4 -1
- package/dist/src/toolbar/localization.js +4 -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/ToolController.d.ts +5 -6
- package/dist/src/tools/ToolController.js +8 -10
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/package.json +1 -1
- package/src/Editor.ts +4 -0
- package/src/EditorImage.ts +4 -0
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +25 -2
- package/src/Viewport.ts +13 -1
- package/src/components/AbstractComponent.ts +16 -1
- package/src/components/Stroke.ts +1 -1
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.ts +99 -67
- package/src/rendering/renderers/AbstractRenderer.ts +2 -1
- package/src/rendering/renderers/SVGRenderer.ts +22 -10
- package/src/toolbar/HTMLToolbar.ts +199 -170
- package/src/toolbar/icons.ts +203 -0
- package/src/toolbar/localization.ts +9 -2
- package/src/toolbar/toolbar.css +21 -8
- package/src/toolbar/types.ts +5 -0
- package/src/tools/PanZoom.ts +37 -27
- package/src/tools/Pen.ts +7 -3
- package/src/tools/ToolController.ts +3 -5
- package/src/tools/localization.ts +2 -0
package/src/geometry/Path.ts
CHANGED
@@ -371,6 +371,7 @@ export default class Path {
|
|
371
371
|
|
372
372
|
let lastPos: Point2 = Vec2.zero;
|
373
373
|
let firstPos: Point2|null = null;
|
374
|
+
let startPos: Point2|null = null;
|
374
375
|
let isFirstCommand: boolean = true;
|
375
376
|
const commands: PathCommand[] = [];
|
376
377
|
|
@@ -413,19 +414,67 @@ export default class Path {
|
|
413
414
|
endPoint,
|
414
415
|
});
|
415
416
|
};
|
417
|
+
const commandArgCounts: Record<string, number> = {
|
418
|
+
'm': 1,
|
419
|
+
'l': 1,
|
420
|
+
'c': 3,
|
421
|
+
'q': 2,
|
422
|
+
'z': 0,
|
423
|
+
'h': 1,
|
424
|
+
'v': 1,
|
425
|
+
};
|
416
426
|
|
417
427
|
// Each command: Command character followed by anything that isn't a command character
|
418
|
-
const commandExp = /([
|
428
|
+
const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
|
419
429
|
let current;
|
420
430
|
while ((current = commandExp.exec(pathString)) !== null) {
|
421
|
-
const argParts = current[2].trim().split(/[^0-
|
431
|
+
const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(
|
422
432
|
part => part.length > 0
|
423
|
-
)
|
424
|
-
|
433
|
+
).reduce((accumualtor: string[], current: string): string[] => {
|
434
|
+
// As of 09/2022, iOS Safari doesn't support support lookbehind in regular
|
435
|
+
// expressions. As such, we need an alternative.
|
436
|
+
// Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
|
437
|
+
// we need special cases:
|
438
|
+
current = current.replace(/([^eE])[-]/g, '$1 -');
|
439
|
+
const parts = current.split(' -');
|
440
|
+
if (parts[0] !== '') {
|
441
|
+
accumualtor.push(parts[0]);
|
442
|
+
}
|
443
|
+
accumualtor.push(...parts.slice(1).map(part => `-${part}`));
|
444
|
+
return accumualtor;
|
445
|
+
}, []);
|
446
|
+
|
447
|
+
let numericArgs = argParts.map(arg => parseFloat(arg));
|
448
|
+
|
449
|
+
let commandChar = current[1].toLowerCase();
|
450
|
+
let uppercaseCommand = current[1] !== commandChar;
|
451
|
+
|
452
|
+
// Convert commands that don't take points into commands that do.
|
453
|
+
if (commandChar === 'v' || commandChar === 'h') {
|
454
|
+
numericArgs = numericArgs.reduce((accumulator: number[], current: number): number[] => {
|
455
|
+
if (commandChar === 'v') {
|
456
|
+
return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
|
457
|
+
} else {
|
458
|
+
return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
|
459
|
+
}
|
460
|
+
}, []);
|
461
|
+
commandChar = 'l';
|
462
|
+
} else if (commandChar === 'z') {
|
463
|
+
if (firstPos) {
|
464
|
+
numericArgs = [ firstPos.x, firstPos.y ];
|
465
|
+
firstPos = lastPos;
|
466
|
+
} else {
|
467
|
+
continue;
|
468
|
+
}
|
469
|
+
|
470
|
+
// 'z' always acts like an uppercase lineTo(startPos)
|
471
|
+
uppercaseCommand = true;
|
472
|
+
commandChar = 'l';
|
473
|
+
}
|
474
|
+
|
425
475
|
|
426
|
-
const
|
427
|
-
const
|
428
|
-
const args = numericArgs.reduce((
|
476
|
+
const commandArgCount: number = commandArgCounts[commandChar] ?? 0;
|
477
|
+
const allArgs = numericArgs.reduce((
|
429
478
|
accumulator: Point2[], current, index, parts
|
430
479
|
): Point2[] => {
|
431
480
|
if (index % 2 !== 0) {
|
@@ -435,82 +484,65 @@ export default class Path {
|
|
435
484
|
} else {
|
436
485
|
return accumulator;
|
437
486
|
}
|
438
|
-
}, []).map((coordinate
|
487
|
+
}, []).map((coordinate, index): Point2 => {
|
439
488
|
// Lowercase commands are relative, uppercase commands use absolute
|
440
489
|
// positioning
|
490
|
+
let newPos;
|
441
491
|
if (uppercaseCommand) {
|
442
|
-
|
443
|
-
return coordinate;
|
492
|
+
newPos = coordinate;
|
444
493
|
} else {
|
445
|
-
|
446
|
-
return lastPos;
|
494
|
+
newPos = lastPos.plus(coordinate);
|
447
495
|
}
|
448
|
-
});
|
449
|
-
|
450
|
-
let expectedPointArgCount;
|
451
496
|
|
452
|
-
|
453
|
-
|
454
|
-
expectedPointArgCount = 1;
|
455
|
-
moveTo(args[0]);
|
456
|
-
break;
|
457
|
-
case 'l':
|
458
|
-
expectedPointArgCount = 1;
|
459
|
-
lineTo(args[0]);
|
460
|
-
break;
|
461
|
-
case 'z':
|
462
|
-
expectedPointArgCount = 0;
|
463
|
-
// firstPos can be null if the stroke data is just 'z'.
|
464
|
-
if (firstPos) {
|
465
|
-
lineTo(firstPos);
|
497
|
+
if ((index + 1) % commandArgCount === 0) {
|
498
|
+
lastPos = newPos;
|
466
499
|
}
|
467
|
-
break;
|
468
|
-
case 'c':
|
469
|
-
expectedPointArgCount = 3;
|
470
|
-
cubicBezierTo(args[0], args[1], args[2]);
|
471
|
-
break;
|
472
|
-
case 'q':
|
473
|
-
expectedPointArgCount = 2;
|
474
|
-
quadraticBeierTo(args[0], args[1]);
|
475
|
-
break;
|
476
500
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
501
|
+
return newPos;
|
502
|
+
});
|
503
|
+
|
504
|
+
if (allArgs.length % commandArgCount !== 0) {
|
505
|
+
throw new Error([
|
506
|
+
`Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
|
507
|
+
`The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
|
508
|
+
`Command: ${current[0]}`,
|
509
|
+
].join('\n'));
|
510
|
+
}
|
487
511
|
|
488
|
-
|
489
|
-
|
490
|
-
expectedPointArgCount = 0;
|
512
|
+
for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
|
513
|
+
const args = allArgs.slice(argPos, argPos + commandArgCount);
|
491
514
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
515
|
+
switch (commandChar.toLowerCase()) {
|
516
|
+
case 'm':
|
517
|
+
if (argPos === 0) {
|
518
|
+
moveTo(args[0]);
|
519
|
+
} else {
|
520
|
+
lineTo(args[0]);
|
521
|
+
}
|
522
|
+
break;
|
523
|
+
case 'l':
|
524
|
+
lineTo(args[0]);
|
525
|
+
break;
|
526
|
+
case 'c':
|
527
|
+
cubicBezierTo(args[0], args[1], args[2]);
|
528
|
+
break;
|
529
|
+
case 'q':
|
530
|
+
quadraticBeierTo(args[0], args[1]);
|
531
|
+
break;
|
532
|
+
default:
|
533
|
+
throw new Error(`Unknown path command ${commandChar}`);
|
496
534
|
}
|
497
|
-
break;
|
498
|
-
default:
|
499
|
-
throw new Error(`Unknown path command ${commandChar}`);
|
500
|
-
}
|
501
535
|
|
502
|
-
|
503
|
-
throw new Error(`
|
504
|
-
Incorrect number of arguments: got ${JSON.stringify(args)} with a length of ${args.length} ≠ ${expectedPointArgCount}.
|
505
|
-
`.trim());
|
536
|
+
isFirstCommand = false;
|
506
537
|
}
|
507
538
|
|
508
|
-
if (
|
509
|
-
firstPos ??=
|
539
|
+
if (allArgs.length > 0) {
|
540
|
+
firstPos ??= allArgs[0];
|
541
|
+
startPos ??= firstPos;
|
542
|
+
lastPos = allArgs[allArgs.length - 1];
|
510
543
|
}
|
511
|
-
isFirstCommand = false;
|
512
544
|
}
|
513
545
|
|
514
|
-
return new Path(
|
546
|
+
return new Path(startPos ?? Vec2.zero, commands);
|
515
547
|
}
|
516
548
|
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
+
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
2
3
|
import Mat33 from '../../geometry/Mat33';
|
3
4
|
import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
|
4
5
|
import Rect2 from '../../geometry/Rect2';
|
@@ -128,7 +129,7 @@ export default abstract class AbstractRenderer {
|
|
128
129
|
this.objectLevel ++;
|
129
130
|
}
|
130
131
|
|
131
|
-
public endObject() {
|
132
|
+
public endObject(_loaderData?: LoadSaveDataTable) {
|
132
133
|
// Render the paths all at once
|
133
134
|
this.flushPath();
|
134
135
|
this.currentPaths = null;
|
@@ -1,7 +1,9 @@
|
|
1
1
|
|
2
|
+
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
2
3
|
import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
|
3
4
|
import Rect2 from '../../geometry/Rect2';
|
4
5
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
6
|
+
import { svgAttributesDataKey, SVGLoaderUnknownAttribute } from '../../SVGLoader';
|
5
7
|
import Viewport from '../../Viewport';
|
6
8
|
import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
|
7
9
|
|
@@ -13,8 +15,8 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
13
15
|
private lastPathStyle: RenderingStyle|null;
|
14
16
|
private lastPath: PathCommand[]|null;
|
15
17
|
private lastPathStart: Point2|null;
|
18
|
+
private objectElems: SVGElement[]|null = null;
|
16
19
|
|
17
|
-
private mainGroup: SVGGElement;
|
18
20
|
private overwrittenAttrs: Record<string, string|null> = {};
|
19
21
|
|
20
22
|
public constructor(private elem: SVGSVGElement, viewport: Viewport) {
|
@@ -41,8 +43,6 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
41
43
|
}
|
42
44
|
|
43
45
|
public clear() {
|
44
|
-
this.mainGroup = document.createElementNS(svgNameSpace, 'g');
|
45
|
-
|
46
46
|
// Restore all alltributes
|
47
47
|
for (const attrName in this.overwrittenAttrs) {
|
48
48
|
const value = this.overwrittenAttrs[attrName];
|
@@ -54,9 +54,6 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
54
54
|
}
|
55
55
|
}
|
56
56
|
this.overwrittenAttrs = {};
|
57
|
-
|
58
|
-
// Remove all children
|
59
|
-
this.elem.replaceChildren(this.mainGroup);
|
60
57
|
}
|
61
58
|
|
62
59
|
protected beginPath(startPoint: Point2) {
|
@@ -106,7 +103,8 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
106
103
|
pathElem.setAttribute('stroke-width', style.stroke.width.toString());
|
107
104
|
}
|
108
105
|
|
109
|
-
this.
|
106
|
+
this.elem.appendChild(pathElem);
|
107
|
+
this.objectElems?.push(pathElem);
|
110
108
|
}
|
111
109
|
|
112
110
|
public startObject(boundingBox: Rect2) {
|
@@ -116,13 +114,27 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
116
114
|
this.lastPath = null;
|
117
115
|
this.lastPathStart = null;
|
118
116
|
this.lastPathStyle = null;
|
117
|
+
this.objectElems = [];
|
119
118
|
}
|
120
119
|
|
121
|
-
public endObject() {
|
122
|
-
super.endObject();
|
120
|
+
public endObject(loaderData?: LoadSaveDataTable) {
|
121
|
+
super.endObject(loaderData);
|
123
122
|
|
124
123
|
// Don't extend paths across objects
|
125
124
|
this.addPathToSVG();
|
125
|
+
|
126
|
+
if (loaderData) {
|
127
|
+
// Restore any attributes unsupported by the app.
|
128
|
+
for (const elem of this.objectElems ?? []) {
|
129
|
+
const attrs = loaderData[svgAttributesDataKey] as SVGLoaderUnknownAttribute[]|undefined;
|
130
|
+
|
131
|
+
if (attrs) {
|
132
|
+
for (const [ attr, value ] of attrs) {
|
133
|
+
elem.setAttribute(attr, value);
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
126
138
|
}
|
127
139
|
|
128
140
|
protected lineTo(point: Point2) {
|
@@ -175,7 +187,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
175
187
|
elem.setAttribute('cx', `${point.x}`);
|
176
188
|
elem.setAttribute('cy', `${point.y}`);
|
177
189
|
elem.setAttribute('r', '15');
|
178
|
-
this.
|
190
|
+
this.elem.appendChild(elem);
|
179
191
|
});
|
180
192
|
}
|
181
193
|
|