Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

js-draw

Package Overview
Dependencies
Maintainers
1
Versions
119
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.6.0 to 0.7.0

.firebase/hosting.ZG9jcw.cache

2

.github/ISSUE_TEMPLATE/translation.md

@@ -49,3 +49,3 @@ ---

colorChangedAnnouncement: t=>`Color changed to ${t}`,
penTool: t=>`Pen ${t}`,
penTool: penNumber=>`Pen ${penNumber}`,
selectionTool: "Selection",

@@ -52,0 +52,0 @@ eraserTool: "Eraser",

@@ -0,1 +1,9 @@

# 0.7.0
* Text tool
* Edit existing text.
* Shift+enter to insert a new line.
* Preserve multi-line text when loading/saving
* Pen
* Decrease smoothing amount for thick strokes.
# 0.6.0

@@ -2,0 +10,0 @@ * Selection tool:

import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import Rect2 from '../../math/Rect2';
import Stroke from '../Stroke';
import Viewport from '../../Viewport';
import { StrokeDataPoint } from '../../types';

@@ -11,2 +12,3 @@ import { ComponentBuilder, ComponentBuilderFactory } from './types';

private maxFitAllowed;
private viewport;
private isFirstSegment;

@@ -17,2 +19,5 @@ private pathStartConnector;

private lowerSegments;
private lastUpperBezier;
private lastLowerBezier;
private parts;
private buffer;

@@ -26,6 +31,7 @@ private lastPoint;

private bbox;
constructor(startPoint: StrokeDataPoint, minFitAllowed: number, maxFitAllowed: number);
constructor(startPoint: StrokeDataPoint, minFitAllowed: number, maxFitAllowed: number, viewport: Viewport);
getBBox(): Rect2;
private getRenderingStyle;
private previewPath;
private previewCurrentPath;
private previewFullPath;
private previewStroke;

@@ -35,2 +41,3 @@ preview(renderer: AbstractRenderer): void;

private roundPoint;
private shouldStartNewSegment;
private approxCurrentCurveLength;

@@ -37,0 +44,0 @@ private finalizeCurrentCurve;

@@ -9,7 +9,7 @@ import { Bezier } from 'bezier-js';

export const makeFreehandLineBuilder = (initialPoint, viewport) => {
// Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
// Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
// less than ±1 px from the curve.
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist);
return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport);
};

@@ -23,9 +23,13 @@ // Handles stroke smoothing and creates Strokes from user/stylus input.

// [maxFitAllowed].
minFitAllowed, maxFitAllowed) {
minFitAllowed, maxFitAllowed, viewport) {
this.startPoint = startPoint;
this.minFitAllowed = minFitAllowed;
this.maxFitAllowed = maxFitAllowed;
this.viewport = viewport;
this.isFirstSegment = true;
this.pathStartConnector = null;
this.mostRecentConnector = null;
this.lastUpperBezier = null;
this.lastLowerBezier = null;
this.parts = [];
this.lastExitingVec = null;

@@ -51,12 +55,12 @@ this.currentCurve = null;

}
previewPath() {
previewCurrentPath() {
var _a;
let upperPath;
let lowerPath;
const upperPath = this.upperSegments.slice();
const lowerPath = this.lowerSegments.slice();
let lowerToUpperCap;
let pathStartConnector;
if (this.currentCurve) {
const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
upperPath = this.upperSegments.concat(upperCurve);
lowerPath = this.lowerSegments.concat(lowerCurve);
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand } = this.currentSegmentToPath();
upperPath.push(upperCurveCommand);
lowerPath.push(lowerCurveCommand);
lowerToUpperCap = lowerToUpperConnector;

@@ -69,8 +73,13 @@ pathStartConnector = (_a = this.pathStartConnector) !== null && _a !== void 0 ? _a : upperToLowerConnector;

}
upperPath = this.upperSegments.slice();
lowerPath = this.lowerSegments.slice();
lowerToUpperCap = this.mostRecentConnector;
pathStartConnector = this.pathStartConnector;
}
const startPoint = lowerPath[lowerPath.length - 1].endPoint;
let startPoint;
const lastLowerSegment = lowerPath[lowerPath.length - 1];
if (lastLowerSegment.kind === PathCommandType.LineTo || lastLowerSegment.kind === PathCommandType.MoveTo) {
startPoint = lastLowerSegment.point;
}
else {
startPoint = lastLowerSegment.endPoint;
}
return {

@@ -110,6 +119,13 @@ // Start at the end of the lower curve:

}
previewFullPath() {
const preview = this.previewCurrentPath();
if (preview) {
return [...this.parts, preview];
}
return null;
}
previewStroke() {
const pathPreview = this.previewPath();
const pathPreview = this.previewFullPath();
if (pathPreview) {
return new Stroke([pathPreview]);
return new Stroke(pathPreview);
}

@@ -119,9 +135,14 @@ return null;

preview(renderer) {
const path = this.previewPath();
if (path) {
renderer.drawPath(path);
const paths = this.previewFullPath();
if (paths) {
const approxBBox = this.viewport.visibleRect;
renderer.startObject(approxBBox);
for (const path of paths) {
renderer.drawPath(path);
}
renderer.endObject();
}
}
build() {
if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
if (this.lastPoint) {
this.finalizeCurrentCurve();

@@ -138,2 +159,57 @@ }

}
// Returns true if, due to overlap with previous segments, a new RenderablePathSpec should be created.
shouldStartNewSegment(lowerCurve, upperCurve) {
if (!this.lastLowerBezier || !this.lastUpperBezier) {
return false;
}
const getIntersection = (curve1, curve2) => {
const intersection = curve1.intersects(curve2);
if (!intersection || intersection.length === 0) {
return null;
}
// From http://pomax.github.io/bezierjs/#intersect-curve,
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
const firstTPair = intersection[0];
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
if (!match) {
throw new Error(`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`);
}
const t = parseFloat(match[1]);
return Vec2.ofXY(curve1.get(t));
};
const getExitDirection = (curve) => {
return Vec2.ofXY(curve.points[2]).minus(Vec2.ofXY(curve.points[1])).normalized();
};
const getEnterDirection = (curve) => {
return Vec2.ofXY(curve.points[1]).minus(Vec2.ofXY(curve.points[0])).normalized();
};
// Prevent
// /
// / /
// / / /|
// / / |
// / |
// where the next stroke and the previous stroke are in different directions.
//
// Are the exit/enter directions of the previous and current curves in different enough directions?
if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.3
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.3
// Also handle if the curves exit/enter directions differ
|| getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
|| getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
return true;
}
// Check whether the lower curve intersects the other wall:
// / / ← lower
// / / /
// / / /
// //
// / /
const lowerIntersection = getIntersection(lowerCurve, this.lastUpperBezier);
const upperIntersection = getIntersection(upperCurve, this.lastLowerBezier);
if (lowerIntersection || upperIntersection) {
return true;
}
return false;
}
// Returns the distance between the start, control, and end points of the curve.

@@ -195,4 +271,13 @@ approxCurrentCurveLength() {

}
const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
if (this.isFirstSegment) {
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, } = this.currentSegmentToPath();
const shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
if (shouldStartNew) {
const part = this.previewCurrentPath();
if (part) {
this.parts.push(part);
this.upperSegments = [];
this.lowerSegments = [];
}
}
if (this.isFirstSegment || shouldStartNew) {
// We draw the upper path (reversed), then the lower path, so we need the

@@ -206,4 +291,6 @@ // upperToLowerConnector to join the two paths.

this.mostRecentConnector = lowerToUpperConnector;
this.upperSegments.push(upperCurve);
this.lowerSegments.push(lowerCurve);
this.lowerSegments.push(lowerCurveCommand);
this.upperSegments.push(upperCurveCommand);
this.lastLowerBezier = lowerCurve;
this.lastUpperBezier = upperCurve;
const lastPoint = this.buffer[this.buffer.length - 1];

@@ -250,2 +337,3 @@ this.lastExitingVec = Vec2.ofXY(this.currentCurve.points[2]).minus(Vec2.ofXY(this.currentCurve.points[1]));

// Each starts at startPt ± startVec
const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));

@@ -255,3 +343,4 @@ const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));

const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
const lowerCurve = {
const upperCurveEndPoint = this.roundPoint(startPt.minus(startVec));
const lowerCurveCommand = {
kind: PathCommandType.QuadraticBezierTo,

@@ -264,3 +353,3 @@ controlPoint: lowerCurveControlPoint,

kind: PathCommandType.LineTo,
point: this.roundPoint(startPt.plus(startVec)),
point: lowerCurveStartPoint,
};

@@ -272,8 +361,13 @@ // From the end of lowerCurve to the start of upperCurve:

};
const upperCurve = {
const upperCurveCommand = {
kind: PathCommandType.QuadraticBezierTo,
controlPoint: upperCurveControlPoint,
endPoint: this.roundPoint(startPt.minus(startVec)),
endPoint: upperCurveEndPoint,
};
return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
const upperCurve = new Bezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
const lowerCurve = new Bezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
return {
upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
upperCurve, lowerCurve,
};
}

@@ -315,2 +409,6 @@ // Compute the direction of the velocity at the end of this.buffer

this.curveEndWidth = pointRadius;
if (this.isFirstSegment) {
// The start of a curve often lacks accurate pressure information. Update it.
this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
}
// recompute bbox

@@ -327,2 +425,3 @@ this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);

}
// If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
let enteringVec = this.lastExitingVec;

@@ -329,0 +428,0 @@ if (!enteringVec) {

@@ -6,4 +6,4 @@ export * from './builders/types';

import Stroke from './Stroke';
import Text from './Text';
import TextComponent from './Text';
import ImageComponent from './ImageComponent';
export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };

@@ -6,4 +6,4 @@ export * from './builders/types';

import Stroke from './Stroke';
import Text from './Text';
import TextComponent from './Text';
import ImageComponent from './ImageComponent';
export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };

@@ -6,2 +6,3 @@ import Path from '../math/Path';

export default class Stroke extends AbstractComponent {
// Creates a `Stroke` from the given `parts`.
constructor(parts) {

@@ -8,0 +9,0 @@ var _a;

@@ -15,8 +15,8 @@ import LineSegment2 from '../math/LineSegment2';

}
export default class Text extends AbstractComponent {
protected readonly textObjects: Array<string | Text>;
export default class TextComponent extends AbstractComponent {
protected readonly textObjects: Array<string | TextComponent>;
private transform;
private readonly style;
private style;
protected contentBBox: Rect2;
constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle);
constructor(textObjects: Array<string | TextComponent>, transform: Mat33, style: TextStyle);
static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;

@@ -28,4 +28,8 @@ private static textMeasuringCtx;

private recomputeBBox;
private renderInternal;
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
intersects(lineSegment: LineSegment2): boolean;
getBaselinePos(): import("../lib").Vec3;
getTextStyle(): TextStyle;
getTransform(): Mat33;
protected applyTransformation(affineTransfm: Mat33): void;

@@ -36,3 +40,4 @@ protected createClone(): AbstractComponent;

protected serializeToJSON(): Record<string, any>;
static deserializeFromString(json: any): Text;
static deserializeFromString(json: any): TextComponent;
static fromLines(lines: string[], transform: Mat33, style: TextStyle): AbstractComponent;
}
import LineSegment2 from '../math/LineSegment2';
import Mat33 from '../math/Mat33';
import Rect2 from '../math/Rect2';
import { Vec2 } from '../math/Vec2';
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
import AbstractComponent from './AbstractComponent';
const componentTypeId = 'text';
export default class Text extends AbstractComponent {
export default class TextComponent extends AbstractComponent {
constructor(textObjects, transform, style) {

@@ -14,2 +15,8 @@ super(componentTypeId);

this.recomputeBBox();
// If this has no direct children, choose a style representative of this' content
// (useful for estimating the style of the TextComponent).
const hasDirectContent = textObjects.some(obj => typeof obj === 'string');
if (!hasDirectContent && textObjects.length > 0) {
this.style = textObjects[0].getTextStyle();
}
}

@@ -39,8 +46,8 @@ static applyTextStyles(ctx, style) {

var _a, _b;
(_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = (_b = document.createElement('canvas').getContext('2d')) !== null && _b !== void 0 ? _b : null);
if (!Text.textMeasuringCtx) {
(_a = TextComponent.textMeasuringCtx) !== null && _a !== void 0 ? _a : (TextComponent.textMeasuringCtx = (_b = document.createElement('canvas').getContext('2d')) !== null && _b !== void 0 ? _b : null);
if (!TextComponent.textMeasuringCtx) {
return this.estimateTextDimens(text, style);
}
const ctx = Text.textMeasuringCtx;
Text.applyTextStyles(ctx, style);
const ctx = TextComponent.textMeasuringCtx;
TextComponent.applyTextStyles(ctx, style);
const measure = ctx.measureText(text);

@@ -54,3 +61,3 @@ // Text is drawn with (0,0) at the bottom left of the baseline.

if (typeof part === 'string') {
const textBBox = Text.getTextDimens(part, this.style);
const textBBox = TextComponent.getTextDimens(part, this.style);
return textBBox.transformedBoundingBox(this.transform);

@@ -72,5 +79,4 @@ }

}
render(canvas, _visibleRect) {
renderInternal(canvas) {
const cursor = this.transform;
canvas.startObject(this.contentBBox);
for (const textObject of this.textObjects) {

@@ -82,6 +88,10 @@ if (typeof textObject === 'string') {

canvas.pushTransform(cursor);
textObject.render(canvas);
textObject.renderInternal(canvas);
canvas.popTransform();
}
}
}
render(canvas, _visibleRect) {
canvas.startObject(this.contentBBox);
this.renderInternal(canvas);
canvas.endObject(this.getLoadSaveData());

@@ -97,3 +107,3 @@ }

if (typeof subObject === 'string') {
const textBBox = Text.getTextDimens(subObject, this.style);
const textBBox = TextComponent.getTextDimens(subObject, this.style);
// TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and

@@ -113,2 +123,11 @@ // use pixel-testing to check for intersection with its contour.

}
getBaselinePos() {
return this.transform.transformVec2(Vec2.zero);
}
getTextStyle() {
return this.style;
}
getTransform() {
return this.transform;
}
applyTransformation(affineTransfm) {

@@ -119,3 +138,3 @@ this.transform = affineTransfm.rightMul(this.transform);

createClone() {
return new Text(this.textObjects, this.transform, this.style);
return new TextComponent(this.textObjects, this.transform, this.style);
}

@@ -170,3 +189,3 @@ getText() {

}
return Text.deserializeFromString(data.json);
return TextComponent.deserializeFromString(data.json);
});

@@ -179,6 +198,21 @@ json.transform = json.transform.filter((elem) => typeof elem === 'number');

const transform = new Mat33(...transformData);
return new Text(textObjects, transform, style);
return new TextComponent(textObjects, transform, style);
}
static fromLines(lines, transform, style) {
let lastComponent = null;
const components = [];
for (const line of lines) {
let position = Vec2.zero;
if (lastComponent) {
const lineMargin = Math.floor(style.size);
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
}
const component = new TextComponent([line], Mat33.translation(position), style);
components.push(component);
lastComponent = component;
}
return new TextComponent(components, transform, style);
}
}
Text.textMeasuringCtx = null;
AbstractComponent.registerComponent(componentTypeId, (data) => Text.deserializeFromString(data));
TextComponent.textMeasuringCtx = null;
AbstractComponent.registerComponent(componentTypeId, (data) => TextComponent.deserializeFromString(data));

@@ -96,3 +96,2 @@ /**

* Global event dispatcher/subscriber.
* @see {@link types.EditorEventType}
*/

@@ -99,0 +98,0 @@ readonly notifier: EditorNotifier;

@@ -45,2 +45,3 @@ /**

import IconProvider from './toolbar/IconProvider';
import { toRoundedString } from './math/rounding';
// { @inheritDoc Editor! }

@@ -642,5 +643,5 @@ export class Editor {

const rect = importExportViewport.visibleRect;
result.setAttribute('viewBox', `${rect.x} ${rect.y} ${rect.w} ${rect.h}`);
result.setAttribute('width', `${rect.w}`);
result.setAttribute('height', `${rect.h}`);
result.setAttribute('viewBox', [rect.x, rect.y, rect.w, rect.h].map(part => toRoundedString(part)).join(' '));
result.setAttribute('width', toRoundedString(rect.w));
result.setAttribute('height', toRoundedString(rect.h));
// Ensure the image can be identified as an SVG if downloaded.

@@ -647,0 +648,0 @@ // See https://jwatt.org/svg/authoring/

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

@@ -116,3 +116,3 @@ import AbstractRenderer from './AbstractRenderer';

this.transformBy(transform);
Text.applyTextStyles(this.ctx, style);
TextComponent.applyTextStyles(this.ctx, style);
if (style.renderingStyle.fill.a !== 0) {

@@ -119,0 +119,0 @@ this.ctx.fillStyle = style.renderingStyle.fill.toHexString();

@@ -23,2 +23,4 @@ import { LoadSaveDataTable } from '../../components/AbstractComponent';

private transformFrom;
private textContainer;
private textContainerTransform;
drawText(text: string, transform: Mat33, style: TextStyle): void;

@@ -25,0 +27,0 @@ drawImage(image: RenderableImage): void;

@@ -18,2 +18,4 @@ import Mat33 from '../../math/Mat33';

this.overwrittenAttrs = {};
this.textContainer = null;
this.textContainerTransform = null;
this.clear();

@@ -86,11 +88,16 @@ }

// Apply [elemTransform] to [elem].
transformFrom(elemTransform, elem) {
let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
transformFrom(elemTransform, elem, inCanvasSpace = false) {
let transform = !inCanvasSpace ? this.getCanvasToScreenTransform().rightMul(elemTransform) : elemTransform;
const translation = transform.transformVec2(Vec2.zero);
transform = transform.rightMul(Mat33.translation(translation.times(-1)));
elem.style.transform = `matrix(
${transform.a1}, ${transform.b1},
${transform.a2}, ${transform.b2},
${transform.a3}, ${transform.b3}
)`;
if (!transform.eq(Mat33.identity)) {
elem.style.transform = `matrix(
${transform.a1}, ${transform.b1},
${transform.a2}, ${transform.b2},
${transform.a3}, ${transform.b3}
)`;
}
else {
elem.style.transform = '';
}
elem.setAttribute('x', `${toRoundedString(translation.x)}`);

@@ -100,18 +107,37 @@ elem.setAttribute('y', `${toRoundedString(translation.y)}`);

drawText(text, transform, style) {
var _a, _b, _c;
const textElem = document.createElementNS(svgNameSpace, 'text');
textElem.appendChild(document.createTextNode(text));
this.transformFrom(transform, textElem);
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';
var _a;
const applyTextStyles = (elem, style) => {
var _a, _b;
elem.style.fontFamily = style.fontFamily;
elem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
elem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
elem.style.fontSize = style.size + 'px';
elem.style.fill = style.renderingStyle.fill.toHexString();
if (style.renderingStyle.stroke) {
const strokeStyle = style.renderingStyle.stroke;
elem.style.stroke = strokeStyle.color.toHexString();
elem.style.strokeWidth = strokeStyle.width + 'px';
}
};
transform = this.getCanvasToScreenTransform().rightMul(transform);
if (!this.textContainer) {
const container = document.createElementNS(svgNameSpace, 'text');
container.appendChild(document.createTextNode(text));
this.transformFrom(transform, container, true);
applyTextStyles(container, style);
this.elem.appendChild(container);
(_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(container);
if (this.objectLevel > 0) {
this.textContainer = container;
this.textContainerTransform = transform;
}
}
this.elem.appendChild(textElem);
(_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
else {
const elem = document.createElementNS(svgNameSpace, 'tspan');
elem.appendChild(document.createTextNode(text));
this.textContainer.appendChild(elem);
transform = this.textContainerTransform.inverse().rightMul(transform);
this.transformFrom(transform, elem, true);
applyTextStyles(elem, style);
}
}

@@ -134,2 +160,3 @@ drawImage(image) {

this.lastPathStyle = null;
this.textContainer = null;
this.objectElems = [];

@@ -136,0 +163,0 @@ }

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

import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
import Text from './components/Text';
import TextComponent from './components/Text';
import UnknownSVGObject from './components/UnknownSVGObject';

@@ -213,3 +213,3 @@ import Mat33 from './math/Mat33';

const transform = this.getTransform(elem, supportedAttrs, computedStyles);
const result = new Text(contentList, transform, style);
const result = new TextComponent(contentList, transform, style);
this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));

@@ -216,0 +216,0 @@ return result;

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

import Pen from '../tools/Pen';
declare type IconType = SVGSVGElement | HTMLImageElement;
export default class IconProvider {
makeUndoIcon(): SVGSVGElement;
makeRedoIcon(mirror?: boolean): SVGSVGElement;
makeDropdownIcon(): SVGSVGElement;
makeEraserIcon(): SVGSVGElement;
makeSelectionIcon(): SVGSVGElement;
protected makeIconFromPath(pathData: string, fill?: string, strokeColor?: string, strokeWidth?: string): SVGSVGElement;
makeHandToolIcon(): SVGSVGElement;
makeTouchPanningIcon(): SVGSVGElement;
makeAllDevicePanningIcon(): SVGSVGElement;
makeZoomIcon: () => SVGSVGElement;
makeTextIcon(textStyle: TextStyle): SVGSVGElement;
makePenIcon(tipThickness: number, color: string | Color4): SVGSVGElement;
makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): SVGSVGElement;
makePipetteIcon(color?: Color4): SVGSVGElement;
makeResizeViewportIcon(): SVGSVGElement;
makeDuplicateSelectionIcon(): SVGSVGElement;
makeDeleteSelectionIcon(): SVGSVGElement;
makeSaveIcon(): SVGSVGElement;
makeUndoIcon(): IconType;
makeRedoIcon(mirror?: boolean): IconType;
makeDropdownIcon(): IconType;
makeEraserIcon(): IconType;
makeSelectionIcon(): IconType;
/**
* @param pathData - SVG path data (e.g. `m10,10l30,30z`)
* @param fill - A valid CSS color (e.g. `var(--icon-color)` or `#f0f`). This can be `none`.
*/
protected makeIconFromPath(pathData: string, fill?: string, strokeColor?: string, strokeWidth?: string): IconType;
makeHandToolIcon(): IconType;
makeTouchPanningIcon(): IconType;
makeAllDevicePanningIcon(): IconType;
makeZoomIcon(): IconType;
makeTextIcon(textStyle: TextStyle): IconType;
makePenIcon(tipThickness: number, color: string | Color4): IconType;
makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
makePipetteIcon(color?: Color4): IconType;
makeResizeViewportIcon(): IconType;
makeDuplicateSelectionIcon(): IconType;
makeDeleteSelectionIcon(): IconType;
makeSaveIcon(): IconType;
}
export {};

@@ -30,23 +30,2 @@ import Color4 from '../Color4';

export default class IconProvider {
constructor() {
this.makeZoomIcon = () => {
const icon = document.createElementNS(svgNamespace, 'svg');
icon.setAttribute('viewBox', '0 0 100 100');
const addTextNode = (text, x, y) => {
const textNode = document.createElementNS(svgNamespace, 'text');
textNode.appendChild(document.createTextNode(text));
textNode.setAttribute('x', x.toString());
textNode.setAttribute('y', y.toString());
textNode.style.textAlign = 'center';
textNode.style.textAnchor = 'middle';
textNode.style.fontSize = '55px';
textNode.style.fill = 'var(--icon-color)';
textNode.style.fontFamily = 'monospace';
icon.appendChild(textNode);
};
addTextNode('+', 40, 45);
addTextNode('-', 70, 75);
return icon;
};
}
makeUndoIcon() {

@@ -119,2 +98,6 @@ return this.makeRedoIcon(true);

}
/**
* @param pathData - SVG path data (e.g. `m10,10l30,30z`)
* @param fill - A valid CSS color (e.g. `var(--icon-color)` or `#f0f`). This can be `none`.
*/
makeIconFromPath(pathData, fill = 'var(--icon-color)', strokeColor = 'none', strokeWidth = '0px') {

@@ -245,2 +228,21 @@ const icon = document.createElementNS(svgNamespace, 'svg');

}
makeZoomIcon() {
const icon = document.createElementNS(svgNamespace, 'svg');
icon.setAttribute('viewBox', '0 0 100 100');
const addTextNode = (text, x, y) => {
const textNode = document.createElementNS(svgNamespace, 'text');
textNode.appendChild(document.createTextNode(text));
textNode.setAttribute('x', x.toString());
textNode.setAttribute('y', y.toString());
textNode.style.textAlign = 'center';
textNode.style.textAnchor = 'middle';
textNode.style.fontSize = '55px';
textNode.style.fill = 'var(--icon-color)';
textNode.style.fontFamily = 'monospace';
icon.appendChild(textNode);
};
addTextNode('+', 40, 45);
addTextNode('-', 70, 75);
return icon;
}
makeTextIcon(textStyle) {

@@ -247,0 +249,0 @@ var _a, _b;

@@ -80,8 +80,11 @@ import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';

objectSelectLabel.setAttribute('for', objectTypeSelect.id);
// Use a logarithmic scale for thicknessInput (finer control over thinner strokewidths.)
const inverseThicknessInputFn = (t) => Math.log10(t);
const thicknessInputFn = (t) => Math.pow(10, t);
thicknessInput.type = 'range';
thicknessInput.min = '2';
thicknessInput.max = '20';
thicknessInput.step = '1';
thicknessInput.min = `${inverseThicknessInputFn(2)}`;
thicknessInput.max = `${inverseThicknessInputFn(400)}`;
thicknessInput.step = '0.1';
thicknessInput.oninput = () => {
this.tool.setThickness(Math.pow(parseFloat(thicknessInput.value), 2));
this.tool.setThickness(thicknessInputFn(parseFloat(thicknessInput.value)));
};

@@ -112,3 +115,3 @@ thicknessRow.appendChild(thicknessLabel);

colorInput.value = this.tool.getColor().toHexString();
thicknessInput.value = Math.sqrt(this.tool.getThickness()).toString();
thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
objectTypeSelect.replaceChildren();

@@ -115,0 +118,0 @@ for (let i = 0; i < this.penTypes.length; i++) {

@@ -17,3 +17,3 @@ /**

import SVGLoader from '../SVGLoader';
import { Mat33, Vec2 } from '../math/lib';
import { Mat33 } from '../math/lib';
import BaseTool from './BaseTool';

@@ -114,23 +114,3 @@ import EditorImage from '../EditorImage';

const lines = text.split('\n');
let lastComponent = null;
const components = [];
for (const line of lines) {
let position = Vec2.zero;
if (lastComponent) {
const lineMargin = Math.floor(pastedTextStyle.size);
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
}
const component = new TextComponent([line], Mat33.translation(position), pastedTextStyle);
components.push(component);
lastComponent = component;
}
if (components.length === 1) {
yield this.addComponentsFromPaste([components[0]]);
}
else {
// Wrap the existing `TextComponent`s --- dragging one component should drag all.
yield this.addComponentsFromPaste([
new TextComponent(components, Mat33.identity, pastedTextStyle)
]);
}
yield this.addComponentsFromPaste([TextComponent.fromLines(lines, Mat33.identity, pastedTextStyle)]);
});

@@ -137,0 +117,0 @@ }

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

private textRotation;
private textScale;
private removeExistingCommand;
constructor(editor: Editor, description: string, localizationTable: ToolLocalization);
private getTextAscent;
private flushInput;
private getTextScaleMatrix;
private updateTextInput;

@@ -30,2 +33,3 @@ private startTextInput;

getTextStyle(): TextStyle;
private setTextStyle;
}
import Color4 from '../Color4';
import Text from '../components/Text';
import TextComponent from '../components/Text';
import EditorImage from '../EditorImage';
import Rect2 from '../math/Rect2';
import Mat33 from '../math/Mat33';
import { Vec2 } from '../math/Vec2';
import { PointerDevice } from '../Pointer';
import { EditorEventType } from '../types';
import BaseTool from './BaseTool';
import Erase from '../commands/Erase';
import uniteCommands from '../commands/uniteCommands';
const overlayCssClass = 'textEditorOverlay';

@@ -17,2 +21,4 @@ export default class TextTool extends BaseTool {

this.textMeasuringCtx = null;
this.textScale = Vec2.of(1, 1);
this.removeExistingCommand = null;
this.textStyle = {

@@ -33,6 +39,14 @@ size: 32,

.${overlayCssClass} input {
.${overlayCssClass} textarea {
background-color: rgba(0, 0, 0, 0);
white-space: pre;
padding: 0;
margin: 0;
border: none;
padding: 0;
min-width: 100px;
min-height: 1.1em;
}

@@ -47,3 +61,3 @@ `);

if (this.textMeasuringCtx) {
Text.applyTextStyles(this.textMeasuringCtx, style);
TextComponent.applyTextStyles(this.textMeasuringCtx, style);
return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;

@@ -56,3 +70,3 @@ }

if (this.textInputElem && this.textTargetPosition) {
const content = this.textInputElem.value;
const content = this.textInputElem.value.trimEnd();
this.textInputElem.remove();

@@ -63,8 +77,19 @@ this.textInputElem = null;

}
const textTransform = Mat33.translation(this.textTargetPosition).rightMul(Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())).rightMul(Mat33.zRotation(this.textRotation));
const textComponent = new Text([content], textTransform, this.textStyle);
const textTransform = Mat33.translation(this.textTargetPosition).rightMul(this.getTextScaleMatrix()).rightMul(Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())).rightMul(Mat33.zRotation(this.textRotation));
const textComponent = TextComponent.fromLines(content.split('\n'), textTransform, this.textStyle);
const action = EditorImage.addElement(textComponent);
this.editor.dispatch(action);
if (this.removeExistingCommand) {
// Unapply so that `removeExistingCommand` can be added to the undo stack.
this.removeExistingCommand.unapply(this.editor);
this.editor.dispatch(uniteCommands([this.removeExistingCommand, action]));
this.removeExistingCommand = null;
}
else {
this.editor.dispatch(action);
}
}
}
getTextScaleMatrix() {
return Mat33.scaling2D(this.textScale.times(1 / this.editor.viewport.getSizeOfPixelOnCanvas()));
}
updateTextInput() {

@@ -78,3 +103,2 @@ var _a, _b, _c;

const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
this.textInputElem.type = 'text';
this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;

@@ -90,5 +114,8 @@ this.textInputElem.style.fontFamily = this.textStyle.fontFamily;

this.textInputElem.style.margin = '0';
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
const rotation = this.textRotation + viewport.getRotationAngle();
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
const scale = this.getTextScaleMatrix();
this.textInputElem.style.transform = `${scale.toCSSMatrix()} rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
this.textInputElem.style.transformOrigin = 'top left';

@@ -98,11 +125,13 @@ }

this.flushInput();
this.textInputElem = document.createElement('input');
this.textInputElem = document.createElement('textarea');
this.textInputElem.value = initialText;
this.textInputElem.style.display = 'inline-block';
this.textTargetPosition = textCanvasPos;
this.textRotation = -this.editor.viewport.getRotationAngle();
this.textScale = Vec2.of(1, 1).times(this.editor.viewport.getSizeOfPixelOnCanvas());
this.updateTextInput();
this.textInputElem.oninput = () => {
var _a;
if (this.textInputElem) {
this.textInputElem.size = ((_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.value.length) || 10;
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
}

@@ -116,4 +145,4 @@ };

this.textInputElem.onkeyup = (evt) => {
var _a;
if (evt.key === 'Enter') {
var _a, _b;
if (evt.key === 'Enter' && !evt.shiftKey) {
this.flushInput();

@@ -127,2 +156,4 @@ this.editor.focus();

this.editor.focus();
(_b = this.removeExistingCommand) === null || _b === void 0 ? void 0 : _b.unapply(this.editor);
this.removeExistingCommand = null;
}

@@ -145,3 +176,25 @@ };

if (allPointers.length === 1) {
this.startTextInput(current.canvasPos, '');
// Are we clicking on a text node?
const canvasPos = current.canvasPos;
const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
const targetTextNodes = targetNodes.filter(node => node instanceof TextComponent);
if (targetTextNodes.length > 0) {
const targetNode = targetTextNodes[targetTextNodes.length - 1];
this.setTextStyle(targetNode.getTextStyle());
// Create and temporarily apply removeExistingCommand.
this.removeExistingCommand = new Erase([targetNode]);
this.removeExistingCommand.apply(this.editor);
this.startTextInput(targetNode.getBaselinePos(), targetNode.getText());
const transform = targetNode.getTransform();
this.textRotation = transform.transformVec3(Vec2.unitX).angle();
const scaleFactor = transform.transformVec3(Vec2.unitX).magnitude();
this.textScale = Vec2.of(1, 1).times(scaleFactor);
this.updateTextInput();
}
else {
this.removeExistingCommand = null;
this.startTextInput(current.canvasPos, '');
}
return true;

@@ -183,2 +236,7 @@ }

}
setTextStyle(style) {
// Copy the style — we may change parts of it.
this.textStyle = Object.assign({}, style);
this.dispatchUpdateEvent();
}
}
{
"name": "js-draw",
"version": "0.6.0",
"version": "0.7.0",
"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/lib.d.ts",

@@ -14,9 +14,9 @@ import { Bezier } from 'bezier-js';

export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
// Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
// Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
// less than ±1 px from the curve.
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
return new FreehandLineBuilder(
initialPoint, minSmoothingDist, maxSmoothingDist
initialPoint, minSmoothingDist, maxSmoothingDist, viewport
);

@@ -26,6 +26,9 @@ };

type CurrentSegmentToPathResult = {
upperCurve: QuadraticBezierPathCommand,
upperCurveCommand: QuadraticBezierPathCommand,
lowerToUpperConnector: PathCommand,
upperToLowerConnector: PathCommand,
lowerCurve: QuadraticBezierPathCommand,
lowerCurveCommand: QuadraticBezierPathCommand,
upperCurve: Bezier,
lowerCurve: Bezier,
};

@@ -52,4 +55,7 @@

// recent edge.
private upperSegments: QuadraticBezierPathCommand[];
private lowerSegments: QuadraticBezierPathCommand[];
private upperSegments: PathCommand[];
private lowerSegments: PathCommand[];
private lastUpperBezier: Bezier|null = null;
private lastLowerBezier: Bezier|null = null;
private parts: RenderablePathSpec[] = [];

@@ -75,3 +81,5 @@ private buffer: Point2[];

private minFitAllowed: number,
private maxFitAllowed: number
private maxFitAllowed: number,
private viewport: Viewport,
) {

@@ -100,11 +108,15 @@ this.lastPoint = this.startPoint;

private previewPath(): RenderablePathSpec|null {
let upperPath: QuadraticBezierPathCommand[];
let lowerPath: QuadraticBezierPathCommand[];
private previewCurrentPath(): RenderablePathSpec|null {
const upperPath = this.upperSegments.slice();
const lowerPath = this.lowerSegments.slice();
let lowerToUpperCap: PathCommand;
let pathStartConnector: PathCommand;
if (this.currentCurve) {
const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
upperPath = this.upperSegments.concat(upperCurve);
lowerPath = this.lowerSegments.concat(lowerCurve);
const {
upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand
} = this.currentSegmentToPath();
upperPath.push(upperCurveCommand);
lowerPath.push(lowerCurveCommand);
lowerToUpperCap = lowerToUpperConnector;

@@ -117,9 +129,13 @@ pathStartConnector = this.pathStartConnector ?? upperToLowerConnector;

upperPath = this.upperSegments.slice();
lowerPath = this.lowerSegments.slice();
lowerToUpperCap = this.mostRecentConnector;
pathStartConnector = this.pathStartConnector;
}
const startPoint = lowerPath[lowerPath.length - 1].endPoint;
let startPoint: Point2;
const lastLowerSegment = lowerPath[lowerPath.length - 1];
if (lastLowerSegment.kind === PathCommandType.LineTo || lastLowerSegment.kind === PathCommandType.MoveTo) {
startPoint = lastLowerSegment.point;
} else {
startPoint = lastLowerSegment.endPoint;
}

@@ -165,7 +181,15 @@ return {

private previewFullPath(): RenderablePathSpec[]|null {
const preview = this.previewCurrentPath();
if (preview) {
return [ ...this.parts, preview ];
}
return null;
}
private previewStroke(): Stroke|null {
const pathPreview = this.previewPath();
const pathPreview = this.previewFullPath();
if (pathPreview) {
return new Stroke([ pathPreview ]);
return new Stroke(pathPreview);
}

@@ -176,5 +200,10 @@ return null;

public preview(renderer: AbstractRenderer) {
const path = this.previewPath();
if (path) {
renderer.drawPath(path);
const paths = this.previewFullPath();
if (paths) {
const approxBBox = this.viewport.visibleRect;
renderer.startObject(approxBBox);
for (const path of paths) {
renderer.drawPath(path);
}
renderer.endObject();
}

@@ -184,3 +213,3 @@ }

public build(): Stroke {
if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
if (this.lastPoint) {
this.finalizeCurrentCurve();

@@ -201,2 +230,70 @@ }

// Returns true if, due to overlap with previous segments, a new RenderablePathSpec should be created.
private shouldStartNewSegment(lowerCurve: Bezier, upperCurve: Bezier): boolean {
if (!this.lastLowerBezier || !this.lastUpperBezier) {
return false;
}
const getIntersection = (curve1: Bezier, curve2: Bezier): Point2|null => {
const intersection = curve1.intersects(curve2) as (string[] | null | undefined);
if (!intersection || intersection.length === 0) {
return null;
}
// From http://pomax.github.io/bezierjs/#intersect-curve,
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
const firstTPair = intersection[0];
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
if (!match) {
throw new Error(
`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`
);
}
const t = parseFloat(match[1]);
return Vec2.ofXY(curve1.get(t));
};
const getExitDirection = (curve: Bezier): Vec2 => {
return Vec2.ofXY(curve.points[2]).minus(Vec2.ofXY(curve.points[1])).normalized();
};
const getEnterDirection = (curve: Bezier): Vec2 => {
return Vec2.ofXY(curve.points[1]).minus(Vec2.ofXY(curve.points[0])).normalized();
};
// Prevent
// /
// / /
// / / /|
// / / |
// / |
// where the next stroke and the previous stroke are in different directions.
//
// Are the exit/enter directions of the previous and current curves in different enough directions?
if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.3
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.3
// Also handle if the curves exit/enter directions differ
|| getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
|| getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
return true;
}
// Check whether the lower curve intersects the other wall:
// / / ← lower
// / / /
// / / /
// //
// / /
const lowerIntersection = getIntersection(lowerCurve, this.lastUpperBezier);
const upperIntersection = getIntersection(upperCurve, this.lastLowerBezier);
if (lowerIntersection || upperIntersection) {
return true;
}
return false;
}
// Returns the distance between the start, control, and end points of the curve.

@@ -270,5 +367,19 @@ private approxCurrentCurveLength() {

const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
const {
upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand,
lowerCurve, upperCurve,
} = this.currentSegmentToPath();
if (this.isFirstSegment) {
const shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
if (shouldStartNew) {
const part = this.previewCurrentPath();
if (part) {
this.parts.push(part);
this.upperSegments = [];
this.lowerSegments = [];
}
}
if (this.isFirstSegment || shouldStartNew) {
// We draw the upper path (reversed), then the lower path, so we need the

@@ -283,5 +394,8 @@ // upperToLowerConnector to join the two paths.

this.upperSegments.push(upperCurve);
this.lowerSegments.push(lowerCurve);
this.lowerSegments.push(lowerCurveCommand);
this.upperSegments.push(upperCurveCommand);
this.lastLowerBezier = lowerCurve;
this.lastUpperBezier = upperCurve;
const lastPoint = this.buffer[this.buffer.length - 1];

@@ -340,2 +454,3 @@ this.lastExitingVec = Vec2.ofXY(

// Each starts at startPt ± startVec
const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));

@@ -345,4 +460,5 @@ const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));

const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
const upperCurveEndPoint = this.roundPoint(startPt.minus(startVec));
const lowerCurve: QuadraticBezierPathCommand = {
const lowerCurveCommand: QuadraticBezierPathCommand = {
kind: PathCommandType.QuadraticBezierTo,

@@ -356,3 +472,3 @@ controlPoint: lowerCurveControlPoint,

kind: PathCommandType.LineTo,
point: this.roundPoint(startPt.plus(startVec)),
point: lowerCurveStartPoint,
};

@@ -366,9 +482,15 @@

const upperCurve: QuadraticBezierPathCommand = {
const upperCurveCommand: QuadraticBezierPathCommand = {
kind: PathCommandType.QuadraticBezierTo,
controlPoint: upperCurveControlPoint,
endPoint: this.roundPoint(startPt.minus(startVec)),
endPoint: upperCurveEndPoint,
};
return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
const upperCurve = new Bezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
const lowerCurve = new Bezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
return {
upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
upperCurve, lowerCurve,
};
}

@@ -416,2 +538,7 @@

if (this.isFirstSegment) {
// The start of a curve often lacks accurate pressure information. Update it.
this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
}
// recompute bbox

@@ -433,2 +560,3 @@ this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);

// If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
let enteringVec = this.lastExitingVec;

@@ -435,0 +563,0 @@ if (!enteringVec) {

@@ -7,3 +7,3 @@ export * from './builders/types';

import Stroke from './Stroke';
import Text from './Text';
import TextComponent from './Text';
import ImageComponent from './ImageComponent';

@@ -13,7 +13,7 @@

Stroke,
Text,
TextComponent as Text,
Text as TextComponent,
TextComponent as TextComponent,
Stroke as StrokeComponent,
ImageComponent,
};

@@ -18,2 +18,3 @@ import LineSegment2 from '../math/LineSegment2';

// Creates a `Stroke` from the given `parts`.
public constructor(parts: RenderablePathSpec[]) {

@@ -20,0 +21,0 @@ super('stroke');

import Color4 from '../Color4';
import Mat33 from '../math/Mat33';
import AbstractComponent from './AbstractComponent';
import Text, { TextStyle } from './Text';
import TextComponent, { TextStyle } from './Text';

@@ -14,5 +14,5 @@

};
const text = new Text([ 'Foo' ], Mat33.identity, style);
const text = new TextComponent([ 'Foo' ], Mat33.identity, style);
const serialized = text.serialize();
const deserialized = AbstractComponent.deserialize(serialized) as Text;
const deserialized = AbstractComponent.deserialize(serialized) as TextComponent;
expect(deserialized.getBBox()).objEq(text.getBBox());

@@ -19,0 +19,0 @@ expect(deserialized['getText']()).toContain('Foo');

import LineSegment2 from '../math/LineSegment2';
import Mat33, { Mat33Array } from '../math/Mat33';
import Rect2 from '../math/Rect2';
import { Vec2 } from '../math/Vec2';
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';

@@ -18,12 +19,19 @@ import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';

const componentTypeId = 'text';
export default class Text extends AbstractComponent {
export default class TextComponent extends AbstractComponent {
protected contentBBox: Rect2;
public constructor(
protected readonly textObjects: Array<string|Text>,
protected readonly textObjects: Array<string|TextComponent>,
private transform: Mat33,
private readonly style: TextStyle,
private style: TextStyle,
) {
super(componentTypeId);
this.recomputeBBox();
// If this has no direct children, choose a style representative of this' content
// (useful for estimating the style of the TextComponent).
const hasDirectContent = textObjects.some(obj => typeof obj === 'string');
if (!hasDirectContent && textObjects.length > 0) {
this.style = (textObjects[0] as TextComponent).getTextStyle();
}
}

@@ -59,9 +67,9 @@

private static getTextDimens(text: string, style: TextStyle): Rect2 {
Text.textMeasuringCtx ??= document.createElement('canvas').getContext('2d') ?? null;
if (!Text.textMeasuringCtx) {
TextComponent.textMeasuringCtx ??= document.createElement('canvas').getContext('2d') ?? null;
if (!TextComponent.textMeasuringCtx) {
return this.estimateTextDimens(text, style);
}
const ctx = Text.textMeasuringCtx;
Text.applyTextStyles(ctx, style);
const ctx = TextComponent.textMeasuringCtx;
TextComponent.applyTextStyles(ctx, style);

@@ -76,5 +84,5 @@ const measure = ctx.measureText(text);

private computeBBoxOfPart(part: string|Text) {
private computeBBoxOfPart(part: string|TextComponent) {
if (typeof part === 'string') {
const textBBox = Text.getTextDimens(part, this.style);
const textBBox = TextComponent.getTextDimens(part, this.style);
return textBBox.transformedBoundingBox(this.transform);

@@ -99,6 +107,5 @@ } else {

public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
private renderInternal(canvas: AbstractRenderer) {
const cursor = this.transform;
canvas.startObject(this.contentBBox);
for (const textObject of this.textObjects) {

@@ -109,6 +116,11 @@ if (typeof textObject === 'string') {

canvas.pushTransform(cursor);
textObject.render(canvas);
textObject.renderInternal(canvas);
canvas.popTransform();
}
}
}
public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
canvas.startObject(this.contentBBox);
this.renderInternal(canvas);
canvas.endObject(this.getLoadSaveData());

@@ -127,3 +139,3 @@ }

if (typeof subObject === 'string') {
const textBBox = Text.getTextDimens(subObject, this.style);
const textBBox = TextComponent.getTextDimens(subObject, this.style);

@@ -145,2 +157,14 @@ // TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and

public getBaselinePos() {
return this.transform.transformVec2(Vec2.zero);
}
public getTextStyle() {
return this.style;
}
public getTransform(): Mat33 {
return this.transform;
}
protected applyTransformation(affineTransfm: Mat33): void {

@@ -152,3 +176,3 @@ this.transform = affineTransfm.rightMul(this.transform);

protected createClone(): AbstractComponent {
return new Text(this.textObjects, this.transform, this.style);
return new TextComponent(this.textObjects, this.transform, this.style);
}

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

public static deserializeFromString(json: any): Text {
public static deserializeFromString(json: any): TextComponent {
const style: TextStyle = {

@@ -209,3 +233,3 @@ renderingStyle: styleFromJSON(json.style.renderingStyle),

const textObjects: Array<string|Text> = json.textObjects.map((data: any) => {
const textObjects: Array<string|TextComponent> = json.textObjects.map((data: any) => {
if ((data.text ?? null) !== null) {

@@ -215,3 +239,3 @@ return data.text;

return Text.deserializeFromString(data.json);
return TextComponent.deserializeFromString(data.json);
});

@@ -227,6 +251,25 @@

return new Text(textObjects, transform, style);
return new TextComponent(textObjects, transform, style);
}
public static fromLines(lines: string[], transform: Mat33, style: TextStyle): AbstractComponent {
let lastComponent: TextComponent|null = null;
const components: TextComponent[] = [];
for (const line of lines) {
let position = Vec2.zero;
if (lastComponent) {
const lineMargin = Math.floor(style.size);
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
}
const component = new TextComponent([ line ], Mat33.translation(position), style);
components.push(component);
lastComponent = component;
}
return new TextComponent(components, transform, style);
}
}
AbstractComponent.registerComponent(componentTypeId, (data: string) => Text.deserializeFromString(data));
AbstractComponent.registerComponent(componentTypeId, (data: string) => TextComponent.deserializeFromString(data));

@@ -41,2 +41,3 @@ /**

import IconProvider from './toolbar/IconProvider';
import { toRoundedString } from './math/rounding';

@@ -122,3 +123,2 @@ type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';

* Global event dispatcher/subscriber.
* @see {@link types.EditorEventType}
*/

@@ -845,5 +845,5 @@ public readonly notifier: EditorNotifier;

const rect = importExportViewport.visibleRect;
result.setAttribute('viewBox', `${rect.x} ${rect.y} ${rect.w} ${rect.h}`);
result.setAttribute('width', `${rect.w}`);
result.setAttribute('height', `${rect.h}`);
result.setAttribute('viewBox', [rect.x, rect.y, rect.w, rect.h].map(part => toRoundedString(part)).join(' '));
result.setAttribute('width', toRoundedString(rect.w));
result.setAttribute('height', toRoundedString(rect.h));

@@ -850,0 +850,0 @@ // Ensure the image can be identified as an SVG if downloaded.

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

@@ -156,3 +156,3 @@ import Rect2 from '../../math/Rect2';

this.transformBy(transform);
Text.applyTextStyles(this.ctx, style);
TextComponent.applyTextStyles(this.ctx, style);

@@ -159,0 +159,0 @@ if (style.renderingStyle.fill.a !== 0) {

@@ -103,12 +103,17 @@

// Apply [elemTransform] to [elem].
private transformFrom(elemTransform: Mat33, elem: SVGElement) {
let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
private transformFrom(elemTransform: Mat33, elem: SVGElement, inCanvasSpace: boolean = false) {
let transform = !inCanvasSpace ? this.getCanvasToScreenTransform().rightMul(elemTransform) : elemTransform;
const translation = transform.transformVec2(Vec2.zero);
transform = transform.rightMul(Mat33.translation(translation.times(-1)));
elem.style.transform = `matrix(
${transform.a1}, ${transform.b1},
${transform.a2}, ${transform.b2},
${transform.a3}, ${transform.b3}
)`;
if (!transform.eq(Mat33.identity)) {
elem.style.transform = `matrix(
${transform.a1}, ${transform.b1},
${transform.a2}, ${transform.b2},
${transform.a3}, ${transform.b3}
)`;
} else {
elem.style.transform = '';
}
elem.setAttribute('x', `${toRoundedString(translation.x)}`);

@@ -118,21 +123,41 @@ elem.setAttribute('y', `${toRoundedString(translation.y)}`);

private textContainer: SVGTextElement|null = null;
private textContainerTransform: Mat33|null = null;
public drawText(text: string, transform: Mat33, style: TextStyle): void {
const textElem = document.createElementNS(svgNameSpace, 'text');
textElem.appendChild(document.createTextNode(text));
this.transformFrom(transform, textElem);
const applyTextStyles = (elem: SVGTextElement|SVGTSpanElement, style: TextStyle) => {
elem.style.fontFamily = style.fontFamily;
elem.style.fontVariant = style.fontVariant ?? '';
elem.style.fontWeight = style.fontWeight ?? '';
elem.style.fontSize = style.size + 'px';
elem.style.fill = style.renderingStyle.fill.toHexString();
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;
elem.style.stroke = strokeStyle.color.toHexString();
elem.style.strokeWidth = strokeStyle.width + 'px';
}
};
transform = this.getCanvasToScreenTransform().rightMul(transform);
if (style.renderingStyle.stroke) {
const strokeStyle = style.renderingStyle.stroke;
textElem.style.stroke = strokeStyle.color.toHexString();
textElem.style.strokeWidth = strokeStyle.width + 'px';
if (!this.textContainer) {
const container = document.createElementNS(svgNameSpace, 'text');
container.appendChild(document.createTextNode(text));
this.transformFrom(transform, container, true);
applyTextStyles(container, style);
this.elem.appendChild(container);
this.objectElems?.push(container);
if (this.objectLevel > 0) {
this.textContainer = container;
this.textContainerTransform = transform;
}
} else {
const elem = document.createElementNS(svgNameSpace, 'tspan');
elem.appendChild(document.createTextNode(text));
this.textContainer.appendChild(elem);
transform = this.textContainerTransform!.inverse().rightMul(transform);
this.transformFrom(transform, elem, true);
applyTextStyles(elem, style);
}
this.elem.appendChild(textElem);
this.objectElems?.push(textElem);
}

@@ -158,2 +183,3 @@

this.lastPathStyle = null;
this.textContainer = null;
this.objectElems = [];

@@ -160,0 +186,0 @@ }

@@ -37,2 +37,22 @@ import { Rect2, TextComponent, Vec2 } from './lib';

});
it('should correctly load tspans within texts nodes', async () => {
const editor = createEditor();
await editor.loadFrom(SVGLoader.fromString(`
<svg>
<text>
Testing...
<tspan x=0 y=100>Test 2...</tspan>
<tspan x=0 y=200>Test 2...</tspan>
</text>
</svg>
`, true));
const elem = editor.image
.getElementsIntersectingRegion(new Rect2(-1000, -1000, 10000, 10000))
.filter(elem => elem instanceof TextComponent)[0];
expect(elem).not.toBeNull();
expect(elem.getBBox().topLeft.y).toBeLessThan(0);
expect(elem.getBBox().topLeft.x).toBe(0);
expect(elem.getBBox().h).toBeGreaterThan(200);
});
});

@@ -6,3 +6,3 @@ import Color4 from './Color4';

import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
import Text, { TextStyle } from './components/Text';
import TextComponent, { TextStyle } from './components/Text';
import UnknownSVGObject from './components/UnknownSVGObject';

@@ -214,4 +214,4 @@ import Mat33 from './math/Mat33';

private makeText(elem: SVGTextElement|SVGTSpanElement): Text {
const contentList: Array<Text|string> = [];
private makeText(elem: SVGTextElement|SVGTSpanElement): TextComponent {
const contentList: Array<TextComponent|string> = [];
for (const child of elem.childNodes) {

@@ -256,3 +256,3 @@ if (child.nodeType === Node.TEXT_NODE) {

const transform = this.getTransform(elem, supportedAttrs, computedStyles);
const result = new Text(contentList, transform, style);
const result = new TextComponent(contentList, transform, style);
this.attachUnrecognisedAttrs(

@@ -259,0 +259,0 @@ result,

@@ -11,4 +11,4 @@ import Color4 from '../Color4';

type IconType = SVGSVGElement|HTMLImageElement;
const svgNamespace = 'http://www.w3.org/2000/svg';

@@ -40,3 +40,3 @@ const iconColorFill = `

public makeUndoIcon() {
public makeUndoIcon(): IconType {
return this.makeRedoIcon(true);

@@ -47,3 +47,3 @@ }

// @returns a redo icon.
public makeRedoIcon(mirror: boolean = false) {
public makeRedoIcon(mirror: boolean = false): IconType {
const icon = document.createElementNS(svgNamespace, 'svg');

@@ -71,3 +71,3 @@ icon.innerHTML = `

public makeDropdownIcon() {
public makeDropdownIcon(): IconType {
const icon = document.createElementNS(svgNamespace, 'svg');

@@ -86,3 +86,3 @@ icon.innerHTML = `

public makeEraserIcon() {
public makeEraserIcon(): IconType {
const icon = document.createElementNS(svgNamespace, 'svg');

@@ -104,3 +104,3 @@

public makeSelectionIcon() {
public makeSelectionIcon(): IconType {
const icon = document.createElementNS(svgNamespace, 'svg');

@@ -120,2 +120,6 @@

/**
* @param pathData - SVG path data (e.g. `m10,10l30,30z`)
* @param fill - A valid CSS color (e.g. `var(--icon-color)` or `#f0f`). This can be `none`.
*/
protected makeIconFromPath(

@@ -126,3 +130,3 @@ pathData: string,

strokeWidth: string = '0px',
) {
): IconType {
const icon = document.createElementNS(svgNamespace, 'svg');

@@ -140,3 +144,3 @@ const path = document.createElementNS(svgNamespace, 'path');

public makeHandToolIcon() {
public makeHandToolIcon(): IconType {
const fill = 'none';

@@ -170,3 +174,3 @@ const strokeColor = 'var(--icon-color)';

public makeTouchPanningIcon() {
public makeTouchPanningIcon(): IconType {
const fill = 'none';

@@ -205,3 +209,3 @@ const strokeColor = 'var(--icon-color)';

public makeAllDevicePanningIcon() {
public makeAllDevicePanningIcon(): IconType {
const fill = 'none';

@@ -262,3 +266,3 @@ const strokeColor = 'var(--icon-color)';

public makeZoomIcon = () => {
public makeZoomIcon(): IconType {
const icon = document.createElementNS(svgNamespace, 'svg');

@@ -285,5 +289,5 @@ icon.setAttribute('viewBox', '0 0 100 100');

return icon;
};
}
public makeTextIcon(textStyle: TextStyle) {
public makeTextIcon(textStyle: TextStyle): IconType {
const icon = document.createElementNS(svgNamespace, 'svg');

@@ -311,3 +315,3 @@ icon.setAttribute('viewBox', '0 0 100 100');

public makePenIcon(tipThickness: number, color: string|Color4) {
public makePenIcon(tipThickness: number, color: string|Color4): IconType {
if (color instanceof Color4) {

@@ -351,3 +355,3 @@ color = color.toHexString();

public makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory) {
public makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType {
const toolThickness = pen.getThickness();

@@ -383,3 +387,3 @@

public makePipetteIcon(color?: Color4) {
public makePipetteIcon(color?: Color4): IconType {
const icon = document.createElementNS(svgNamespace, 'svg');

@@ -438,3 +442,3 @@ const pipette = document.createElementNS(svgNamespace, 'path');

public makeResizeViewportIcon() {
public makeResizeViewportIcon(): IconType {
return this.makeIconFromPath(`

@@ -452,3 +456,3 @@ M 75 5 75 10 90 10 90 25 95 25 95 5 75 5 z

public makeDuplicateSelectionIcon() {
public makeDuplicateSelectionIcon(): IconType {
return this.makeIconFromPath(`

@@ -460,3 +464,3 @@ M 45,10 45,55 90,55 90,10 45,10 z

public makeDeleteSelectionIcon() {
public makeDeleteSelectionIcon(): IconType {
const strokeWidth = '5px';

@@ -472,3 +476,3 @@ const strokeColor = 'var(--icon-color)';

public makeSaveIcon() {
public makeSaveIcon(): IconType {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');

@@ -475,0 +479,0 @@ svg.innerHTML = `

@@ -108,8 +108,12 @@ import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';

// Use a logarithmic scale for thicknessInput (finer control over thinner strokewidths.)
const inverseThicknessInputFn = (t: number) => Math.log10(t);
const thicknessInputFn = (t: number) => 10**t;
thicknessInput.type = 'range';
thicknessInput.min = '2';
thicknessInput.max = '20';
thicknessInput.step = '1';
thicknessInput.min = `${inverseThicknessInputFn(2)}`;
thicknessInput.max = `${inverseThicknessInputFn(400)}`;
thicknessInput.step = '0.1';
thicknessInput.oninput = () => {
this.tool.setThickness(parseFloat(thicknessInput.value) ** 2);
this.tool.setThickness(thicknessInputFn(parseFloat(thicknessInput.value)));
};

@@ -146,3 +150,3 @@ thicknessRow.appendChild(thicknessLabel);

colorInput.value = this.tool.getColor().toHexString();
thicknessInput.value = Math.sqrt(this.tool.getThickness()).toString();
thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();

@@ -149,0 +153,0 @@ objectTypeSelect.replaceChildren();

@@ -11,3 +11,3 @@ /**

import { PasteEvent } from '../types';
import { Mat33, Rect2, Vec2 } from '../math/lib';
import { Mat33, Rect2 } from '../math/lib';
import BaseTool from './BaseTool';

@@ -129,25 +129,3 @@ import EditorImage from '../EditorImage';

const lines = text.split('\n');
let lastComponent: TextComponent|null = null;
const components: TextComponent[] = [];
for (const line of lines) {
let position = Vec2.zero;
if (lastComponent) {
const lineMargin = Math.floor(pastedTextStyle.size);
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
}
const component = new TextComponent([ line ], Mat33.translation(position), pastedTextStyle);
components.push(component);
lastComponent = component;
}
if (components.length === 1) {
await this.addComponentsFromPaste([ components[0] ]);
} else {
// Wrap the existing `TextComponent`s --- dragging one component should drag all.
await this.addComponentsFromPaste([
new TextComponent(components, Mat33.identity, pastedTextStyle)
]);
}
await this.addComponentsFromPaste([ TextComponent.fromLines(lines, Mat33.identity, pastedTextStyle) ]);
}

@@ -154,0 +132,0 @@

import Color4 from '../Color4';
import Text, { TextStyle } from '../components/Text';
import TextComponent, { TextStyle } from '../components/Text';
import Editor from '../Editor';
import EditorImage from '../EditorImage';
import Rect2 from '../math/Rect2';
import Mat33 from '../math/Mat33';

@@ -11,2 +12,4 @@ import { Vec2 } from '../math/Vec2';

import { ToolLocalization } from './localization';
import Erase from '../commands/Erase';
import uniteCommands from '../commands/uniteCommands';

@@ -18,7 +21,10 @@ const overlayCssClass = 'textEditorOverlay';

private textEditOverlay: HTMLElement;
private textInputElem: HTMLInputElement|null = null;
private textInputElem: HTMLTextAreaElement|null = null;
private textTargetPosition: Vec2|null = null;
private textMeasuringCtx: CanvasRenderingContext2D|null = null;
private textRotation: number;
private textScale: Vec2 = Vec2.of(1, 1);
private removeExistingCommand: Erase|null = null;
public constructor(private editor: Editor, description: string, private localizationTable: ToolLocalization) {

@@ -42,6 +48,14 @@ super(editor.notifier, description);

.${overlayCssClass} input {
.${overlayCssClass} textarea {
background-color: rgba(0, 0, 0, 0);
white-space: pre;
padding: 0;
margin: 0;
border: none;
padding: 0;
min-width: 100px;
min-height: 1.1em;
}

@@ -56,3 +70,3 @@ `);

if (this.textMeasuringCtx) {
Text.applyTextStyles(this.textMeasuringCtx, style);
TextComponent.applyTextStyles(this.textMeasuringCtx, style);
return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;

@@ -67,3 +81,3 @@ }

if (this.textInputElem && this.textTargetPosition) {
const content = this.textInputElem.value;
const content = this.textInputElem.value.trimEnd();
this.textInputElem.remove();

@@ -79,2 +93,4 @@ this.textInputElem = null;

).rightMul(
this.getTextScaleMatrix()
).rightMul(
Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())

@@ -85,13 +101,21 @@ ).rightMul(

const textComponent = new Text(
[ content ],
textTransform,
this.textStyle,
);
const textComponent = TextComponent.fromLines(content.split('\n'), textTransform, this.textStyle);
const action = EditorImage.addElement(textComponent);
this.editor.dispatch(action);
if (this.removeExistingCommand) {
// Unapply so that `removeExistingCommand` can be added to the undo stack.
this.removeExistingCommand.unapply(this.editor);
this.editor.dispatch(uniteCommands([ this.removeExistingCommand, action ]));
this.removeExistingCommand = null;
} else {
this.editor.dispatch(action);
}
}
}
private getTextScaleMatrix() {
return Mat33.scaling2D(this.textScale.times(1/this.editor.viewport.getSizeOfPixelOnCanvas()));
}
private updateTextInput() {

@@ -105,3 +129,2 @@ if (!this.textInputElem || !this.textTargetPosition) {

const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
this.textInputElem.type = 'text';
this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;

@@ -119,5 +142,9 @@ this.textInputElem.style.fontFamily = this.textStyle.fontFamily;

this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
const rotation = this.textRotation + viewport.getRotationAngle();
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
const scale: Mat33 = this.getTextScaleMatrix();
this.textInputElem.style.transform = `${scale.toCSSMatrix()} rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
this.textInputElem.style.transformOrigin = 'top left';

@@ -129,6 +156,8 @@ }

this.textInputElem = document.createElement('input');
this.textInputElem = document.createElement('textarea');
this.textInputElem.value = initialText;
this.textInputElem.style.display = 'inline-block';
this.textTargetPosition = textCanvasPos;
this.textRotation = -this.editor.viewport.getRotationAngle();
this.textScale = Vec2.of(1, 1).times(this.editor.viewport.getSizeOfPixelOnCanvas());
this.updateTextInput();

@@ -138,3 +167,4 @@

if (this.textInputElem) {
this.textInputElem.size = this.textInputElem?.value.length || 10;
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
}

@@ -148,3 +178,3 @@ };

this.textInputElem.onkeyup = (evt) => {
if (evt.key === 'Enter') {
if (evt.key === 'Enter' && !evt.shiftKey) {
this.flushInput();

@@ -157,2 +187,5 @@ this.editor.focus();

this.editor.focus();
this.removeExistingCommand?.unapply(this.editor);
this.removeExistingCommand = null;
}

@@ -181,3 +214,29 @@ };

if (allPointers.length === 1) {
this.startTextInput(current.canvasPos, '');
// Are we clicking on a text node?
const canvasPos = current.canvasPos;
const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
const targetTextNodes = targetNodes.filter(node => node instanceof TextComponent) as TextComponent[];
if (targetTextNodes.length > 0) {
const targetNode = targetTextNodes[targetTextNodes.length - 1];
this.setTextStyle(targetNode.getTextStyle());
// Create and temporarily apply removeExistingCommand.
this.removeExistingCommand = new Erase([ targetNode ]);
this.removeExistingCommand.apply(this.editor);
this.startTextInput(targetNode.getBaselinePos(), targetNode.getText());
const transform = targetNode.getTransform();
this.textRotation = transform.transformVec3(Vec2.unitX).angle();
const scaleFactor = transform.transformVec3(Vec2.unitX).magnitude();
this.textScale = Vec2.of(1, 1).times(scaleFactor);
this.updateTextInput();
} else {
this.removeExistingCommand = null;
this.startTextInput(current.canvasPos, '');
}
return true;

@@ -241,2 +300,8 @@ }

}
private setTextStyle(style: TextStyle) {
// Copy the style — we may change parts of it.
this.textStyle = {...style};
this.dispatchUpdateEvent();
}
}

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