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.3.2 to 0.4.0

dist/src/language/assertions.d.ts

10

CHANGELOG.md

@@ -0,3 +1,11 @@

# 0.4.0
* Moved the selection tool rotate handle to the top, added resize horizontally and resize vertically handles.
* Selection-tool-related bug fixes
* Reduced increase in file size after rotating/resizing selected objects.
* Fix "resize to selection" button disabled when working with selections created by pasting.
* Other bug fixes
* Fix occasional stroke distortion when saving.
# 0.3.2
* PNG/JPEG image loading
* Embedded PNG/JPEG image loading
* Copy and paste

@@ -4,0 +12,0 @@ * Open images when dropped into editor

4

dist/src/components/ImageComponent.d.ts
import LineSegment2 from '../math/LineSegment2';
import Mat33 from '../math/Mat33';
import Mat33, { Mat33Array } from '../math/Mat33';
import Rect2 from '../math/Rect2';

@@ -21,3 +21,3 @@ import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';

height: number;
transform: number[];
transform: Mat33Array;
};

@@ -24,0 +24,0 @@ protected applyTransformation(affineTransfm: Mat33): void;

@@ -31,2 +31,4 @@ /**

import { EditorLocalization } from './localization';
declare type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
declare type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
export interface EditorSettings {

@@ -144,4 +146,12 @@ /** Defaults to `RenderingMode.CanvasRenderer` */

private registerListeners;
private pointers;
private getPointerList;
/**
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
* as the content of the editor.
*/
handleHTMLPointerEvent(eventType: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel', evt: PointerEvent): boolean;
private isEventSink;
private handlePaste;
handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter): void;
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */

@@ -188,2 +198,3 @@ handleKeyEventsFrom(elem: HTMLElement): void;

addStyleSheet(content: string): HTMLStyleElement;
sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean): void;
sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;

@@ -190,0 +201,0 @@ toSVG(): SVGElement;

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

this.previousAccessibilityAnnouncement = '';
this.pointers = {};
this.announceUndoCallback = (command) => {

@@ -186,77 +187,3 @@ this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this, this.localization)));

registerListeners() {
const pointers = {};
const getPointerList = () => {
const nowTime = (new Date()).getTime();
const res = [];
for (const id in pointers) {
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
if (pointers[id] && (nowTime - pointers[id].timeStamp) < maxUnupdatedTime) {
res.push(pointers[id]);
}
}
return res;
};
// May be required to prevent text selection on iOS/Safari:
// See https://stackoverflow.com/a/70992717/17055750
this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
this.renderingRegion.addEventListener('contextmenu', evt => {
// Don't show a context menu
evt.preventDefault();
});
this.renderingRegion.addEventListener('pointerdown', evt => {
const pointer = Pointer.ofEvent(evt, true, this.viewport);
pointers[pointer.id] = pointer;
this.renderingRegion.setPointerCapture(pointer.id);
const event = {
kind: InputEvtType.PointerDownEvt,
current: pointer,
allPointers: getPointerList(),
};
this.toolController.dispatchInputEvent(event);
return true;
});
this.renderingRegion.addEventListener('pointermove', evt => {
var _a, _b;
const pointer = Pointer.ofEvent(evt, (_b = (_a = pointers[evt.pointerId]) === null || _a === void 0 ? void 0 : _a.down) !== null && _b !== void 0 ? _b : false, this.viewport);
if (pointer.down) {
const prevData = pointers[pointer.id];
if (prevData) {
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
// If the pointer moved less than two pixels, don't send a new event.
if (distanceMoved < 2) {
return;
}
}
pointers[pointer.id] = pointer;
if (this.toolController.dispatchInputEvent({
kind: InputEvtType.PointerMoveEvt,
current: pointer,
allPointers: getPointerList(),
})) {
evt.preventDefault();
}
}
});
const pointerEnd = (evt) => {
const pointer = Pointer.ofEvent(evt, false, this.viewport);
if (!pointers[pointer.id]) {
return;
}
pointers[pointer.id] = pointer;
this.renderingRegion.releasePointerCapture(pointer.id);
if (this.toolController.dispatchInputEvent({
kind: InputEvtType.PointerUpEvt,
current: pointer,
allPointers: getPointerList(),
})) {
evt.preventDefault();
}
delete pointers[pointer.id];
};
this.renderingRegion.addEventListener('pointerup', evt => {
pointerEnd(evt);
});
this.renderingRegion.addEventListener('pointercancel', evt => {
pointerEnd(evt);
});
this.handlePointerEventsFrom(this.renderingRegion);
this.handleKeyEventsFrom(this.renderingRegion);

@@ -287,3 +214,5 @@ this.container.addEventListener('wheel', evt => {

}
const pos = Vec2.of(evt.offsetX, evt.offsetY);
// Ensure that `pos` is relative to `this.container`
const bbox = this.container.getBoundingClientRect();
const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
if (this.toolController.dispatchInputEvent({

@@ -330,2 +259,74 @@ kind: InputEvtType.WheelEvt,

}
getPointerList() {
const nowTime = (new Date()).getTime();
const res = [];
for (const id in this.pointers) {
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
if (this.pointers[id] && (nowTime - this.pointers[id].timeStamp) < maxUnupdatedTime) {
res.push(this.pointers[id]);
}
}
return res;
}
/**
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
* as the content of the editor.
*/
handleHTMLPointerEvent(eventType, evt) {
var _a, _b, _c;
const eventsRelativeTo = this.renderingRegion;
const eventTarget = (_a = evt.target) !== null && _a !== void 0 ? _a : this.renderingRegion;
if (eventType === 'pointerdown') {
const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
this.pointers[pointer.id] = pointer;
eventTarget.setPointerCapture(pointer.id);
const event = {
kind: InputEvtType.PointerDownEvt,
current: pointer,
allPointers: this.getPointerList(),
};
this.toolController.dispatchInputEvent(event);
return true;
}
else if (eventType === 'pointermove') {
const pointer = Pointer.ofEvent(evt, (_c = (_b = this.pointers[evt.pointerId]) === null || _b === void 0 ? void 0 : _b.down) !== null && _c !== void 0 ? _c : false, this.viewport, eventsRelativeTo);
if (pointer.down) {
const prevData = this.pointers[pointer.id];
if (prevData) {
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
// If the pointer moved less than two pixels, don't send a new event.
if (distanceMoved < 2) {
return false;
}
}
this.pointers[pointer.id] = pointer;
if (this.toolController.dispatchInputEvent({
kind: InputEvtType.PointerMoveEvt,
current: pointer,
allPointers: this.getPointerList(),
})) {
evt.preventDefault();
}
}
return true;
}
else if (eventType === 'pointercancel' || eventType === 'pointerup') {
const pointer = Pointer.ofEvent(evt, false, this.viewport, eventsRelativeTo);
if (!this.pointers[pointer.id]) {
return false;
}
this.pointers[pointer.id] = pointer;
eventTarget.releasePointerCapture(pointer.id);
if (this.toolController.dispatchInputEvent({
kind: InputEvtType.PointerUpEvt,
current: pointer,
allPointers: this.getPointerList(),
})) {
evt.preventDefault();
}
delete this.pointers[pointer.id];
return true;
}
return eventType;
}
isEventSink(evtTarget) {

@@ -418,2 +419,20 @@ let currentElem = evtTarget;

}
handlePointerEventsFrom(elem, filter) {
// May be required to prevent text selection on iOS/Safari:
// See https://stackoverflow.com/a/70992717/17055750
elem.addEventListener('touchstart', evt => evt.preventDefault());
elem.addEventListener('contextmenu', evt => {
// Don't show a context menu
evt.preventDefault();
});
const eventNames = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
for (const eventName of eventNames) {
elem.addEventListener(eventName, evt => {
if (filter && !filter(eventName, evt)) {
return true;
}
return this.handleHTMLPointerEvent(eventName, evt);
});
}
}
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */

@@ -585,2 +604,11 @@ handleKeyEventsFrom(elem) {

}
// Dispatch a keyboard event to the currently selected tool.
// Intended for unit testing
sendKeyboardEvent(eventType, key, ctrlKey = false) {
this.toolController.dispatchInputEvent({
kind: eventType,
key,
ctrlKey
});
}
// Dispatch a pen event to the currently selected tool.

@@ -587,0 +615,0 @@ // Intended primarially for unit tests.

import { Point2, Vec2 } from './Vec2';
import Vec3 from './Vec3';
export declare type Mat33Array = [
number,
number,
number,
number,
number,
number,
number,
number,
number
];
/**

@@ -71,3 +82,17 @@ * Represents a three dimensional linear transformation or

*/
toArray(): number[];
toArray(): Mat33Array;
/**
* @example
* ```
* new Mat33(
* 1, 2, 3,
* 4, 5, 6,
* 7, 8, 9,
* ).mapEntries(component => component - 1);
* // → ⎡ 0, 1, 2 ⎤
* // ⎢ 3, 4, 5 ⎥
* // ⎣ 6, 7, 8 ⎦
* ```
*/
mapEntries(mapping: (component: number) => number): Mat33;
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */

@@ -77,4 +102,15 @@ static translation(amount: Vec2): Mat33;

static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
/** Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. */
/** @see {@link !fromCSSMatrix} */
toCSSMatrix(): string;
/**
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
*
* Note that such a matrix has the form,
* ```
* ⎡ a c e ⎤
* ⎢ b d f ⎥
* ⎣ 0 0 1 ⎦
* ```
*/
static fromCSSMatrix(cssString: string): Mat33;
}

@@ -186,2 +186,18 @@ import { Vec2 } from './Vec2';

}
/**
* @example
* ```
* new Mat33(
* 1, 2, 3,
* 4, 5, 6,
* 7, 8, 9,
* ).mapEntries(component => component - 1);
* // → ⎡ 0, 1, 2 ⎤
* // ⎢ 3, 4, 5 ⎥
* // ⎣ 6, 7, 8 ⎦
* ```
*/
mapEntries(mapping) {
return new Mat33(mapping(this.a1), mapping(this.a2), mapping(this.a3), mapping(this.b1), mapping(this.b2), mapping(this.b3), mapping(this.c1), mapping(this.c2), mapping(this.c3));
}
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */

@@ -218,3 +234,16 @@ static translation(amount) {

}
/** Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. */
/** @see {@link !fromCSSMatrix} */
toCSSMatrix() {
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
}
/**
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
*
* Note that such a matrix has the form,
* ```
* ⎡ a c e ⎤
* ⎢ b d f ⎥
* ⎣ 0 0 1 ⎦
* ```
*/
static fromCSSMatrix(cssString) {

@@ -221,0 +250,0 @@ if (cssString === '' || cssString === 'none') {

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

private cachedStringVersion;
toString(): string;
toString(useNonAbsCommands?: boolean): string;
serialize(): string;

@@ -61,0 +61,0 @@ static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands?: boolean): string;

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

}
toString() {
toString(useNonAbsCommands) {
if (this.cachedStringVersion) {
return this.cachedStringVersion;
}
// Hueristic: Try to determine whether converting absolute to relative commands is worth it.
const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
const result = Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
if (useNonAbsCommands === undefined) {
// Hueristic: Try to determine whether converting absolute to relative commands is worth it.
useNonAbsCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
}
const result = Path.toString(this.startPoint, this.parts, !useNonAbsCommands);
this.cachedStringVersion = result;

@@ -311,6 +313,8 @@ return result;

for (const point of points) {
const xComponent = toRoundedString(point.x);
const yComponent = toRoundedString(point.y);
// Relative commands are often shorter as strings than absolute commands.
if (!makeAbsCommand) {
const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint.x, roundedPrevX, roundedPrevY);
const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint.y, roundedPrevX, roundedPrevY);
const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint.x, xComponent, roundedPrevX, roundedPrevY);
const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint.y, yComponent, roundedPrevX, roundedPrevY);
// No need for an additional separator if it starts with a '-'

@@ -325,4 +329,2 @@ if (yComponentRelative.charAt(0) === '-') {

else {
const xComponent = toRoundedString(point.x);
const yComponent = toRoundedString(point.y);
absoluteCommandParts.push(`${xComponent},${yComponent}`);

@@ -329,0 +331,0 @@ }

@@ -0,3 +1,4 @@

export declare const cleanUpNumber: (text: string) => string;
export declare const toRoundedString: (num: number) => string;
export declare const getLenAfterDecimal: (numberAsString: string) => number;
export declare const toStringOfSamePrecision: (num: number, ...references: string[]) => string;
// @packageDocumentation @internal
// Clean up stringified numbers
const cleanUpNumber = (text) => {
export const cleanUpNumber = (text) => {
// Regular expression substitions can be somewhat expensive. Only do them
// if necessary.
if (text.indexOf('e') > 0) {
// Round to zero.
if (text.match(/[eE][-]\d{2,}$/)) {
return '0';
}
}
const lastChar = text.charAt(text.length - 1);

@@ -13,5 +19,2 @@ if (lastChar === '0' || lastChar === '.') {

text = text.replace(/[.]$/, '');
if (text === '-0') {
return '0';
}
}

@@ -23,3 +26,7 @@ const firstChar = text.charAt(0);

text = text.replace(/^-(0+)[.]/, '-.');
text = text.replace(/^(-?)0+$/, '$10');
}
if (text === '-0') {
return '0';
}
return text;

@@ -30,4 +37,4 @@ };

// (or nines) just one or two digits, it's probably a rounding error.
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,2}$/;
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,2}$/;
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
let text = num.toString(10);

@@ -34,0 +41,0 @@ if (text.indexOf('.') === -1) {

@@ -42,2 +42,12 @@ /**

/**
* If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
* if `other is a `number`, returns the result of scalar multiplication.
*
* @example
* ```
* Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
* ```
*/
scale(other: Vec3 | number): Vec3;
/**
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated

@@ -77,3 +87,3 @@ * 90 degrees counter-clockwise.

map(fn: (component: number, index: number) => number): Vec3;
asArray(): number[];
asArray(): [number, number, number];
/**

@@ -80,0 +90,0 @@ * [fuzz] The maximum difference between two components for this and [other]

@@ -80,2 +80,17 @@ /**

/**
* If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
* if `other is a `number`, returns the result of scalar multiplication.
*
* @example
* ```
* Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
* ```
*/
scale(other) {
if (typeof other === 'number') {
return this.times(other);
}
return Vec3.of(this.x * other.x, this.y * other.y, this.z * other.z);
}
/**
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated

@@ -82,0 +97,0 @@ * 90 degrees counter-clockwise.

@@ -21,4 +21,4 @@ import { Point2 } from './math/Vec2';

private constructor();
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer;
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer;
static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
}

@@ -34,6 +34,11 @@ import { Vec2 } from './math/Vec2';

}
// Creates a Pointer from a DOM event.
static ofEvent(evt, isDown, viewport) {
// Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
// considered the top left of `relativeTo`.
static ofEvent(evt, isDown, viewport, relativeTo) {
var _a, _b;
const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
let screenPos = Vec2.of(evt.clientX, evt.clientY);
if (relativeTo) {
const bbox = relativeTo.getBoundingClientRect();
screenPos = screenPos.minus(Vec2.of(bbox.left, bbox.top));
}
const pointerTypeToDevice = {

@@ -40,0 +45,0 @@ 'mouse': PointerDevice.PrimaryButtonMouse,

@@ -67,3 +67,4 @@ import Path, { PathCommandType } from '../../math/Path';

}
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
// This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
drawRect(rect, lineWidth, lineFill) {

@@ -70,0 +71,0 @@ const path = Path.fromRect(rect, lineWidth);

import { EditorEventType } from '../types';
import { coloris, init as colorisInit } from '@melloware/coloris';
import Color4 from '../Color4';
import SelectionTool from '../tools/SelectionTool';
import { defaultToolbarLocalization } from './localization';
import { makeRedoIcon, makeUndoIcon } from './icons';
import PanZoom from '../tools/PanZoom';
import SelectionTool from '../tools/SelectionTool/SelectionTool';
import PanZoomTool from '../tools/PanZoom';
import TextTool from '../tools/TextTool';
import EraserTool from '../tools/Eraser';
import PenTool from '../tools/Pen';
import PenToolWidget from './widgets/PenToolWidget';

@@ -14,3 +16,2 @@ import EraserWidget from './widgets/EraserToolWidget';

import HandToolWidget from './widgets/HandToolWidget';
import { EraserTool, PenTool } from '../tools/lib';
export const toolbarCSSPrefix = 'toolbar-';

@@ -162,3 +163,3 @@ export default class HTMLToolbar {

}
const panZoomTool = toolController.getMatchingTools(PanZoom)[0];
const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
if (panZoomTool) {

@@ -165,0 +166,0 @@ this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));

import Editor from '../../Editor';
import SelectionTool from '../../tools/SelectionTool';
import SelectionTool from '../../tools/SelectionTool/SelectionTool';
import { ToolbarLocalization } from '../localization';

@@ -4,0 +4,0 @@ import BaseToolWidget from './BaseToolWidget';

@@ -12,4 +12,4 @@ /**

export { default as TextTool } from './TextTool';
export { default as SelectionTool } from './SelectionTool';
export { default as SelectionTool } from './SelectionTool/SelectionTool';
export { default as EraserTool } from './Eraser';
export { default as PasteHandler } from './PasteHandler';

@@ -12,4 +12,4 @@ /**

export { default as TextTool } from './TextTool';
export { default as SelectionTool } from './SelectionTool';
export { default as SelectionTool } from './SelectionTool/SelectionTool';
export { default as EraserTool } from './Eraser';
export { default as PasteHandler } from './PasteHandler';

@@ -20,6 +20,7 @@ /**

import EditorImage from '../EditorImage';
import SelectionTool from './SelectionTool';
import SelectionTool from './SelectionTool/SelectionTool';
import TextTool from './TextTool';
import Color4 from '../Color4';
import ImageComponent from '../components/ImageComponent';
import Viewport from '../Viewport';
// { @inheritDoc PasteHandler! }

@@ -70,2 +71,3 @@ export default class PasteHandler extends BaseTool {

scaleRatio *= 2 / 3;
scaleRatio = Viewport.roundScaleRatio(scaleRatio);
const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));

@@ -72,0 +74,0 @@ const commands = [];

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

if (newThickness !== undefined) {
newThickness = Math.min(Math.max(1, newThickness), 128);
newThickness = Math.min(Math.max(1, newThickness), 256);
this.setThickness(newThickness);

@@ -137,0 +137,0 @@ return true;

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

import Eraser from './Eraser';
import SelectionTool from './SelectionTool';
import SelectionTool from './SelectionTool/SelectionTool';
import Color4 from '../Color4';

@@ -9,0 +9,0 @@ import UndoRedoShortcut from './UndoRedoShortcut';

@@ -11,3 +11,3 @@ import EventDispatcher from './EventDispatcher';

import Command from './commands/Command';
import { BaseWidget } from './lib';
import BaseWidget from './toolbar/widgets/BaseWidget';
export interface PointerEvtListener {

@@ -14,0 +14,0 @@ onPointerDown(event: PointerEvt): boolean;

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

roundPoint(point: Point2): Point2;
static roundScaleRatio(scaleRatio: number, roundAmount?: number): number;
computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Mat33;

@@ -34,0 +35,0 @@ zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;

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

}
// Returns the angle of the canvas in radians
// Returns the angle of the canvas in radians.
// This is the angle by which the canvas is rotated relative to the screen.
getRotationAngle() {

@@ -98,2 +99,14 @@ return this.transform.transformVec3(Vec3.unitX).angle();

}
// `roundAmount`: An integer >= 0, larger numbers cause less rounding. Smaller numbers cause more
// (as such `roundAmount = 0` does the most rounding).
static roundScaleRatio(scaleRatio, roundAmount = 1) {
if (Math.abs(scaleRatio) <= 1e-12) {
return 0;
}
// Represent as k 10ⁿ for some n, k ∈ ℤ.
const decimalComponent = Math.pow(10, Math.floor(Math.log10(Math.abs(scaleRatio))));
const roundAnountFactor = Math.pow(2, roundAmount);
scaleRatio = Math.round(scaleRatio / decimalComponent * roundAnountFactor) / roundAnountFactor * decimalComponent;
return scaleRatio;
}
// Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.

@@ -100,0 +113,0 @@ computeZoomToTransform(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {

{
"name": "js-draw",
"version": "0.3.2",
"version": "0.4.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",

@@ -7,6 +7,4 @@ # js-draw

For example usage, see [docs/example/example.ts](docs/example/example.ts) or read [the (work-in-progress) documentation](https://personalizedrefrigerator.github.io/js-draw/typedoc/modules/lib.html).
For example usage, see [one of the examples](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/examples.md) or read [the (work-in-progress) documentation](https://personalizedrefrigerator.github.io/js-draw/typedoc/modules/lib.html).
At present, not all documented modules are `import`-able.
# API

@@ -13,0 +11,0 @@

@@ -5,3 +5,3 @@ import SerializableCommand from '../commands/SerializableCommand';

import LineSegment2 from '../math/LineSegment2';
import Mat33 from '../math/Mat33';
import Mat33, { Mat33Array } from '../math/Mat33';
import Rect2 from '../math/Rect2';

@@ -187,7 +187,3 @@ import { EditorLocalization } from '../localization';

const transform = new Mat33(...(json.transfm as [
number, number, number,
number, number, number,
number, number, number,
]));
const transform = new Mat33(...(json.transfm as Mat33Array));

@@ -194,0 +190,0 @@ if (!elem) {

import LineSegment2 from '../math/LineSegment2';
import Mat33 from '../math/Mat33';
import Mat33, { Mat33Array } from '../math/Mat33';
import Rect2 from '../math/Rect2';

@@ -144,7 +144,3 @@ import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';

label: data.label,
transform: new Mat33(...(data.transform as [
number, number, number,
number, number, number,
number, number, number,
])),
transform: new Mat33(...(data.transform as Mat33Array)),
});

@@ -151,0 +147,0 @@ }

import LineSegment2 from '../math/LineSegment2';
import Mat33 from '../math/Mat33';
import Mat33, { Mat33Array } from '../math/Mat33';
import Rect2 from '../math/Rect2';

@@ -203,7 +203,3 @@ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';

const transformData = json.transform as [
number, number, number,
number, number, number,
number, number, number,
];
const transformData = json.transform as Mat33Array;
const transform = new Mat33(...transformData);

@@ -210,0 +206,0 @@

/**
* The main entrypoint for the full editor.
*
*
* @example

@@ -8,3 +8,3 @@ * To create an editor with a toolbar,

* const editor = new Editor(document.body);
*
*
* const toolbar = editor.addToolbar();

@@ -16,3 +16,3 @@ * toolbar.addActionButton('Save', () => {

* ```
*
*
* @packageDocumentation

@@ -43,2 +43,5 @@ */

type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
export interface EditorSettings {

@@ -73,10 +76,10 @@ /** Defaults to `RenderingMode.CanvasRenderer` */

* Handles undo/redo.
*
*
* @example
* ```
* const editor = new Editor(document.body);
*
*
* // Do something undoable.
* // ...
*
*
* // Undo the last action

@@ -90,7 +93,7 @@ * editor.history.undo();

* Data structure for adding/removing/querying objects in the image.
*
*
* @example
* ```
* const editor = new Editor(document.body);
*
*
* // Create a path.

@@ -101,3 +104,3 @@ * const stroke = new Stroke([

* const addElementCommand = editor.image.addElement(stroke);
*
*
* // Add the stroke to the editor

@@ -135,3 +138,3 @@ * editor.dispatch(addElementCommand);

* const container = document.body;
*
*
* // Create an editor

@@ -143,3 +146,3 @@ * const editor = new Editor(container, {

* });
*
*
* // Add the default toolbar

@@ -235,3 +238,3 @@ * const toolbar = editor.addToolbar();

}
this.viewport.resetTransform(resetTransform);

@@ -245,3 +248,3 @@ }

* @returns a reference to the editor's container.
*
*
* @example

@@ -298,92 +301,3 @@ * ```

private registerListeners() {
const pointers: Record<number, Pointer> = {};
const getPointerList = () => {
const nowTime = (new Date()).getTime();
const res: Pointer[] = [];
for (const id in pointers) {
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
if (pointers[id] && (nowTime - pointers[id].timeStamp) < maxUnupdatedTime) {
res.push(pointers[id]);
}
}
return res;
};
// May be required to prevent text selection on iOS/Safari:
// See https://stackoverflow.com/a/70992717/17055750
this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
this.renderingRegion.addEventListener('contextmenu', evt => {
// Don't show a context menu
evt.preventDefault();
});
this.renderingRegion.addEventListener('pointerdown', evt => {
const pointer = Pointer.ofEvent(evt, true, this.viewport);
pointers[pointer.id] = pointer;
this.renderingRegion.setPointerCapture(pointer.id);
const event: PointerEvt = {
kind: InputEvtType.PointerDownEvt,
current: pointer,
allPointers: getPointerList(),
};
this.toolController.dispatchInputEvent(event);
return true;
});
this.renderingRegion.addEventListener('pointermove', evt => {
const pointer = Pointer.ofEvent(
evt, pointers[evt.pointerId]?.down ?? false, this.viewport
);
if (pointer.down) {
const prevData = pointers[pointer.id];
if (prevData) {
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
// If the pointer moved less than two pixels, don't send a new event.
if (distanceMoved < 2) {
return;
}
}
pointers[pointer.id] = pointer;
if (this.toolController.dispatchInputEvent({
kind: InputEvtType.PointerMoveEvt,
current: pointer,
allPointers: getPointerList(),
})) {
evt.preventDefault();
}
}
});
const pointerEnd = (evt: PointerEvent) => {
const pointer = Pointer.ofEvent(evt, false, this.viewport);
if (!pointers[pointer.id]) {
return;
}
pointers[pointer.id] = pointer;
this.renderingRegion.releasePointerCapture(pointer.id);
if (this.toolController.dispatchInputEvent({
kind: InputEvtType.PointerUpEvt,
current: pointer,
allPointers: getPointerList(),
})) {
evt.preventDefault();
}
delete pointers[pointer.id];
};
this.renderingRegion.addEventListener('pointerup', evt => {
pointerEnd(evt);
});
this.renderingRegion.addEventListener('pointercancel', evt => {
pointerEnd(evt);
});
this.handlePointerEventsFrom(this.renderingRegion);
this.handleKeyEventsFrom(this.renderingRegion);

@@ -418,3 +332,6 @@

const pos = Vec2.of(evt.offsetX, evt.offsetY);
// Ensure that `pos` is relative to `this.container`
const bbox = this.container.getBoundingClientRect();
const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
if (this.toolController.dispatchInputEvent({

@@ -474,2 +391,87 @@ kind: InputEvtType.WheelEvt,

private pointers: Record<number, Pointer> = {};
private getPointerList() {
const nowTime = (new Date()).getTime();
const res: Pointer[] = [];
for (const id in this.pointers) {
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
if (this.pointers[id] && (nowTime - this.pointers[id].timeStamp) < maxUnupdatedTime) {
res.push(this.pointers[id]);
}
}
return res;
}
/**
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
* as the content of the editor.
*/
public handleHTMLPointerEvent(eventType: 'pointerdown'|'pointermove'|'pointerup'|'pointercancel', evt: PointerEvent): boolean {
const eventsRelativeTo = this.renderingRegion;
const eventTarget = (evt.target as HTMLElement|null) ?? this.renderingRegion;
if (eventType === 'pointerdown') {
const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
this.pointers[pointer.id] = pointer;
eventTarget.setPointerCapture(pointer.id);
const event: PointerEvt = {
kind: InputEvtType.PointerDownEvt,
current: pointer,
allPointers: this.getPointerList(),
};
this.toolController.dispatchInputEvent(event);
return true;
}
else if (eventType === 'pointermove') {
const pointer = Pointer.ofEvent(
evt, this.pointers[evt.pointerId]?.down ?? false, this.viewport, eventsRelativeTo
);
if (pointer.down) {
const prevData = this.pointers[pointer.id];
if (prevData) {
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
// If the pointer moved less than two pixels, don't send a new event.
if (distanceMoved < 2) {
return false;
}
}
this.pointers[pointer.id] = pointer;
if (this.toolController.dispatchInputEvent({
kind: InputEvtType.PointerMoveEvt,
current: pointer,
allPointers: this.getPointerList(),
})) {
evt.preventDefault();
}
}
return true;
}
else if (eventType === 'pointercancel' || eventType === 'pointerup') {
const pointer = Pointer.ofEvent(evt, false, this.viewport, eventsRelativeTo);
if (!this.pointers[pointer.id]) {
return false;
}
this.pointers[pointer.id] = pointer;
eventTarget.releasePointerCapture(pointer.id);
if (this.toolController.dispatchInputEvent({
kind: InputEvtType.PointerUpEvt,
current: pointer,
allPointers: this.getPointerList(),
})) {
evt.preventDefault();
}
delete this.pointers[pointer.id];
return true;
}
return eventType;
}
private isEventSink(evtTarget: Element|EventTarget|null) {

@@ -569,2 +571,23 @@ let currentElem: Element|null = evtTarget as Element|null;

public handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter) {
// May be required to prevent text selection on iOS/Safari:
// See https://stackoverflow.com/a/70992717/17055750
elem.addEventListener('touchstart', evt => evt.preventDefault());
elem.addEventListener('contextmenu', evt => {
// Don't show a context menu
evt.preventDefault();
});
const eventNames: HTMLPointerEventType[] = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
for (const eventName of eventNames) {
elem.addEventListener(eventName, evt => {
if (filter && !filter(eventName, evt)) {
return true;
}
return this.handleHTMLPointerEvent(eventName, evt);
});
}
}
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */

@@ -584,3 +607,3 @@ public handleKeyEventsFrom(elem: HTMLElement) {

this.renderingRegion.blur();
}
}
});

@@ -627,7 +650,7 @@

* called.
*
*
* Prefer `command.apply(editor)` for incomplete commands. `dispatchNoAnnounce` may allow
* clients to listen for the application of commands (e.g. `SerializableCommand`s so they can
* be sent across the network), while `apply` does not.
*
*
* @example

@@ -774,2 +797,16 @@ * ```

// Dispatch a keyboard event to the currently selected tool.
// Intended for unit testing
public sendKeyboardEvent(
eventType: InputEvtType.KeyPressEvent|InputEvtType.KeyUpEvent,
key: string,
ctrlKey: boolean = false
) {
this.toolController.dispatchInputEvent({
kind: eventType,
key,
ctrlKey
});
}
// Dispatch a pen event to the currently selected tool.

@@ -884,3 +921,3 @@ // Intended primarially for unit tests.

* Alias for loadFrom(SVGLoader.fromString).
*
*
* This is particularly useful when accessing a bundled version of the editor,

@@ -887,0 +924,0 @@ * where `SVGLoader.fromString` is unavailable.

@@ -145,2 +145,16 @@ import Mat33 from './Mat33';

it('should correctly apply a mapping to all components', () => {
expect(
new Mat33(
1, 2, 3,
4, 5, 6,
7, 8, 9,
).mapEntries(component => component - 1)
).toMatchObject(new Mat33(
0, 1, 2,
3, 4, 5,
6, 7, 8,
));
});
it('should convert CSS matrix(...) strings to matricies', () => {

@@ -147,0 +161,0 @@ // From MDN:

import { Point2, Vec2 } from './Vec2';
import Vec3 from './Vec3';
export type Mat33Array = [
number, number, number,
number, number, number,
number, number, number,
];
/**

@@ -242,3 +248,3 @@ * Represents a three dimensional linear transformation or

*/
public toArray(): number[] {
public toArray(): Mat33Array {
return [

@@ -251,2 +257,23 @@ this.a1, this.a2, this.a3,

/**
* @example
* ```
* new Mat33(
* 1, 2, 3,
* 4, 5, 6,
* 7, 8, 9,
* ).mapEntries(component => component - 1);
* // → ⎡ 0, 1, 2 ⎤
* // ⎢ 3, 4, 5 ⎥
* // ⎣ 6, 7, 8 ⎦
* ```
*/
public mapEntries(mapping: (component: number)=>number): Mat33 {
return new Mat33(
mapping(this.a1), mapping(this.a2), mapping(this.a3),
mapping(this.b1), mapping(this.b2), mapping(this.b3),
mapping(this.c1), mapping(this.c2), mapping(this.c3),
);
}
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */

@@ -302,3 +329,17 @@ public static translation(amount: Vec2): Mat33 {

/** Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. */
/** @see {@link !fromCSSMatrix} */
public toCSSMatrix(): string {
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
}
/**
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
*
* Note that such a matrix has the form,
* ```
* ⎡ a c e ⎤
* ⎢ b d f ⎥
* ⎣ 0 0 1 ⎦
* ```
*/
public static fromCSSMatrix(cssString: string): Mat33 {

@@ -305,0 +346,0 @@ if (cssString === '' || cssString === 'none') {

@@ -41,3 +41,3 @@ import Path, { PathCommandType } from './Path';

kind: PathCommandType.LineTo,
point: Vec2.of(30.0001, 40.000000001),
point: Vec2.of(30.00000001, 40.000000001),
},

@@ -57,2 +57,13 @@ ]);

});
it('should not lose precision when saving', () => {
const pathStr = 'M184.2,52.3l-.2-.2q-2.7,2.4 -3.2,3.5q-2.8,7 -.9,6.1q4.3-2.6 4.8-6.1q1.2-8.8 .4-8.3q-4.2,5.2 -3.9,3.9q.2-1.6 .3-2.1q.2-1.3 -.2-1q-3.8,6.5 -3.2,3.3q.6-4.1 1.1-5.3q4.1-10 3.3-8.3q-5.3,13.1 -6.6,14.1q-3.3,2.8 -1.8-1.5q2.8-9.7 2.7-8.4q0,.3 0,.4q-1.4,7.1 -2.7,8.5q-2.6,3.2 -2.5,2.9q-.3-1.9 -.7-1.9q-4.1,4.4 -2.9,1.9q1.1-3 .3-2.6q-1.8,2 -2.5,2.4q-4.5,2.8 -4.2,1.9q.3-1.6 .2-1.4q1.5,2.2 1.3,2.9q-.8,3.9 -.5,3.3q.8-7.6 2.5-13.3q2.6-9.2 2.9-6.9q.3,1.4 .3,1.2q-.7-.4 -.9,0q-2.2,11.6 -7.6,13.6q-3.9,1.6 -2.1-1.3q3-5.5 2.6-3.4q-.2,1.8 -.5,1.8q-3.2,.5 -4.1,1.2q-2.6,2.6 -1.9,2.5q4.7-4.4 3.7-5.5q-1.1-.9 -1.6-.6q-7.2,7.5 -3.9,6.5q.3-.1 .4-.4q.6-5.3 -.2-4.9q-2.8,2.3 -3.1,2.4q-3.7,1.5 -3.5,.5q.3-3.6 1.4-3.3q3.5,.7 1.9,2.4q-1.7,2.3 -1.6,.8q0-3.5 -.9-3.1q-5.1,3.3 -4.9,2.8q.1-4 -.8-3.5q-4.3,3.4 -4.6,2.5q-1-2.1 .5-8.7l-.2,0q-1.6,6.6 -.7,8.9q.7,1.2 5.2-2.3q.4-.5 .2,3.1q.1,1 5.5-2.4q.4-.4 .3,2.7q.1,2 2.4-.4q1.7-2.3 -2.1-3.2q-1.7-.3 -2,3.7q0,1.4 4.1-.1q.3-.1 3.1-2.4q.3-.5 -.4,4.5q0-.1 -.2,0q-2.6,1.2 4.5-5.7q0-.2 .8,.6q.9,.6 -3.7,4.7q-.5,1 2.7-1.7q.6-.7 3.7-1.2q.7-.2 .9-2.2q.1-2.7 -3.4,3.2q-1.8,3.4 2.7,1.9q5.6-2.1 7.8-14q-.1,.1 .3,.4q.6,.1 .3-1.6q-.7-2.8 -3.7,6.7q-1.8,5.8 -2.5,13.5q.1,1.1 1.3-3.1q.2-1 -1.3-3.3q-.5-.5 -1,1.6q-.1,1.3 4.8-1.5q1-1 3-2q.1-.4 -1.1,2q-1.1,3.1 3.7-1.3q-.4,0 -.1,1.5q.3,.8 3.3-2.5q1.3-1.6 2.7-8.9q0-.1 0-.4q-.3-1.9 -3.5,8.2q-1.3,4.9 2.4,2.1q1.4-1.2 6.6-14.3q.8-2.4 -3.9,7.9q-.6,1.3 -1.1,5.5q-.3,3.7 4-3.1q-.2,0 -.6,.6q-.2,.6 -.3,2.3q0,1.8 4.7-3.5q.1-.5 -1.2,7.9q-.5,3.2 -4.6,5.7q-1.3,1 1.5-5.5q.4-1.1 3.01-3.5';
const path1 = Path.fromString(pathStr);
path1['cachedStringVersion'] = null; // Clear the cache.
const path = Path.fromString(path1.toString(true));
path1['cachedStringVersion'] = null; // Clear the cache.
expect(path.toString(true)).toBe(path1.toString(true));
});
});

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

public toString(): string {
public toString(useNonAbsCommands?: boolean): string {
if (this.cachedStringVersion) {

@@ -387,6 +387,8 @@ return this.cachedStringVersion;

// Hueristic: Try to determine whether converting absolute to relative commands is worth it.
const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
if (useNonAbsCommands === undefined) {
// Hueristic: Try to determine whether converting absolute to relative commands is worth it.
useNonAbsCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
}
const result = Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
const result = Path.toString(this.startPoint, this.parts, !useNonAbsCommands);
this.cachedStringVersion = result;

@@ -414,6 +416,9 @@ return result;

for (const point of points) {
const xComponent = toRoundedString(point.x);
const yComponent = toRoundedString(point.y);
// Relative commands are often shorter as strings than absolute commands.
if (!makeAbsCommand) {
const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint!.x, roundedPrevX, roundedPrevY);
const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint!.y, roundedPrevX, roundedPrevY);
const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint!.x, xComponent, roundedPrevX, roundedPrevY);
const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint!.y, yComponent, roundedPrevX, roundedPrevY);

@@ -427,5 +432,2 @@ // No need for an additional separator if it starts with a '-'

} else {
const xComponent = toRoundedString(point.x);
const yComponent = toRoundedString(point.y);
absoluteCommandParts.push(`${xComponent},${yComponent}`);

@@ -432,0 +434,0 @@ }

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

import { toRoundedString, toStringOfSamePrecision } from './rounding';
import { cleanUpNumber, toRoundedString, toStringOfSamePrecision } from './rounding';

@@ -15,10 +15,19 @@ describe('toRoundedString', () => {

// Handling this creates situations with potential error:
//it('should round strings with multiple digits after the ending decimal points', () => {
// expect(toRoundedString(292.2 - 292.8)).toBe('-0.6');
//});
it('should round strings with multiple digits after the ending decimal points', () => {
expect(toRoundedString(292.2 - 292.8)).toBe('-.6');
expect(toRoundedString(4.06425600000023)).toBe('4.064256');
});
it('should round down strings ending endings similar to .00000001', () => {
expect(toRoundedString(10.00000001)).toBe('10');
expect(toRoundedString(-30.00000001)).toBe('-30');
expect(toRoundedString(-14.20000000000002)).toBe('-14.2');
});
it('should not round numbers insufficiently close to the next', () => {
expect(toRoundedString(-10.9999)).toBe('-10.9999');
expect(toRoundedString(-10.0001)).toBe('-10.0001');
expect(toRoundedString(-10.123499)).toBe('-10.123499');
expect(toRoundedString(0.00123499)).toBe('.00123499');
});
});

@@ -28,2 +37,3 @@

expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235');
expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2');

@@ -42,2 +52,17 @@ expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23');

expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2');
});
it('cleanUpNumber', () => {
expect(cleanUpNumber('000.0000')).toBe('0');
expect(cleanUpNumber('-000.0000')).toBe('0');
expect(cleanUpNumber('0.0000')).toBe('0');
expect(cleanUpNumber('0.001')).toBe('.001');
expect(cleanUpNumber('-0.001')).toBe('-.001');
expect(cleanUpNumber('-0.000000001')).toBe('-.000000001');
expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001');
expect(cleanUpNumber('1234')).toBe('1234');
expect(cleanUpNumber('1234.5')).toBe('1234.5');
expect(cleanUpNumber('1234.500')).toBe('1234.5');
expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0');
});
// @packageDocumentation @internal
// Clean up stringified numbers
const cleanUpNumber = (text: string) => {
export const cleanUpNumber = (text: string) => {
// Regular expression substitions can be somewhat expensive. Only do them
// if necessary.
if (text.indexOf('e') > 0) {
// Round to zero.
if (text.match(/[eE][-]\d{2,}$/)) {
return '0';
}
}
const lastChar = text.charAt(text.length - 1);

@@ -15,6 +23,2 @@ if (lastChar === '0' || lastChar === '.') {

text = text.replace(/[.]$/, '');
if (text === '-0') {
return '0';
}
}

@@ -27,4 +31,9 @@

text = text.replace(/^-(0+)[.]/, '-.');
text = text.replace(/^(-?)0+$/, '$10');
}
if (text === '-0') {
return '0';
}
return text;

@@ -36,4 +45,4 @@ };

// (or nines) just one or two digits, it's probably a rounding error.
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,2}$/;
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,2}$/;
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;

@@ -40,0 +49,0 @@ let text = num.toString(10);

@@ -99,2 +99,23 @@

/**
* If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
* if `other is a `number`, returns the result of scalar multiplication.
*
* @example
* ```
* Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
* ```
*/
public scale(other: Vec3|number): Vec3 {
if (typeof other === 'number') {
return this.times(other);
}
return Vec3.of(
this.x * other.x,
this.y * other.y,
this.z * other.z,
);
}
/**
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated

@@ -162,3 +183,3 @@ * 90 degrees counter-clockwise.

public asArray(): number[] {
public asArray(): [ number, number, number ] {
return [this.x, this.y, this.z];

@@ -165,0 +186,0 @@ }

@@ -39,5 +39,10 @@ import { Point2, Vec2 } from './math/Vec2';

// Creates a Pointer from a DOM event.
public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer {
const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
// Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
// considered the top left of `relativeTo`.
public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer {
let screenPos = Vec2.of(evt.clientX, evt.clientY);
if (relativeTo) {
const bbox = relativeTo.getBoundingClientRect();
screenPos = screenPos.minus(Vec2.of(bbox.left, bbox.top));
}

@@ -44,0 +49,0 @@ const pointerTypeToDevice: Record<string, PointerDevice> = {

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

// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]
public drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void {
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
// This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
public drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle) {
const path = Path.fromRect(rect, lineWidth);

@@ -129,0 +130,0 @@ this.drawPath(path.toRenderable(lineFill));

@@ -6,8 +6,10 @@ import Editor from '../Editor';

import Color4 from '../Color4';
import SelectionTool from '../tools/SelectionTool';
import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
import { ActionButtonIcon } from './types';
import { makeRedoIcon, makeUndoIcon } from './icons';
import PanZoom from '../tools/PanZoom';
import SelectionTool from '../tools/SelectionTool/SelectionTool';
import PanZoomTool from '../tools/PanZoom';
import TextTool from '../tools/TextTool';
import EraserTool from '../tools/Eraser';
import PenTool from '../tools/Pen';
import PenToolWidget from './widgets/PenToolWidget';

@@ -19,3 +21,2 @@ import EraserWidget from './widgets/EraserToolWidget';

import BaseWidget from './widgets/BaseWidget';
import { EraserTool, PenTool } from '../tools/lib';

@@ -205,3 +206,3 @@ export const toolbarCSSPrefix = 'toolbar-';

const panZoomTool = toolController.getMatchingTools(PanZoom)[0];
const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
if (panZoomTool) {

@@ -208,0 +209,0 @@ this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));

import Editor from '../../Editor';
import SelectionTool from '../../tools/SelectionTool';
import SelectionTool from '../../tools/SelectionTool/SelectionTool';
import { EditorEventType } from '../../types';

@@ -4,0 +4,0 @@ import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons';

@@ -16,5 +16,5 @@

export { default as TextTool } from './TextTool';
export { default as SelectionTool } from './SelectionTool';
export { default as SelectionTool } from './SelectionTool/SelectionTool';
export { default as EraserTool } from './Eraser';
export { default as PasteHandler } from './PasteHandler';

@@ -14,3 +14,3 @@ /**

import EditorImage from '../EditorImage';
import SelectionTool from './SelectionTool';
import SelectionTool from './SelectionTool/SelectionTool';
import TextTool from './TextTool';

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

import ImageComponent from '../components/ImageComponent';
import Viewport from '../Viewport';

@@ -72,2 +73,4 @@ // { @inheritDoc PasteHandler! }

scaleRatio = Viewport.roundScaleRatio(scaleRatio);
const transfm = Mat33.translation(

@@ -74,0 +77,0 @@ visibleRect.center.minus(bbox.center)

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

if (newThickness !== undefined) {
newThickness = Math.min(Math.max(1, newThickness), 128);
newThickness = Math.min(Math.max(1, newThickness), 256);
this.setThickness(newThickness);

@@ -177,0 +177,0 @@ return true;

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

import Eraser from './Eraser';
import SelectionTool from './SelectionTool';
import SelectionTool from './SelectionTool/SelectionTool';
import Color4 from '../Color4';

@@ -11,0 +11,0 @@ import { ToolLocalization } from './localization';

@@ -13,3 +13,3 @@ // Types related to the image editor

import Command from './commands/Command';
import { BaseWidget } from './lib';
import BaseWidget from './toolbar/widgets/BaseWidget';

@@ -16,0 +16,0 @@

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

// Returns the angle of the canvas in radians
// Returns the angle of the canvas in radians.
// This is the angle by which the canvas is rotated relative to the screen.
public getRotationAngle(): number {

@@ -179,3 +180,2 @@ return this.transform.transformVec3(Vec3.unitX).angle();

// Round a point with a tolerance of ±1 screen unit.

@@ -186,2 +186,17 @@ public roundPoint(point: Point2): Point2 {

// `roundAmount`: An integer >= 0, larger numbers cause less rounding. Smaller numbers cause more
// (as such `roundAmount = 0` does the most rounding).
public static roundScaleRatio(scaleRatio: number, roundAmount: number = 1): number {
if (Math.abs(scaleRatio) <= 1e-12) {
return 0;
}
// Represent as k 10ⁿ for some n, k ∈ ℤ.
const decimalComponent = 10 ** Math.floor(Math.log10(Math.abs(scaleRatio)));
const roundAnountFactor = 2 ** roundAmount;
scaleRatio = Math.round(scaleRatio / decimalComponent * roundAnountFactor) / roundAnountFactor * decimalComponent;
return scaleRatio;
}
// Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.

@@ -188,0 +203,0 @@ public computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn: boolean = true, allowZoomOut: boolean = true): Mat33 {

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

Sorry, the diff of this file is not supported yet

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