Socket
Socket
Sign inDemoInstall

js-draw

Package Overview
Dependencies
Maintainers
1
Versions
117
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

js-draw - npm Package Compare versions

Comparing version 0.1.2 to 0.1.3

dist/src/components/Text.d.ts

5

CHANGELOG.md

@@ -0,1 +1,6 @@

# 0.1.3
* Very minimalistic text tool.
* Ability to load and save text.
* Fix a rounding bug where small strokes could be stretched/moved to the wrong locations.
# 0.1.2

@@ -2,0 +7,0 @@ * Replace 'touch drawing' with a hand tool.

1

dist/src/components/localization.d.ts
export interface ImageComponentLocalization {
text: (text: string) => string;
stroke: string;

@@ -3,0 +4,0 @@ svgObject: string;

export const defaultComponentLocalization = {
stroke: 'Stroke',
svgObject: 'SVG Object',
text: (text) => `Text object: ${text}`,
};

1

dist/src/components/SVGGlobalAttributesObject.js

@@ -16,3 +16,2 @@ import Rect2 from '../geometry/Rect2';

}
console.log('Rendering to SVG.', this.attrs);
for (const [attr, value] of this.attrs) {

@@ -19,0 +18,0 @@ canvas.setRootSVGAttribute(attr, value);

@@ -284,9 +284,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

const renderer = this.display.getDryInkRenderer();
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
if (showImageBounds) {
const exportRectFill = { fill: Color4.fromHex('#44444455') };
const exportRectStrokeWidth = 12;
const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
}
//this.image.render(renderer, this.viewport);
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
this.rerenderQueued = false;

@@ -293,0 +292,0 @@ }

@@ -31,2 +31,3 @@ import { Point2, Vec2 } from './Vec2';

static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
static fromCSSMatrix(cssString: string): Mat33;
}

@@ -189,3 +189,33 @@ import { Vec2 } from './Vec2';

}
// Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
static fromCSSMatrix(cssString) {
if (cssString === '' || cssString === 'none') {
return Mat33.identity;
}
const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
const numberSepExp = '[, \\t\\n]';
const regExpSource = `^\\s*matrix\\s*\\(${[
// According to MDN, matrix(a,b,c,d,e,f) has form:
// ⎡ a c e ⎤
// ⎢ b d f ⎥
// ⎣ 0 0 1 ⎦
numberExp, numberExp, numberExp,
numberExp, numberExp, numberExp, // b, d, f
].join(`${numberSepExp}+`)}${numberSepExp}*\\)\\s*$`;
const matrixExp = new RegExp(regExpSource, 'i');
const match = matrixExp.exec(cssString);
if (!match) {
throw new Error(`Unsupported transformation: ${cssString}`);
}
const matrixData = match.slice(1).map(entry => parseFloat(entry));
const a = matrixData[0];
const b = matrixData[1];
const c = matrixData[2];
const d = matrixData[3];
const e = matrixData[4];
const f = matrixData[5];
const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
return transform;
}
}
Mat33.identity = new Mat33(1, 0, 0, 0, 1, 0, 0, 0, 1);

@@ -223,2 +223,3 @@ import { Bezier } from 'bezier-js';

const preDecimal = parseInt(roundingDownMatch[2], 10);
const origPostDecimalString = roundingDownMatch[3];
let newPostDecimal = (postDecimal + 10 - lastDigit).toString();

@@ -231,2 +232,7 @@ let carry = 0;

}
// parseInt(...).toString() removes leading zeroes. Add them back.
while (newPostDecimal.length < origPostDecimalString.length) {
newPostDecimal = carry.toString(10) + newPostDecimal;
carry = 0;
}
text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;

@@ -236,3 +242,4 @@ }

// Remove trailing zeroes
text = text.replace(/([.][^0]*)0+$/, '$1');
text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
text = text.replace(/[.]0+$/, '.');
// Remove trailing period

@@ -239,0 +246,0 @@ return text.replace(/[.]$/, '');

@@ -37,2 +37,4 @@ import LineSegment2 from './LineSegment2';

get bottomLeft(): import("./Vec3").default;
get width(): number;
get height(): number;
getEdges(): LineSegment2[];

@@ -39,0 +41,0 @@ transformedBoundingBox(affineTransform: Mat33): Rect2;

@@ -129,2 +129,8 @@ import LineSegment2 from './LineSegment2';

}
get width() {
return this.w;
}
get height() {
return this.h;
}
// Returns edges in the order

@@ -131,0 +137,0 @@ // [ rightEdge, topEdge, leftEdge, bottomEdge ]

import Color4 from '../../Color4';
import { LoadSaveDataTable } from '../../components/AbstractComponent';
import { TextStyle } from '../../components/Text';
import Mat33 from '../../geometry/Mat33';

@@ -23,2 +24,3 @@ import { PathCommand } from '../../geometry/Path';

private selfTransform;
private transformStack;
protected constructor(viewport: Viewport);

@@ -34,2 +36,3 @@ protected getViewport(): Viewport;

protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
abstract isTooSmallToRender(rect: Rect2): boolean;

@@ -49,2 +52,4 @@ setDraftMode(_draftMode: boolean): void;

setTransform(transform: Mat33 | null): void;
pushTransform(transform: Mat33): void;
popTransform(): void;
getCanvasToScreenTransform(): Mat33;

@@ -51,0 +56,0 @@ canvasToScreen(vec: Vec2): Vec2;

@@ -14,2 +14,3 @@ import Path, { PathCommandType } from '../../geometry/Path';

this.selfTransform = null;
this.transformStack = [];
this.objectLevel = 0;

@@ -108,2 +109,13 @@ this.currentPaths = null;

}
pushTransform(transform) {
this.transformStack.push(this.selfTransform);
this.setTransform(this.getCanvasToScreenTransform().rightMul(transform));
}
popTransform() {
var _a;
if (this.transformStack.length === 0) {
throw new Error('Unable to pop more transforms than have been pushed!');
}
this.setTransform((_a = this.transformStack.pop()) !== null && _a !== void 0 ? _a : null);
}
// Get the matrix that transforms a vector on the canvas to a vector on this'

@@ -110,0 +122,0 @@ // rendering target.

@@ -0,1 +1,2 @@

import { TextStyle } from '../../components/Text';
import Mat33 from '../../geometry/Mat33';

@@ -15,2 +16,3 @@ import Rect2 from '../../geometry/Rect2';

constructor(ctx: CanvasRenderingContext2D, viewport: Viewport);
private transformBy;
canRenderFromWithoutDataLoss(other: AbstractRenderer): boolean;

@@ -28,2 +30,3 @@ renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void;

drawPath(path: RenderablePathSpec): void;
drawText(text: string, transform: Mat33, style: TextStyle): void;
private clipLevels;

@@ -30,0 +33,0 @@ startObject(boundingBox: Rect2, clip: boolean): void;

import Color4 from '../../Color4';
import Text from '../../components/Text';
import { Vec2 } from '../../geometry/Vec2';

@@ -13,2 +14,12 @@ import AbstractRenderer from './AbstractRenderer';

}
transformBy(transformBy) {
// From MDN, transform(a,b,c,d,e,f)
// takes input such that
// ⎡ a c e ⎤
// ⎢ b d f ⎥ transforms content drawn to [ctx].
// ⎣ 0 0 1 ⎦
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
transformBy.a2, transformBy.b2, // c, d
transformBy.a3, transformBy.b3);
}
canRenderFromWithoutDataLoss(other) {

@@ -23,10 +34,3 @@ return other instanceof CanvasRenderer;

this.ctx.save();
// From MDN, transform(a,b,c,d,e,f)
// takes input such that
// ⎡ a c e ⎤
// ⎢ b d f ⎥ transforms content drawn to [ctx].
// ⎣ 0 0 1 ⎦
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
transformBy.a2, transformBy.b2, // c, d
transformBy.a3, transformBy.b3);
this.transformBy(transformBy);
this.ctx.drawImage(other.ctx.canvas, 0, 0);

@@ -110,2 +114,18 @@ this.ctx.restore();

}
drawText(text, transform, style) {
this.ctx.save();
transform = this.getCanvasToScreenTransform().rightMul(transform);
this.transformBy(transform);
Text.applyTextStyles(this.ctx, style);
if (style.renderingStyle.fill.a !== 0) {
this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
this.ctx.fillText(text, 0, 0);
}
if (style.renderingStyle.stroke) {
this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
this.ctx.lineWidth = style.renderingStyle.stroke.width;
this.ctx.strokeText(text, 0, 0);
}
this.ctx.restore();
}
startObject(boundingBox, clip) {

@@ -112,0 +132,0 @@ if (this.isTooSmallToRender(boundingBox)) {

@@ -0,1 +1,2 @@

import { TextStyle } from '../../components/Text';
import Mat33 from '../../geometry/Mat33';

@@ -13,2 +14,3 @@ import Rect2 from '../../geometry/Rect2';

objectNestingLevel: number;
lastText: string | null;
pointBuffer: Point2[];

@@ -25,2 +27,3 @@ constructor(viewport: Viewport);

drawPoints(..._points: Vec3[]): void;
drawText(text: string, _transform: Mat33, _style: TextStyle): void;
startObject(boundingBox: Rect2, _clip: boolean): void;

@@ -27,0 +30,0 @@ endObject(): void;

@@ -13,2 +13,3 @@ // Renderer that outputs nothing. Useful for automated tests.

this.objectNestingLevel = 0;
this.lastText = null;
// List of points drawn since the last clear.

@@ -32,2 +33,3 @@ this.pointBuffer = [];

this.pointBuffer = [];
this.lastText = null;
// Ensure all objects finished rendering

@@ -73,2 +75,5 @@ if (this.objectNestingLevel > 0) {

}
drawText(text, _transform, _style) {
this.lastText = text;
}
startObject(boundingBox, _clip) {

@@ -75,0 +80,0 @@ super.startObject(boundingBox);

import { LoadSaveDataTable } from '../../components/AbstractComponent';
import { TextStyle } from '../../components/Text';
import Mat33 from '../../geometry/Mat33';
import Rect2 from '../../geometry/Rect2';

@@ -22,2 +24,3 @@ import { Point2, Vec2 } from '../../geometry/Vec2';

private addPathToSVG;
drawText(text: string, transform: Mat33, style: TextStyle): void;
startObject(boundingBox: Rect2): void;

@@ -24,0 +27,0 @@ endObject(loaderData?: LoadSaveDataTable): void;

import Path, { PathCommandType } from '../../geometry/Path';
import { Vec2 } from '../../geometry/Vec2';
import { svgAttributesDataKey } from '../../SVGLoader';
import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader';
import AbstractRenderer from './AbstractRenderer';

@@ -89,2 +89,25 @@ const svgNameSpace = 'http://www.w3.org/2000/svg';

}
drawText(text, transform, style) {
var _a, _b, _c;
transform = this.getCanvasToScreenTransform().rightMul(transform);
const textElem = document.createElementNS(svgNameSpace, 'text');
textElem.appendChild(document.createTextNode(text));
textElem.style.transform = `matrix(
${transform.a1}, ${transform.b1},
${transform.a2}, ${transform.b2},
${transform.a3}, ${transform.b3}
)`;
textElem.style.fontFamily = style.fontFamily;
textElem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
textElem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
textElem.style.fontSize = style.size + 'px';
textElem.style.fill = style.renderingStyle.fill.toHexString();
if (style.renderingStyle.stroke) {
const strokeStyle = style.renderingStyle.stroke;
textElem.style.stroke = strokeStyle.color.toHexString();
textElem.style.strokeWidth = strokeStyle.width + 'px';
}
this.elem.appendChild(textElem);
(_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
}
startObject(boundingBox) {

@@ -107,2 +130,3 @@ super.startObject(boundingBox);

const attrs = loaderData[svgAttributesDataKey];
const styleAttrs = loaderData[svgStyleAttributesDataKey];
if (attrs) {

@@ -113,2 +137,7 @@ for (const [attr, value] of attrs) {

}
if (styleAttrs) {
for (const attr of styleAttrs) {
elem.style.setProperty(attr.key, attr.value, attr.priority);
}
}
}

@@ -115,0 +144,0 @@ }

@@ -5,3 +5,9 @@ import Rect2 from './geometry/Rect2';

export declare const svgAttributesDataKey = "svgAttrs";
export declare const svgStyleAttributesDataKey = "svgStyleAttrs";
export declare type SVGLoaderUnknownAttribute = [string, string];
export declare type SVGLoaderUnknownStyleAttribute = {
key: string;
value: string;
priority?: string;
};
export default class SVGLoader implements ImageLoader {

@@ -21,2 +27,4 @@ private source;

private addPath;
private makeText;
private addText;
private addUnknownNode;

@@ -23,0 +31,0 @@ private updateViewBox;

@@ -13,5 +13,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
import Text from './components/Text';
import UnknownSVGObject from './components/UnknownSVGObject';
import Mat33 from './geometry/Mat33';
import Path from './geometry/Path';
import Rect2 from './geometry/Rect2';
import { Vec2 } from './geometry/Vec2';
// Size of a loaded image if no size is specified.

@@ -21,2 +24,3 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);

export const svgAttributesDataKey = 'svgAttrs';
export const svgStyleAttributesDataKey = 'svgStyleAttrs';
export default class SVGLoader {

@@ -88,5 +92,5 @@ constructor(source, onFinish) {

}
attachUnrecognisedAttrs(elem, node, supportedAttrs) {
attachUnrecognisedAttrs(elem, node, supportedAttrs, supportedStyleAttrs) {
for (const attr of node.getAttributeNames()) {
if (supportedAttrs.has(attr)) {
if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
continue;

@@ -96,2 +100,18 @@ }

}
if (supportedStyleAttrs) {
for (const attr of node.style) {
if (attr === '' || !attr) {
continue;
}
if (supportedStyleAttrs.has(attr)) {
continue;
}
// TODO: Do we need special logic for !important properties?
elem.attachLoadSaveData(svgStyleAttributesDataKey, {
key: attr,
value: node.style.getPropertyValue(attr),
priority: node.style.getPropertyPriority(attr)
});
}
}
}

@@ -105,3 +125,4 @@ // Adds a stroke with a single path

elem = new Stroke(strokeData);
this.attachUnrecognisedAttrs(elem, node, new Set(['stroke', 'fill', 'stroke-width', 'd']));
const supportedStyleAttrs = ['stroke', 'fill', 'stroke-width'];
this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStyleAttrs, 'd']), new Set(supportedStyleAttrs));
}

@@ -114,2 +135,70 @@ catch (e) {

}
makeText(elem) {
var _a;
const contentList = [];
for (const child of elem.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
contentList.push((_a = child.nodeValue) !== null && _a !== void 0 ? _a : '');
}
else if (child.nodeType === Node.ELEMENT_NODE) {
const subElem = child;
if (subElem.tagName.toLowerCase() === 'tspan') {
contentList.push(this.makeText(subElem));
}
else {
throw new Error(`Unrecognized text child element: ${subElem}`);
}
}
else {
throw new Error(`Unrecognized text child node: ${child}.`);
}
}
// Compute styles.
const computedStyles = window.getComputedStyle(elem);
const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
const supportedStyleAttrs = [
'fontFamily',
'fill',
'transform'
];
let fontSize = 12;
if (fontSizeMatch) {
supportedStyleAttrs.push('fontSize');
fontSize = parseFloat(fontSizeMatch[1]);
}
const style = {
size: fontSize,
fontFamily: computedStyles.fontFamily || 'sans',
renderingStyle: {
fill: Color4.fromString(computedStyles.fill)
},
};
// Compute transform matrix
let transform = Mat33.fromCSSMatrix(computedStyles.transform);
const supportedAttrs = [];
const elemX = elem.getAttribute('x');
const elemY = elem.getAttribute('y');
if (elemX && elemY) {
const x = parseFloat(elemX);
const y = parseFloat(elemY);
if (!isNaN(x) && !isNaN(y)) {
supportedAttrs.push('x', 'y');
transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
}
}
const result = new Text(contentList, transform, style);
this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
return result;
}
addText(elem) {
var _a;
try {
const textElem = this.makeText(elem);
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
}
catch (e) {
console.error('Invalid text object in node', elem, '. Adding as an unknown object. Error:', e);
this.addUnknownNode(elem);
}
}
addUnknownNode(node) {

@@ -126,3 +215,3 @@ var _a;

}
const components = viewBoxAttr.split(/[ \t,]/);
const components = viewBoxAttr.split(/[ \t\n,]+/);
const x = parseFloat(components[0]);

@@ -133,2 +222,3 @@ const y = parseFloat(components[1]);

if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
console.warn(`node ${node} has an unparsable viewbox. Viewbox: ${viewBoxAttr}. Match: ${components}.`);
return;

@@ -147,2 +237,3 @@ }

this.totalToProcess += node.childElementCount;
let visitChildren = true;
switch (node.tagName.toLowerCase()) {

@@ -155,2 +246,6 @@ case 'g':

break;
case 'text':
this.addText(node);
visitChildren = false;
break;
case 'svg':

@@ -168,4 +263,6 @@ this.updateViewBox(node);

}
for (const child of node.children) {
yield this.visit(child);
if (visitChildren) {
for (const child of node.children) {
yield this.visit(child);
}
}

@@ -237,3 +334,5 @@ this.processedCount++;

svgElem.innerHTML = text;
sandboxDoc.body.appendChild(svgElem);
return new SVGLoader(svgElem, () => {
svgElem.remove();
sandbox.remove();

@@ -240,0 +339,0 @@ });

@@ -18,6 +18,3 @@ export const loadExpectExtensions = () => {

message: () => {
if (pass) {
return `Expected ${expected} not to .eq ${actual}. Options(${eqArgs})`;
}
return `Expected ${expected} to .eq ${actual}. Options(${eqArgs})`;
return `Expected ${pass ? '!' : ''}(${actual}).eq(${expected}). Options(${eqArgs})`;
},

@@ -24,0 +21,0 @@ };

@@ -13,6 +13,7 @@ import { ToolType } from '../tools/ToolController';

import { defaultToolbarLocalization } from './localization';
import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon } from './icons';
import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon, makeTextIcon } from './icons';
import PanZoom, { PanZoomMode } from '../tools/PanZoom';
import Mat33 from '../geometry/Mat33';
import Viewport from '../Viewport';
import TextTool from '../tools/TextTool';
const toolbarCSSPrefix = 'toolbar-';

@@ -320,2 +321,46 @@ class ToolbarWidget {

}
class TextToolWidget extends ToolbarWidget {
constructor(editor, tool, localization) {
super(editor, tool, localization);
this.tool = tool;
this.updateDropdownInputs = null;
editor.notifier.on(EditorEventType.ToolUpdated, evt => {
var _a;
if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
this.updateIcon();
(_a = this.updateDropdownInputs) === null || _a === void 0 ? void 0 : _a.call(this);
}
});
}
getTitle() {
return this.targetTool.description;
}
createIcon() {
const textStyle = this.tool.getTextStyle();
return makeTextIcon(textStyle);
}
fillDropdown(dropdown) {
const colorRow = document.createElement('div');
const colorInput = document.createElement('input');
const colorLabel = document.createElement('label');
colorLabel.innerText = this.localizationTable.colorLabel;
colorInput.classList.add('coloris_input');
colorInput.type = 'button';
colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
colorLabel.setAttribute('for', colorInput.id);
colorInput.oninput = () => {
this.tool.setColor(Color4.fromString(colorInput.value));
};
colorRow.appendChild(colorLabel);
colorRow.appendChild(colorInput);
this.updateDropdownInputs = () => {
const style = this.tool.getTextStyle();
colorInput.value = style.renderingStyle.fill.toHexString();
};
this.updateDropdownInputs();
dropdown.appendChild(colorRow);
return true;
}
}
TextToolWidget.idCounter = 0;
class PenWidget extends ToolbarWidget {

@@ -564,2 +609,8 @@ constructor(editor, tool, localization, penTypes) {

}
for (const tool of toolController.getMatchingTools(ToolType.Text)) {
if (!(tool instanceof TextTool)) {
throw new Error('All text tools must have kind === ToolType.Text');
}
(new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
}
for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {

@@ -566,0 +617,0 @@ if (!(tool instanceof PanZoom)) {

import { ComponentBuilderFactory } from '../components/builders/types';
import { TextStyle } from '../components/Text';
import Pen from '../tools/Pen';

@@ -9,3 +10,4 @@ export declare const makeUndoIcon: () => SVGSVGElement;

export declare const makeHandToolIcon: () => SVGSVGElement;
export declare const makeTextIcon: (textStyle: TextStyle) => SVGSVGElement;
export declare const makePenIcon: (tipThickness: number, color: string) => SVGSVGElement;
export declare const makeIconFromFactory: (pen: Pen, factory: ComponentBuilderFactory) => SVGSVGElement;

@@ -114,2 +114,19 @@ import EventDispatcher from '../EventDispatcher';

};
export const makeTextIcon = (textStyle) => {
var _a, _b;
const icon = document.createElementNS(svgNamespace, 'svg');
icon.setAttribute('viewBox', '0 0 100 100');
const textNode = document.createElementNS(svgNamespace, 'text');
textNode.appendChild(document.createTextNode('T'));
textNode.style.fontFamily = textStyle.fontFamily;
textNode.style.fontWeight = (_a = textStyle.fontWeight) !== null && _a !== void 0 ? _a : '';
textNode.style.fontVariant = (_b = textStyle.fontVariant) !== null && _b !== void 0 ? _b : '';
textNode.style.fill = textStyle.renderingStyle.fill.toHexString();
textNode.style.textAnchor = 'middle';
textNode.setAttribute('x', '50');
textNode.setAttribute('y', '75');
textNode.style.fontSize = '65px';
icon.appendChild(textNode);
return icon;
};
export const makePenIcon = (tipThickness, color) => {

@@ -116,0 +133,0 @@ const icon = document.createElementNS(svgNamespace, 'svg');

export interface ToolLocalization {
RightClickDragPanTool: string;
rightClickDragPanTool: string;
penTool: (penId: number) => string;

@@ -9,2 +9,4 @@ selectionTool: string;

undoRedoTool: string;
textTool: string;
enterTextToInsert: string;
toolEnabledAnnouncement: (toolName: string) => string;

@@ -11,0 +13,0 @@ toolDisabledAnnouncement: (toolName: string) => string;

@@ -8,5 +8,7 @@ export const defaultToolLocalization = {

undoRedoTool: 'Undo/Redo',
RightClickDragPanTool: 'Right-click drag',
rightClickDragPanTool: 'Right-click drag',
textTool: 'Text',
enterTextToInsert: 'Text to insert',
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
};

@@ -399,3 +399,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

const selectionRect = this.selectionBox.region;
this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
this.editor.viewport.zoomTo(selectionRect, false).apply(this.editor);
}

@@ -402,0 +402,0 @@ }

@@ -10,3 +10,4 @@ import { InputEvt } from '../types';

PanZoom = 3,
UndoRedoShortcut = 4
Text = 4,
UndoRedoShortcut = 5
}

@@ -13,0 +14,0 @@ export default class ToolController {

@@ -9,2 +9,3 @@ import { InputEvtType, EditorEventType } from '../types';

import UndoRedoShortcut from './UndoRedoShortcut';
import TextTool from './TextTool';
export var ToolType;

@@ -16,3 +17,4 @@ (function (ToolType) {

ToolType[ToolType["PanZoom"] = 3] = "PanZoom";
ToolType[ToolType["UndoRedoShortcut"] = 4] = "UndoRedoShortcut";
ToolType[ToolType["Text"] = 4] = "Text";
ToolType[ToolType["UndoRedoShortcut"] = 5] = "UndoRedoShortcut";
})(ToolType || (ToolType = {}));

@@ -32,2 +34,3 @@ export default class ToolController {

new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
new TextTool(editor, localization.textTool, localization),
];

@@ -34,0 +37,0 @@ this.tools = [

@@ -38,3 +38,3 @@ import Command from './commands/Command';

roundPoint(point: Point2): Point2;
zoomTo(toMakeVisible: Rect2): Command;
zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;
}

@@ -41,0 +41,0 @@ export declare namespace Viewport {

@@ -88,3 +88,3 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {

// Returns null if no transformation is necessary
zoomTo(toMakeVisible) {
zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
let transform = Mat33.identity;

@@ -108,3 +108,3 @@ if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {

const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
if (largerThanTarget || muchSmallerThanTarget) {
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
// If larger than the target, ensure that the longest axis is visible.

@@ -111,0 +111,0 @@ // If smaller, shrink the visible rectangle as much as possible

{
"name": "js-draw",
"version": "0.1.2",
"version": "0.1.3",
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",

@@ -5,0 +5,0 @@ "main": "dist/src/Editor.js",

@@ -39,4 +39,4 @@ # js-draw

```html
<!-- Replace 0.0.5 with the latest version of js-draw -->
<script src="https://cdn.jsdelivr.net/npm/js-draw@0.0.5/dist/bundle.js"></script>
<!-- Replace 0.1.2 with the latest version of js-draw -->
<script src="https://cdn.jsdelivr.net/npm/js-draw@0.1.2/dist/bundle.js"></script>
<script>

@@ -43,0 +43,0 @@ const editor = new jsdraw.Editor(document.body);

export interface ImageComponentLocalization {
text: (text: string)=> string;
stroke: string;

@@ -9,2 +10,3 @@ svgObject: string;

svgObject: 'SVG Object',
text: (text) => `Text object: ${text}`,
};

@@ -23,3 +23,2 @@ import LineSegment2 from '../geometry/LineSegment2';

console.log('Rendering to SVG.', this.attrs);
for (const [ attr, value ] of this.attrs) {

@@ -26,0 +25,0 @@ canvas.setRootSVGAttribute(attr, value);

@@ -377,5 +377,7 @@

this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
if (showImageBounds) {
const exportRectFill = { fill: Color4.fromHex('#44444455') };
const exportRectStrokeWidth = 12;
const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
renderer.drawRect(

@@ -388,4 +390,2 @@ this.importExportViewport.visibleRect,

//this.image.render(renderer, this.viewport);
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
this.rerenderQueued = false;

@@ -392,0 +392,0 @@ }

@@ -144,2 +144,46 @@ import Mat33 from './Mat33';

});
it('should convert CSS matrix(...) strings to matricies', () => {
// From MDN:
// ⎡ a c e ⎤
// ⎢ b d f ⎥ = matrix(a,b,c,d,e,f)
// ⎣ 0 0 1 ⎦
const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0)');
expect(identity).objEq(Mat33.identity);
expect(Mat33.fromCSSMatrix('matrix(1, 2, 3, 4, 5, 6)')).objEq(new Mat33(
1, 3, 5,
2, 4, 6,
0, 0, 1,
));
expect(Mat33.fromCSSMatrix('matrix(1e2, 2, 3, 4, 5, 6)')).objEq(new Mat33(
1e2, 3, 5,
2, 4, 6,
0, 0, 1,
));
expect(Mat33.fromCSSMatrix('matrix(1.6, 2, .3, 4, 5, 6)')).objEq(new Mat33(
1.6, .3, 5,
2, 4, 6,
0, 0, 1,
));
expect(Mat33.fromCSSMatrix('matrix(-1, 2, 3.E-2, 4, -5.123, -6.5)')).objEq(new Mat33(
-1, 0.03, -5.123,
2, 4, -6.5,
0, 0, 1,
));
expect(Mat33.fromCSSMatrix('matrix(1.6,\n\t2, .3, 4, 5, 6)')).objEq(new Mat33(
1.6, .3, 5,
2, 4, 6,
0, 0, 1,
));
expect(Mat33.fromCSSMatrix('matrix(1.6,2, .3E-2, 4, 5, 6)')).objEq(new Mat33(
1.6, 3e-3, 5,
2, 4, 6,
0, 0, 1,
));
expect(Mat33.fromCSSMatrix('matrix(-1, 2e6, 3E-2,-5.123, -6.5e-1, 0.01)')).objEq(new Mat33(
-1, 3E-2, -6.5e-1,
2e6, -5.123, 0.01,
0, 0, 1,
));
});
});

@@ -271,2 +271,43 @@ import { Point2, Vec2 } from './Vec2';

}
// Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
public static fromCSSMatrix(cssString: string): Mat33 {
if (cssString === '' || cssString === 'none') {
return Mat33.identity;
}
const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
const numberSepExp = '[, \\t\\n]';
const regExpSource = `^\\s*matrix\\s*\\(${
[
// According to MDN, matrix(a,b,c,d,e,f) has form:
// ⎡ a c e ⎤
// ⎢ b d f ⎥
// ⎣ 0 0 1 ⎦
numberExp, numberExp, numberExp, // a, c, e
numberExp, numberExp, numberExp, // b, d, f
].join(`${numberSepExp}+`)
}${numberSepExp}*\\)\\s*$`;
const matrixExp = new RegExp(regExpSource, 'i');
const match = matrixExp.exec(cssString);
if (!match) {
throw new Error(`Unsupported transformation: ${cssString}`);
}
const matrixData = match.slice(1).map(entry => parseFloat(entry));
const a = matrixData[0];
const b = matrixData[1];
const c = matrixData[2];
const d = matrixData[3];
const e = matrixData[4];
const f = matrixData[5];
const transform = new Mat33(
a, c, e,
b, d, f,
0, 0, 1
);
return transform;
}
}

@@ -22,10 +22,14 @@ import Path, { PathCommandType } from './Path';

it('should fix rounding errors', () => {
const path = new Path(Vec2.of(0.10000001, 0.19999999), [
const path = new Path(Vec2.of(0.100000001, 0.199999999), [
{
kind: PathCommandType.QuadraticBezierTo,
controlPoint: Vec2.of(9999, -10.999999995),
endPoint: Vec2.of(0.000300001, 1.40000002),
endPoint: Vec2.of(0.000300001, 1.400000002),
},
{
kind: PathCommandType.LineTo,
point: Vec2.of(184.00482359999998, 1)
}
]);
expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4');
expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4L184.0048236,1');
});

@@ -32,0 +36,0 @@

@@ -306,2 +306,4 @@ import { Bezier } from 'bezier-js';

const origPostDecimalString = roundingDownMatch[3];
let newPostDecimal = (postDecimal + 10 - lastDigit).toString();

@@ -314,2 +316,9 @@ let carry = 0;

}
// parseInt(...).toString() removes leading zeroes. Add them back.
while (newPostDecimal.length < origPostDecimalString.length) {
newPostDecimal = carry.toString(10) + newPostDecimal;
carry = 0;
}
text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;

@@ -321,3 +330,4 @@ }

// Remove trailing zeroes
text = text.replace(/([.][^0]*)0+$/, '$1');
text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
text = text.replace(/[.]0+$/, '.');

@@ -324,0 +334,0 @@ // Remove trailing period

@@ -187,2 +187,10 @@ import LineSegment2 from './LineSegment2';

public get width() {
return this.w;
}
public get height() {
return this.h;
}
// Returns edges in the order

@@ -189,0 +197,0 @@ // [ rightEdge, topEdge, leftEdge, bottomEdge ]

import Color4 from '../../Color4';
import { LoadSaveDataTable } from '../../components/AbstractComponent';
import { TextStyle } from '../../components/Text';
import Mat33 from '../../geometry/Mat33';

@@ -32,2 +33,3 @@ import Path, { PathCommand, PathCommandType } from '../../geometry/Path';

private selfTransform: Mat33|null = null;
private transformStack: Array<Mat33|null> = [];

@@ -55,2 +57,3 @@ protected constructor(private viewport: Viewport) { }

): void;
public abstract drawText(text: string, transform: Mat33, style: TextStyle): void;

@@ -171,2 +174,15 @@ // Returns true iff the given rectangle is so small, rendering anything within

public pushTransform(transform: Mat33) {
this.transformStack.push(this.selfTransform);
this.setTransform(this.getCanvasToScreenTransform().rightMul(transform));
}
public popTransform() {
if (this.transformStack.length === 0) {
throw new Error('Unable to pop more transforms than have been pushed!');
}
this.setTransform(this.transformStack.pop() ?? null);
}
// Get the matrix that transforms a vector on the canvas to a vector on this'

@@ -173,0 +189,0 @@ // rendering target.

import Color4 from '../../Color4';
import Text, { TextStyle } from '../../components/Text';
import Mat33 from '../../geometry/Mat33';

@@ -29,2 +30,15 @@ import Rect2 from '../../geometry/Rect2';

private transformBy(transformBy: Mat33) {
// From MDN, transform(a,b,c,d,e,f)
// takes input such that
// ⎡ a c e ⎤
// ⎢ b d f ⎥ transforms content drawn to [ctx].
// ⎣ 0 0 1 ⎦
this.ctx.transform(
transformBy.a1, transformBy.b1, // a, b
transformBy.a2, transformBy.b2, // c, d
transformBy.a3, transformBy.b3, // e, f
);
}
public canRenderFromWithoutDataLoss(other: AbstractRenderer) {

@@ -40,12 +54,3 @@ return other instanceof CanvasRenderer;

this.ctx.save();
// From MDN, transform(a,b,c,d,e,f)
// takes input such that
// ⎡ a c e ⎤
// ⎢ b d f ⎥ transforms content drawn to [ctx].
// ⎣ 0 0 1 ⎦
this.ctx.transform(
transformBy.a1, transformBy.b1, // a, b
transformBy.a2, transformBy.b2, // c, d
transformBy.a3, transformBy.b3, // e, f
);
this.transformBy(transformBy);
this.ctx.drawImage(other.ctx.canvas, 0, 0);

@@ -148,2 +153,21 @@ this.ctx.restore();

public drawText(text: string, transform: Mat33, style: TextStyle): void {
this.ctx.save();
transform = this.getCanvasToScreenTransform().rightMul(transform);
this.transformBy(transform);
Text.applyTextStyles(this.ctx, style);
if (style.renderingStyle.fill.a !== 0) {
this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
this.ctx.fillText(text, 0, 0);
}
if (style.renderingStyle.stroke) {
this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
this.ctx.lineWidth = style.renderingStyle.stroke.width;
this.ctx.strokeText(text, 0, 0);
}
this.ctx.restore();
}
private clipLevels: number[] = [];

@@ -150,0 +174,0 @@ public startObject(boundingBox: Rect2, clip: boolean) {

// Renderer that outputs nothing. Useful for automated tests.
import { TextStyle } from '../../components/Text';
import Mat33 from '../../geometry/Mat33';

@@ -17,2 +18,3 @@ import Rect2 from '../../geometry/Rect2';

public objectNestingLevel: number = 0;
public lastText: string|null = null;

@@ -44,2 +46,3 @@ // List of points drawn since the last clear.

this.pointBuffer = [];
this.lastText = null;

@@ -93,2 +96,7 @@ // Ensure all objects finished rendering

public drawText(text: string, _transform: Mat33, _style: TextStyle): void {
this.lastText = text;
}
public startObject(boundingBox: Rect2, _clip: boolean) {

@@ -95,0 +103,0 @@ super.startObject(boundingBox);

import { LoadSaveDataTable } from '../../components/AbstractComponent';
import { TextStyle } from '../../components/Text';
import Mat33 from '../../geometry/Mat33';
import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
import Rect2 from '../../geometry/Rect2';
import { Point2, Vec2 } from '../../geometry/Vec2';
import { svgAttributesDataKey, SVGLoaderUnknownAttribute } from '../../SVGLoader';
import { svgAttributesDataKey, SVGLoaderUnknownAttribute, SVGLoaderUnknownStyleAttribute, svgStyleAttributesDataKey } from '../../SVGLoader';
import Viewport from '../../Viewport';

@@ -110,2 +112,28 @@ import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';

public drawText(text: string, transform: Mat33, style: TextStyle): void {
transform = this.getCanvasToScreenTransform().rightMul(transform);
const textElem = document.createElementNS(svgNameSpace, 'text');
textElem.appendChild(document.createTextNode(text));
textElem.style.transform = `matrix(
${transform.a1}, ${transform.b1},
${transform.a2}, ${transform.b2},
${transform.a3}, ${transform.b3}
)`;
textElem.style.fontFamily = style.fontFamily;
textElem.style.fontVariant = style.fontVariant ?? '';
textElem.style.fontWeight = style.fontWeight ?? '';
textElem.style.fontSize = style.size + 'px';
textElem.style.fill = style.renderingStyle.fill.toHexString();
if (style.renderingStyle.stroke) {
const strokeStyle = style.renderingStyle.stroke;
textElem.style.stroke = strokeStyle.color.toHexString();
textElem.style.strokeWidth = strokeStyle.width + 'px';
}
this.elem.appendChild(textElem);
this.objectElems?.push(textElem);
}
public startObject(boundingBox: Rect2) {

@@ -131,2 +159,3 @@ super.startObject(boundingBox);

const attrs = loaderData[svgAttributesDataKey] as SVGLoaderUnknownAttribute[]|undefined;
const styleAttrs = loaderData[svgStyleAttributesDataKey] as SVGLoaderUnknownStyleAttribute[]|undefined;

@@ -138,2 +167,8 @@ if (attrs) {

}
if (styleAttrs) {
for (const attr of styleAttrs) {
elem.style.setProperty(attr.key, attr.value, attr.priority);
}
}
}

@@ -140,0 +175,0 @@ }

@@ -5,5 +5,8 @@ import Color4 from './Color4';

import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
import Text, { TextStyle } from './components/Text';
import UnknownSVGObject from './components/UnknownSVGObject';
import Mat33 from './geometry/Mat33';
import Path from './geometry/Path';
import Rect2 from './geometry/Rect2';
import { Vec2 } from './geometry/Vec2';
import { RenderablePathSpec, RenderingStyle } from './rendering/renderers/AbstractRenderer';

@@ -19,2 +22,3 @@ import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';

export const svgAttributesDataKey = 'svgAttrs';
export const svgStyleAttributesDataKey = 'svgStyleAttrs';

@@ -24,2 +28,5 @@ // [key, value]

// [key, value, priority]
export type SVGLoaderUnknownStyleAttribute = { key: string, value: string, priority?: string };
export default class SVGLoader implements ImageLoader {

@@ -103,6 +110,7 @@ private onAddComponent: ComponentAddedListener|null = null;

node: SVGElement,
supportedAttrs: Set<string>
supportedAttrs: Set<string>,
supportedStyleAttrs?: Set<string>
) {
for (const attr of node.getAttributeNames()) {
if (supportedAttrs.has(attr)) {
if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
continue;

@@ -115,2 +123,23 @@ }

}
if (supportedStyleAttrs) {
for (const attr of node.style) {
if (attr === '' || !attr) {
continue;
}
if (supportedStyleAttrs.has(attr)) {
continue;
}
// TODO: Do we need special logic for !important properties?
elem.attachLoadSaveData(svgStyleAttributesDataKey,
{
key: attr,
value: node.style.getPropertyValue(attr),
priority: node.style.getPropertyPriority(attr)
} as SVGLoaderUnknownStyleAttribute
);
}
}
}

@@ -123,5 +152,10 @@

const strokeData = this.strokeDataFromElem(node);
elem = new Stroke(strokeData);
const supportedStyleAttrs = [ 'stroke', 'fill', 'stroke-width' ];
this.attachUnrecognisedAttrs(
elem, node, new Set([ 'stroke', 'fill', 'stroke-width', 'd' ]),
elem, node,
new Set([ ...supportedStyleAttrs, 'd' ]),
new Set(supportedStyleAttrs)
);

@@ -140,2 +174,76 @@ } catch (e) {

private makeText(elem: SVGTextElement|SVGTSpanElement): Text {
const contentList: Array<Text|string> = [];
for (const child of elem.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
contentList.push(child.nodeValue ?? '');
} else if (child.nodeType === Node.ELEMENT_NODE) {
const subElem = child as SVGElement;
if (subElem.tagName.toLowerCase() === 'tspan') {
contentList.push(this.makeText(subElem as SVGTSpanElement));
} else {
throw new Error(`Unrecognized text child element: ${subElem}`);
}
} else {
throw new Error(`Unrecognized text child node: ${child}.`);
}
}
// Compute styles.
const computedStyles = window.getComputedStyle(elem);
const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
const supportedStyleAttrs = [
'fontFamily',
'fill',
'transform'
];
let fontSize = 12;
if (fontSizeMatch) {
supportedStyleAttrs.push('fontSize');
fontSize = parseFloat(fontSizeMatch[1]);
}
const style: TextStyle = {
size: fontSize,
fontFamily: computedStyles.fontFamily || 'sans',
renderingStyle: {
fill: Color4.fromString(computedStyles.fill)
},
};
// Compute transform matrix
let transform = Mat33.fromCSSMatrix(computedStyles.transform);
const supportedAttrs = [];
const elemX = elem.getAttribute('x');
const elemY = elem.getAttribute('y');
if (elemX && elemY) {
const x = parseFloat(elemX);
const y = parseFloat(elemY);
if (!isNaN(x) && !isNaN(y)) {
supportedAttrs.push('x', 'y');
transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
}
}
const result = new Text(contentList, transform, style);
this.attachUnrecognisedAttrs(
result,
elem,
new Set(supportedAttrs),
new Set(supportedStyleAttrs)
);
return result;
}
private addText(elem: SVGTextElement|SVGTSpanElement) {
try {
const textElem = this.makeText(elem);
this.onAddComponent?.(textElem);
} catch (e) {
console.error('Invalid text object in node', elem, '. Adding as an unknown object. Error:', e);
this.addUnknownNode(elem);
}
}
private addUnknownNode(node: SVGElement) {

@@ -152,3 +260,3 @@ const component = new UnknownSVGObject(node);

const components = viewBoxAttr.split(/[ \t,]/);
const components = viewBoxAttr.split(/[ \t\n,]+/);
const x = parseFloat(components[0]);

@@ -160,2 +268,3 @@ const y = parseFloat(components[1]);

if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
console.warn(`node ${node} has an unparsable viewbox. Viewbox: ${viewBoxAttr}. Match: ${components}.`);
return;

@@ -174,2 +283,3 @@ }

this.totalToProcess += node.childElementCount;
let visitChildren = true;

@@ -183,2 +293,6 @@ switch (node.tagName.toLowerCase()) {

break;
case 'text':
this.addText(node as SVGTextElement);
visitChildren = false;
break;
case 'svg':

@@ -198,4 +312,6 @@ this.updateViewBox(node as SVGSVGElement);

for (const child of node.children) {
await this.visit(child);
if (visitChildren) {
for (const child of node.children) {
await this.visit(child);
}
}

@@ -280,4 +396,6 @@

svgElem.innerHTML = text;
sandboxDoc.body.appendChild(svgElem);
return new SVGLoader(svgElem, () => {
svgElem.remove();
sandbox.remove();

@@ -284,0 +402,0 @@ });

@@ -18,6 +18,3 @@ export const loadExpectExtensions = () => {

message: () => {
if (pass) {
return `Expected ${expected} not to .eq ${actual}. Options(${eqArgs})`;
}
return `Expected ${expected} to .eq ${actual}. Options(${eqArgs})`;
return `Expected ${pass ? '!' : ''}(${actual}).eq(${expected}). Options(${eqArgs})`;
},

@@ -24,0 +21,0 @@ };

@@ -18,6 +18,7 @@ import Editor from '../Editor';

import { ActionButtonIcon } from './types';
import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon } from './icons';
import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon, makeTextIcon } from './icons';
import PanZoom, { PanZoomMode } from '../tools/PanZoom';
import Mat33 from '../geometry/Mat33';
import Viewport from '../Viewport';
import TextTool from '../tools/TextTool';

@@ -407,2 +408,56 @@

class TextToolWidget extends ToolbarWidget {
private updateDropdownInputs: (()=>void)|null = null;
public constructor(editor: Editor, private tool: TextTool, localization: ToolbarLocalization) {
super(editor, tool, localization);
editor.notifier.on(EditorEventType.ToolUpdated, evt => {
if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
this.updateIcon();
this.updateDropdownInputs?.();
}
});
}
protected getTitle(): string {
return this.targetTool.description;
}
protected createIcon(): Element {
const textStyle = this.tool.getTextStyle();
return makeTextIcon(textStyle);
}
private static idCounter: number = 0;
protected fillDropdown(dropdown: HTMLElement): boolean {
const colorRow = document.createElement('div');
const colorInput = document.createElement('input');
const colorLabel = document.createElement('label');
colorLabel.innerText = this.localizationTable.colorLabel;
colorInput.classList.add('coloris_input');
colorInput.type = 'button';
colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
colorLabel.setAttribute('for', colorInput.id);
colorInput.oninput = () => {
this.tool.setColor(Color4.fromString(colorInput.value));
};
colorRow.appendChild(colorLabel);
colorRow.appendChild(colorInput);
this.updateDropdownInputs = () => {
const style = this.tool.getTextStyle();
colorInput.value = style.renderingStyle.fill.toHexString();
};
this.updateDropdownInputs();
dropdown.appendChild(colorRow);
return true;
}
}
class PenWidget extends ToolbarWidget {

@@ -707,2 +762,10 @@ private updateInputs: ()=> void = () => {};

for (const tool of toolController.getMatchingTools(ToolType.Text)) {
if (!(tool instanceof TextTool)) {
throw new Error('All text tools must have kind === ToolType.Text');
}
(new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
}
for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {

@@ -709,0 +772,0 @@ if (!(tool instanceof PanZoom)) {

import { ComponentBuilderFactory } from '../components/builders/types';
import { TextStyle } from '../components/Text';
import EventDispatcher from '../EventDispatcher';

@@ -129,2 +130,24 @@ import { Vec2 } from '../geometry/Vec2';

export const makeTextIcon = (textStyle: TextStyle) => {
const icon = document.createElementNS(svgNamespace, 'svg');
icon.setAttribute('viewBox', '0 0 100 100');
const textNode = document.createElementNS(svgNamespace, 'text');
textNode.appendChild(document.createTextNode('T'));
textNode.style.fontFamily = textStyle.fontFamily;
textNode.style.fontWeight = textStyle.fontWeight ?? '';
textNode.style.fontVariant = textStyle.fontVariant ?? '';
textNode.style.fill = textStyle.renderingStyle.fill.toHexString();
textNode.style.textAnchor = 'middle';
textNode.setAttribute('x', '50');
textNode.setAttribute('y', '75');
textNode.style.fontSize = '65px';
icon.appendChild(textNode);
return icon;
};
export const makePenIcon = (tipThickness: number, color: string) => {

@@ -131,0 +154,0 @@ const icon = document.createElementNS(svgNamespace, 'svg');

export interface ToolLocalization {
RightClickDragPanTool: string;
rightClickDragPanTool: string;
penTool: (penId: number)=>string;

@@ -10,2 +10,4 @@ selectionTool: string;

undoRedoTool: string;
textTool: string;
enterTextToInsert: string;

@@ -23,6 +25,9 @@ toolEnabledAnnouncement: (toolName: string) => string;

undoRedoTool: 'Undo/Redo',
RightClickDragPanTool: 'Right-click drag',
rightClickDragPanTool: 'Right-click drag',
textTool: 'Text',
enterTextToInsert: 'Text to insert',
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
};

@@ -497,3 +497,3 @@ import Command from '../commands/Command';

const selectionRect = this.selectionBox.region;
this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
this.editor.viewport.zoomTo(selectionRect, false).apply(this.editor);
}

@@ -500,0 +500,0 @@ }

@@ -12,2 +12,3 @@ import { InputEvtType, InputEvt, EditorEventType } from '../types';

import UndoRedoShortcut from './UndoRedoShortcut';
import TextTool from './TextTool';

@@ -19,2 +20,3 @@ export enum ToolType {

PanZoom,
Text,
UndoRedoShortcut,

@@ -41,2 +43,4 @@ }

new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
new TextTool(editor, localization.textTool, localization),
];

@@ -43,0 +47,0 @@ this.tools = [

@@ -173,3 +173,3 @@ import Command from './commands/Command';

// Returns null if no transformation is necessary
public zoomTo(toMakeVisible: Rect2): Command {
public zoomTo(toMakeVisible: Rect2, allowZoomIn: boolean = true, allowZoomOut: boolean = true): Command {
let transform = Mat33.identity;

@@ -199,3 +199,3 @@

if (largerThanTarget || muchSmallerThanTarget) {
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
// If larger than the target, ensure that the longest axis is visible.

@@ -202,0 +202,0 @@ // If smaller, shrink the visible rectangle as much as possible

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc