js-draw 0.1.2 → 0.1.5
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 +14 -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 +20 -6
- package/dist/src/SVGLoader.d.ts +8 -0
- package/dist/src/SVGLoader.js +105 -6
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +5 -5
- package/dist/src/components/SVGGlobalAttributesObject.js +0 -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 +8 -1
- package/dist/src/geometry/Rect2.d.ts +2 -0
- package/dist/src/geometry/Rect2.js +6 -0
- package/dist/src/localization.d.ts +2 -1
- package/dist/src/localization.js +2 -1
- package/dist/src/rendering/Display.d.ts +2 -0
- package/dist/src/rendering/Display.js +19 -0
- package/dist/src/rendering/localization.d.ts +5 -0
- package/dist/src/rendering/localization.js +4 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +5 -0
- package/dist/src/rendering/renderers/AbstractRenderer.js +12 -0
- 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 +3 -0
- package/dist/src/rendering/renderers/SVGRenderer.js +30 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +24 -0
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
- package/dist/src/testing/loadExpectExtensions.js +1 -4
- package/dist/src/toolbar/HTMLToolbar.js +78 -1
- package/dist/src/toolbar/icons.d.ts +2 -0
- package/dist/src/toolbar/icons.js +18 -0
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/tools/SelectionTool.js +1 -1
- package/dist/src/tools/TextTool.d.ts +31 -0
- package/dist/src/tools/TextTool.js +174 -0
- package/dist/src/tools/ToolController.d.ts +2 -1
- package/dist/src/tools/ToolController.js +4 -1
- package/dist/src/tools/localization.d.ts +3 -1
- package/dist/src/tools/localization.js +3 -1
- package/dist-test/test-dist-bundle.html +8 -1
- package/package.json +1 -1
- package/src/Editor.css +12 -0
- package/src/Editor.ts +22 -7
- package/src/SVGLoader.ts +124 -6
- package/src/Viewport.ts +5 -5
- package/src/components/SVGGlobalAttributesObject.ts +0 -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.toString.test.ts +7 -3
- package/src/geometry/Path.ts +11 -1
- package/src/geometry/Rect2.ts +8 -0
- package/src/localization.ts +3 -1
- package/src/rendering/Display.ts +26 -0
- package/src/rendering/localization.ts +10 -0
- package/src/rendering/renderers/AbstractRenderer.ts +16 -0
- package/src/rendering/renderers/CanvasRenderer.ts +34 -10
- package/src/rendering/renderers/DummyRenderer.ts +8 -0
- package/src/rendering/renderers/SVGRenderer.ts +36 -1
- package/src/rendering/renderers/TextOnlyRenderer.ts +51 -0
- package/src/testing/loadExpectExtensions.ts +1 -4
- package/src/toolbar/HTMLToolbar.ts +96 -1
- package/src/toolbar/icons.ts +24 -0
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/toolbar.css +6 -3
- package/src/tools/SelectionTool.ts +1 -1
- package/src/tools/TextTool.ts +229 -0
- package/src/tools/ToolController.ts +4 -0
- package/src/tools/localization.ts +7 -2
package/dist/src/Editor.d.ts
CHANGED
@@ -14,7 +14,7 @@ import { EditorLocalization } from './localization';
|
|
14
14
|
export interface EditorSettings {
|
15
15
|
renderingMode: RenderingMode;
|
16
16
|
localization: Partial<EditorLocalization>;
|
17
|
-
wheelEventsEnabled: boolean;
|
17
|
+
wheelEventsEnabled: boolean | 'only-if-focused';
|
18
18
|
}
|
19
19
|
export declare class Editor {
|
20
20
|
private container;
|
@@ -48,6 +48,7 @@ export declare class Editor {
|
|
48
48
|
rerender(showImageBounds?: boolean): void;
|
49
49
|
drawWetInk(...path: RenderablePathSpec[]): void;
|
50
50
|
clearWetInk(): void;
|
51
|
+
focus(): void;
|
51
52
|
createHTMLOverlay(overlay: HTMLElement): {
|
52
53
|
remove: () => void;
|
53
54
|
};
|
package/dist/src/Editor.js
CHANGED
@@ -183,13 +183,24 @@ export class Editor {
|
|
183
183
|
})) {
|
184
184
|
evt.preventDefault();
|
185
185
|
}
|
186
|
+
else if (evt.key === 'Escape') {
|
187
|
+
this.renderingRegion.blur();
|
188
|
+
}
|
186
189
|
});
|
187
190
|
this.container.addEventListener('wheel', evt => {
|
188
191
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
189
|
-
// Process wheel events if the ctrl key is down -- we do want to handle
|
192
|
+
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
190
193
|
// pinch-zooming.
|
191
|
-
if (!
|
192
|
-
|
194
|
+
if (!evt.ctrlKey) {
|
195
|
+
if (!this.settings.wheelEventsEnabled) {
|
196
|
+
return;
|
197
|
+
}
|
198
|
+
else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
199
|
+
const focusedChild = this.container.querySelector(':focus');
|
200
|
+
if (!focusedChild) {
|
201
|
+
return;
|
202
|
+
}
|
203
|
+
}
|
193
204
|
}
|
194
205
|
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
195
206
|
delta = delta.times(15);
|
@@ -281,13 +292,12 @@ export class Editor {
|
|
281
292
|
this.display.startRerender();
|
282
293
|
// Draw a rectangle around the region that will be visible on save
|
283
294
|
const renderer = this.display.getDryInkRenderer();
|
295
|
+
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
|
284
296
|
if (showImageBounds) {
|
285
297
|
const exportRectFill = { fill: Color4.fromHex('#44444455') };
|
286
|
-
const exportRectStrokeWidth =
|
298
|
+
const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
|
287
299
|
renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
|
288
300
|
}
|
289
|
-
//this.image.render(renderer, this.viewport);
|
290
|
-
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
|
291
301
|
this.rerenderQueued = false;
|
292
302
|
}
|
293
303
|
drawWetInk(...path) {
|
@@ -298,6 +308,10 @@ export class Editor {
|
|
298
308
|
clearWetInk() {
|
299
309
|
this.display.getWetInkRenderer().clear();
|
300
310
|
}
|
311
|
+
// Focuses the region used for text input
|
312
|
+
focus() {
|
313
|
+
this.renderingRegion.focus();
|
314
|
+
}
|
301
315
|
createHTMLOverlay(overlay) {
|
302
316
|
overlay.classList.add('overlay');
|
303
317
|
this.container.appendChild(overlay);
|
package/dist/src/SVGLoader.d.ts
CHANGED
@@ -2,7 +2,13 @@ import Rect2 from './geometry/Rect2';
|
|
2
2
|
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
|
3
3
|
export declare const defaultSVGViewRect: Rect2;
|
4
4
|
export declare const svgAttributesDataKey = "svgAttrs";
|
5
|
+
export declare const svgStyleAttributesDataKey = "svgStyleAttrs";
|
5
6
|
export declare type SVGLoaderUnknownAttribute = [string, string];
|
7
|
+
export declare type SVGLoaderUnknownStyleAttribute = {
|
8
|
+
key: string;
|
9
|
+
value: string;
|
10
|
+
priority?: string;
|
11
|
+
};
|
6
12
|
export default class SVGLoader implements ImageLoader {
|
7
13
|
private source;
|
8
14
|
private onFinish?;
|
@@ -17,6 +23,8 @@ export default class SVGLoader implements ImageLoader {
|
|
17
23
|
private strokeDataFromElem;
|
18
24
|
private attachUnrecognisedAttrs;
|
19
25
|
private addPath;
|
26
|
+
private makeText;
|
27
|
+
private addText;
|
20
28
|
private addUnknownNode;
|
21
29
|
private updateViewBox;
|
22
30
|
private updateSVGAttrs;
|
package/dist/src/SVGLoader.js
CHANGED
@@ -10,13 +10,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
10
10
|
import Color4 from './Color4';
|
11
11
|
import Stroke from './components/Stroke';
|
12
12
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
13
|
+
import Text from './components/Text';
|
13
14
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
15
|
+
import Mat33 from './geometry/Mat33';
|
14
16
|
import Path from './geometry/Path';
|
15
17
|
import Rect2 from './geometry/Rect2';
|
18
|
+
import { Vec2 } from './geometry/Vec2';
|
16
19
|
// Size of a loaded image if no size is specified.
|
17
20
|
export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
|
18
21
|
// Key to retrieve unrecognised attributes from an AbstractComponent
|
19
22
|
export const svgAttributesDataKey = 'svgAttrs';
|
23
|
+
export const svgStyleAttributesDataKey = 'svgStyleAttrs';
|
20
24
|
export default class SVGLoader {
|
21
25
|
constructor(source, onFinish) {
|
22
26
|
this.source = source;
|
@@ -83,13 +87,29 @@ export default class SVGLoader {
|
|
83
87
|
}
|
84
88
|
return result;
|
85
89
|
}
|
86
|
-
attachUnrecognisedAttrs(elem, node, supportedAttrs) {
|
90
|
+
attachUnrecognisedAttrs(elem, node, supportedAttrs, supportedStyleAttrs) {
|
87
91
|
for (const attr of node.getAttributeNames()) {
|
88
|
-
if (supportedAttrs.has(attr)) {
|
92
|
+
if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
|
89
93
|
continue;
|
90
94
|
}
|
91
95
|
elem.attachLoadSaveData(svgAttributesDataKey, [attr, node.getAttribute(attr)]);
|
92
96
|
}
|
97
|
+
if (supportedStyleAttrs) {
|
98
|
+
for (const attr of node.style) {
|
99
|
+
if (attr === '' || !attr) {
|
100
|
+
continue;
|
101
|
+
}
|
102
|
+
if (supportedStyleAttrs.has(attr)) {
|
103
|
+
continue;
|
104
|
+
}
|
105
|
+
// TODO: Do we need special logic for !important properties?
|
106
|
+
elem.attachLoadSaveData(svgStyleAttributesDataKey, {
|
107
|
+
key: attr,
|
108
|
+
value: node.style.getPropertyValue(attr),
|
109
|
+
priority: node.style.getPropertyPriority(attr)
|
110
|
+
});
|
111
|
+
}
|
112
|
+
}
|
93
113
|
}
|
94
114
|
// Adds a stroke with a single path
|
95
115
|
addPath(node) {
|
@@ -98,7 +118,8 @@ export default class SVGLoader {
|
|
98
118
|
try {
|
99
119
|
const strokeData = this.strokeDataFromElem(node);
|
100
120
|
elem = new Stroke(strokeData);
|
101
|
-
|
121
|
+
const supportedStyleAttrs = ['stroke', 'fill', 'stroke-width'];
|
122
|
+
this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStyleAttrs, 'd']), new Set(supportedStyleAttrs));
|
102
123
|
}
|
103
124
|
catch (e) {
|
104
125
|
console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
|
@@ -106,6 +127,74 @@ export default class SVGLoader {
|
|
106
127
|
}
|
107
128
|
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
|
108
129
|
}
|
130
|
+
makeText(elem) {
|
131
|
+
var _a;
|
132
|
+
const contentList = [];
|
133
|
+
for (const child of elem.childNodes) {
|
134
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
135
|
+
contentList.push((_a = child.nodeValue) !== null && _a !== void 0 ? _a : '');
|
136
|
+
}
|
137
|
+
else if (child.nodeType === Node.ELEMENT_NODE) {
|
138
|
+
const subElem = child;
|
139
|
+
if (subElem.tagName.toLowerCase() === 'tspan') {
|
140
|
+
contentList.push(this.makeText(subElem));
|
141
|
+
}
|
142
|
+
else {
|
143
|
+
throw new Error(`Unrecognized text child element: ${subElem}`);
|
144
|
+
}
|
145
|
+
}
|
146
|
+
else {
|
147
|
+
throw new Error(`Unrecognized text child node: ${child}.`);
|
148
|
+
}
|
149
|
+
}
|
150
|
+
// Compute styles.
|
151
|
+
const computedStyles = window.getComputedStyle(elem);
|
152
|
+
const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
|
153
|
+
const supportedStyleAttrs = [
|
154
|
+
'fontFamily',
|
155
|
+
'fill',
|
156
|
+
'transform'
|
157
|
+
];
|
158
|
+
let fontSize = 12;
|
159
|
+
if (fontSizeMatch) {
|
160
|
+
supportedStyleAttrs.push('fontSize');
|
161
|
+
fontSize = parseFloat(fontSizeMatch[1]);
|
162
|
+
}
|
163
|
+
const style = {
|
164
|
+
size: fontSize,
|
165
|
+
fontFamily: computedStyles.fontFamily || 'sans-serif',
|
166
|
+
renderingStyle: {
|
167
|
+
fill: Color4.fromString(computedStyles.fill)
|
168
|
+
},
|
169
|
+
};
|
170
|
+
// Compute transform matrix
|
171
|
+
let transform = Mat33.fromCSSMatrix(computedStyles.transform);
|
172
|
+
const supportedAttrs = [];
|
173
|
+
const elemX = elem.getAttribute('x');
|
174
|
+
const elemY = elem.getAttribute('y');
|
175
|
+
if (elemX && elemY) {
|
176
|
+
const x = parseFloat(elemX);
|
177
|
+
const y = parseFloat(elemY);
|
178
|
+
if (!isNaN(x) && !isNaN(y)) {
|
179
|
+
supportedAttrs.push('x', 'y');
|
180
|
+
transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
|
181
|
+
}
|
182
|
+
}
|
183
|
+
const result = new Text(contentList, transform, style);
|
184
|
+
this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
|
185
|
+
return result;
|
186
|
+
}
|
187
|
+
addText(elem) {
|
188
|
+
var _a;
|
189
|
+
try {
|
190
|
+
const textElem = this.makeText(elem);
|
191
|
+
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
|
192
|
+
}
|
193
|
+
catch (e) {
|
194
|
+
console.error('Invalid text object in node', elem, '. Adding as an unknown object. Error:', e);
|
195
|
+
this.addUnknownNode(elem);
|
196
|
+
}
|
197
|
+
}
|
109
198
|
addUnknownNode(node) {
|
110
199
|
var _a;
|
111
200
|
const component = new UnknownSVGObject(node);
|
@@ -117,12 +206,13 @@ export default class SVGLoader {
|
|
117
206
|
if (this.rootViewBox || !viewBoxAttr) {
|
118
207
|
return;
|
119
208
|
}
|
120
|
-
const components = viewBoxAttr.split(/[ \t,]
|
209
|
+
const components = viewBoxAttr.split(/[ \t\n,]+/);
|
121
210
|
const x = parseFloat(components[0]);
|
122
211
|
const y = parseFloat(components[1]);
|
123
212
|
const width = parseFloat(components[2]);
|
124
213
|
const height = parseFloat(components[3]);
|
125
214
|
if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
|
215
|
+
console.warn(`node ${node} has an unparsable viewbox. Viewbox: ${viewBoxAttr}. Match: ${components}.`);
|
126
216
|
return;
|
127
217
|
}
|
128
218
|
this.rootViewBox = new Rect2(x, y, width, height);
|
@@ -136,6 +226,7 @@ export default class SVGLoader {
|
|
136
226
|
var _a;
|
137
227
|
return __awaiter(this, void 0, void 0, function* () {
|
138
228
|
this.totalToProcess += node.childElementCount;
|
229
|
+
let visitChildren = true;
|
139
230
|
switch (node.tagName.toLowerCase()) {
|
140
231
|
case 'g':
|
141
232
|
// Continue -- visit the node's children.
|
@@ -143,6 +234,10 @@ export default class SVGLoader {
|
|
143
234
|
case 'path':
|
144
235
|
this.addPath(node);
|
145
236
|
break;
|
237
|
+
case 'text':
|
238
|
+
this.addText(node);
|
239
|
+
visitChildren = false;
|
240
|
+
break;
|
146
241
|
case 'svg':
|
147
242
|
this.updateViewBox(node);
|
148
243
|
this.updateSVGAttrs(node);
|
@@ -155,8 +250,10 @@ export default class SVGLoader {
|
|
155
250
|
this.addUnknownNode(node);
|
156
251
|
return;
|
157
252
|
}
|
158
|
-
|
159
|
-
|
253
|
+
if (visitChildren) {
|
254
|
+
for (const child of node.children) {
|
255
|
+
yield this.visit(child);
|
256
|
+
}
|
160
257
|
}
|
161
258
|
this.processedCount++;
|
162
259
|
yield ((_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, this.processedCount, this.totalToProcess));
|
@@ -223,7 +320,9 @@ export default class SVGLoader {
|
|
223
320
|
sandboxDoc.close();
|
224
321
|
const svgElem = sandboxDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
225
322
|
svgElem.innerHTML = text;
|
323
|
+
sandboxDoc.body.appendChild(svgElem);
|
226
324
|
return new SVGLoader(svgElem, () => {
|
325
|
+
svgElem.remove();
|
227
326
|
sandbox.remove();
|
228
327
|
});
|
229
328
|
}
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -35,7 +35,7 @@ export declare class Viewport {
|
|
35
35
|
getRotationAngle(): number;
|
36
36
|
static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
|
37
37
|
roundPoint(point: Point2): Point2;
|
38
|
-
zoomTo(toMakeVisible: Rect2): Command;
|
38
|
+
zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;
|
39
39
|
}
|
40
40
|
export declare namespace Viewport {
|
41
41
|
type ViewportTransform = typeof Viewport.ViewportTransform.prototype;
|
package/dist/src/Viewport.js
CHANGED
@@ -85,7 +85,7 @@ export class Viewport {
|
|
85
85
|
// Returns a Command that transforms the view such that [rect] is visible, and perhaps
|
86
86
|
// centered in the viewport.
|
87
87
|
// Returns null if no transformation is necessary
|
88
|
-
zoomTo(toMakeVisible) {
|
88
|
+
zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
|
89
89
|
let transform = Mat33.identity;
|
90
90
|
if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
|
91
91
|
throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
|
@@ -93,18 +93,18 @@ export class Viewport {
|
|
93
93
|
if (isNaN(toMakeVisible.size.magnitude())) {
|
94
94
|
throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
|
95
95
|
}
|
96
|
-
// Try to move the selection within the center
|
96
|
+
// Try to move the selection within the center 3/4ths of the viewport.
|
97
97
|
const recomputeTargetRect = () => {
|
98
98
|
// transform transforms objects on the canvas. As such, we need to invert it
|
99
99
|
// to transform the viewport.
|
100
100
|
const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
|
101
|
-
return visibleRect.transformedBoundingBox(Mat33.scaling2D(
|
101
|
+
return visibleRect.transformedBoundingBox(Mat33.scaling2D(3 / 4, visibleRect.center));
|
102
102
|
};
|
103
103
|
let targetRect = recomputeTargetRect();
|
104
104
|
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
|
105
105
|
// Ensure that toMakeVisible is at least 1/8th of the visible region.
|
106
|
-
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.
|
107
|
-
if (largerThanTarget || muchSmallerThanTarget) {
|
106
|
+
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.25;
|
107
|
+
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
|
108
108
|
// If larger than the target, ensure that the longest axis is visible.
|
109
109
|
// If smaller, shrink the visible rectangle as much as possible
|
110
110
|
const multiplier = (largerThanTarget ? Math.max : Math.min)(toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h);
|
@@ -13,7 +13,6 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
|
|
13
13
|
// Don't draw unrenderable objects if we can't
|
14
14
|
return;
|
15
15
|
}
|
16
|
-
console.log('Rendering to SVG.', this.attrs);
|
17
16
|
for (const [attr, value] of this.attrs) {
|
18
17
|
canvas.setRootSVGAttribute(attr, value);
|
19
18
|
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Mat33 from '../geometry/Mat33';
|
3
|
+
import Rect2 from '../geometry/Rect2';
|
4
|
+
import AbstractRenderer, { RenderingStyle } from '../rendering/renderers/AbstractRenderer';
|
5
|
+
import AbstractComponent from './AbstractComponent';
|
6
|
+
import { ImageComponentLocalization } from './localization';
|
7
|
+
export interface TextStyle {
|
8
|
+
size: number;
|
9
|
+
fontFamily: string;
|
10
|
+
fontWeight?: string;
|
11
|
+
fontVariant?: string;
|
12
|
+
renderingStyle: RenderingStyle;
|
13
|
+
}
|
14
|
+
export default class Text extends AbstractComponent {
|
15
|
+
protected textObjects: Array<string | Text>;
|
16
|
+
private transform;
|
17
|
+
private style;
|
18
|
+
protected contentBBox: Rect2;
|
19
|
+
constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle);
|
20
|
+
static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
|
21
|
+
private static textMeasuringCtx;
|
22
|
+
private static getTextDimens;
|
23
|
+
private computeBBoxOfPart;
|
24
|
+
private recomputeBBox;
|
25
|
+
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
26
|
+
intersects(lineSegment: LineSegment2): boolean;
|
27
|
+
protected applyTransformation(affineTransfm: Mat33): void;
|
28
|
+
private getText;
|
29
|
+
description(localizationTable: ImageComponentLocalization): string;
|
30
|
+
}
|
@@ -0,0 +1,111 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Rect2 from '../geometry/Rect2';
|
3
|
+
import AbstractComponent from './AbstractComponent';
|
4
|
+
export default class Text extends AbstractComponent {
|
5
|
+
constructor(textObjects, transform, style) {
|
6
|
+
super();
|
7
|
+
this.textObjects = textObjects;
|
8
|
+
this.transform = transform;
|
9
|
+
this.style = style;
|
10
|
+
this.recomputeBBox();
|
11
|
+
}
|
12
|
+
static applyTextStyles(ctx, style) {
|
13
|
+
var _a, _b;
|
14
|
+
// Quote the font family if necessary.
|
15
|
+
const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
|
16
|
+
ctx.font = [
|
17
|
+
((_a = style.size) !== null && _a !== void 0 ? _a : 12) + 'px',
|
18
|
+
(_b = style.fontWeight) !== null && _b !== void 0 ? _b : '',
|
19
|
+
`${fontFamily}`,
|
20
|
+
style.fontWeight
|
21
|
+
].join(' ');
|
22
|
+
ctx.textAlign = 'left';
|
23
|
+
}
|
24
|
+
static getTextDimens(text, style) {
|
25
|
+
var _a;
|
26
|
+
(_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = document.createElement('canvas').getContext('2d'));
|
27
|
+
const ctx = Text.textMeasuringCtx;
|
28
|
+
Text.applyTextStyles(ctx, style);
|
29
|
+
const measure = ctx.measureText(text);
|
30
|
+
// Text is drawn with (0,0) at the bottom left of the baseline.
|
31
|
+
const textY = -measure.actualBoundingBoxAscent;
|
32
|
+
const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
|
33
|
+
return new Rect2(0, textY, measure.width, textHeight);
|
34
|
+
}
|
35
|
+
computeBBoxOfPart(part) {
|
36
|
+
if (typeof part === 'string') {
|
37
|
+
const textBBox = Text.getTextDimens(part, this.style);
|
38
|
+
return textBBox.transformedBoundingBox(this.transform);
|
39
|
+
}
|
40
|
+
else {
|
41
|
+
const bbox = part.contentBBox.transformedBoundingBox(this.transform);
|
42
|
+
return bbox;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
recomputeBBox() {
|
46
|
+
let bbox = null;
|
47
|
+
for (const textObject of this.textObjects) {
|
48
|
+
const currentBBox = this.computeBBoxOfPart(textObject);
|
49
|
+
bbox !== null && bbox !== void 0 ? bbox : (bbox = currentBBox);
|
50
|
+
bbox = bbox.union(currentBBox);
|
51
|
+
}
|
52
|
+
this.contentBBox = bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
|
53
|
+
}
|
54
|
+
render(canvas, _visibleRect) {
|
55
|
+
const cursor = this.transform;
|
56
|
+
canvas.startObject(this.contentBBox);
|
57
|
+
for (const textObject of this.textObjects) {
|
58
|
+
if (typeof textObject === 'string') {
|
59
|
+
canvas.drawText(textObject, cursor, this.style);
|
60
|
+
}
|
61
|
+
else {
|
62
|
+
canvas.pushTransform(cursor);
|
63
|
+
textObject.render(canvas);
|
64
|
+
canvas.popTransform();
|
65
|
+
}
|
66
|
+
}
|
67
|
+
canvas.endObject(this.getLoadSaveData());
|
68
|
+
}
|
69
|
+
intersects(lineSegment) {
|
70
|
+
// Convert canvas space to internal space.
|
71
|
+
const invTransform = this.transform.inverse();
|
72
|
+
const p1InThisSpace = invTransform.transformVec2(lineSegment.p1);
|
73
|
+
const p2InThisSpace = invTransform.transformVec2(lineSegment.p2);
|
74
|
+
lineSegment = new LineSegment2(p1InThisSpace, p2InThisSpace);
|
75
|
+
for (const subObject of this.textObjects) {
|
76
|
+
if (typeof subObject === 'string') {
|
77
|
+
const textBBox = Text.getTextDimens(subObject, this.style);
|
78
|
+
// TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and
|
79
|
+
// use pixel-testing to check for intersection with its contour.
|
80
|
+
if (textBBox.getEdges().some(edge => lineSegment.intersection(edge) !== null)) {
|
81
|
+
return true;
|
82
|
+
}
|
83
|
+
}
|
84
|
+
else {
|
85
|
+
if (subObject.intersects(lineSegment)) {
|
86
|
+
return true;
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
return false;
|
91
|
+
}
|
92
|
+
applyTransformation(affineTransfm) {
|
93
|
+
this.transform = affineTransfm.rightMul(this.transform);
|
94
|
+
this.recomputeBBox();
|
95
|
+
}
|
96
|
+
getText() {
|
97
|
+
const result = [];
|
98
|
+
for (const textObject of this.textObjects) {
|
99
|
+
if (typeof textObject === 'string') {
|
100
|
+
result.push(textObject);
|
101
|
+
}
|
102
|
+
else {
|
103
|
+
result.push(textObject.getText());
|
104
|
+
}
|
105
|
+
}
|
106
|
+
return result.join(' ');
|
107
|
+
}
|
108
|
+
description(localizationTable) {
|
109
|
+
return localizationTable.text(this.getText());
|
110
|
+
}
|
111
|
+
}
|
@@ -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
|
};
|
@@ -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,8 +1,9 @@
|
|
1
1
|
import { CommandLocalization } from './commands/localization';
|
2
2
|
import { ImageComponentLocalization } from './components/localization';
|
3
|
+
import { TextRendererLocalization } from './rendering/localization';
|
3
4
|
import { ToolbarLocalization } from './toolbar/localization';
|
4
5
|
import { ToolLocalization } from './tools/localization';
|
5
|
-
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
|
6
|
+
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization, TextRendererLocalization {
|
6
7
|
undoAnnouncement: (actionDescription: string) => string;
|
7
8
|
redoAnnouncement: (actionDescription: string) => string;
|
8
9
|
doneLoading: string;
|
package/dist/src/localization.js
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { defaultCommandLocalization } from './commands/localization';
|
2
2
|
import { defaultComponentLocalization } from './components/localization';
|
3
|
+
import { defaultTextRendererLocalization } from './rendering/localization';
|
3
4
|
import { defaultToolbarLocalization } from './toolbar/localization';
|
4
5
|
import { defaultToolLocalization } from './tools/localization';
|
5
|
-
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
|
6
|
+
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), defaultTextRendererLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
|
@@ -10,6 +10,7 @@ export default class Display {
|
|
10
10
|
private parent;
|
11
11
|
private dryInkRenderer;
|
12
12
|
private wetInkRenderer;
|
13
|
+
private textRenderer;
|
13
14
|
private cache;
|
14
15
|
private resizeSurfacesCallback?;
|
15
16
|
private flattenCallback?;
|
@@ -18,6 +19,7 @@ export default class Display {
|
|
18
19
|
get height(): number;
|
19
20
|
getCache(): RenderingCache;
|
20
21
|
private initializeCanvasRendering;
|
22
|
+
private initializeTextRendering;
|
21
23
|
startRerender(): AbstractRenderer;
|
22
24
|
setDraftMode(draftMode: boolean): void;
|
23
25
|
getDryInkRenderer(): AbstractRenderer;
|