Socket
Socket
Sign inDemoInstall

js-draw

Package Overview
Dependencies
Maintainers
1
Versions
117
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

js-draw - npm Package Compare versions

Comparing version 0.1.12 to 0.2.0

.firebaserc

1

.eslintrc.js

@@ -57,3 +57,4 @@ module.exports = {

'dist/',
'docs/typedoc/'
],
};

@@ -0,1 +1,7 @@

# 0.2.0
* Export `Mat33`, `Vec3`, `Vec2`, and `Color4`.
* [Documentation](https://personalizedrefrigerator.github.io/js-draw/typedoc/index.html)
* Bug fixes:
* After using up all blocks in the rendering cache, a single block was repeatedly re-allocated, leading to slow performance.
# 0.1.12

@@ -2,0 +8,0 @@ * Add icons to the selection menu.

3

dist/src/bundle/bundled.d.ts
import '../styles';
import Editor from '../Editor';
import getLocalizationTable from '../localizations/getLocalizationTable';
export * from '../lib';
export default Editor;
export { Editor, getLocalizationTable };
// Main entrypoint for Webpack when building a bundle for release.
import '../styles';
import Editor from '../Editor';
import getLocalizationTable from '../localizations/getLocalizationTable';
export * from '../lib';
export default Editor;
export { Editor, getLocalizationTable };
export default class Color4 {
/** Red component. Should be in the range [0, 1]. */
readonly r: number;
/** Green component. `g` ∈ [0, 1] */
readonly g: number;
/** Blue component. `b` ∈ [0, 1] */
readonly b: number;
/** Alpha/transparent component. `a` ∈ [0, 1] */
readonly a: number;
private constructor();
/**
* Create a color from red, green, blue components. The color is fully opaque (`a = 1.0`).
*
* Each component should be in the range [0, 1].
*/
static ofRGB(red: number, green: number, blue: number): Color4;
static ofRGBA(red: number, green: number, blue: number, alpha: number): Color4;
static fromHex(hexString: string): Color4;
/** Like fromHex, but can handle additional colors if an `HTMLCanvasElement` is available. */
static fromString(text: string): Color4;
/** @returns true if `this` and `other` are approximately equal. */
eq(other: Color4 | null | undefined): boolean;
private hexString;
/**
* @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
*
* @example
* ```
* Color4.red.toHexString(); // -> #ff0000ff
* ```
*/
toHexString(): string;

@@ -14,0 +33,0 @@ static transparent: Color4;

export default class Color4 {
constructor(r, g, b, a) {
constructor(
/** Red component. Should be in the range [0, 1]. */
r,
/** Green component. `g` ∈ [0, 1] */
g,
/** Blue component. `b` ∈ [0, 1] */
b,
/** Alpha/transparent component. `a` ∈ [0, 1] */
a) {
this.r = r;

@@ -9,3 +17,7 @@ this.g = g;

}
// Each component should be in the range [0, 1]
/**
* Create a color from red, green, blue components. The color is fully opaque (`a = 1.0`).
*
* Each component should be in the range [0, 1].
*/
static ofRGB(red, green, blue) {

@@ -50,3 +62,3 @@ return Color4.ofRGBA(red, green, blue, 1.0);

}
// Like fromHex, but can handle additional colors if an HTML5Canvas is available.
/** Like fromHex, but can handle additional colors if an `HTMLCanvasElement` is available. */
static fromString(text) {

@@ -72,2 +84,3 @@ if (text.startsWith('#')) {

}
/** @returns true if `this` and `other` are approximately equal. */
eq(other) {

@@ -79,2 +92,10 @@ if (other == null) {

}
/**
* @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
*
* @example
* ```
* Color4.red.toHexString(); // -> #ff0000ff
* ```
*/
toHexString() {

@@ -81,0 +102,0 @@ if (this.hexString) {

@@ -13,3 +13,3 @@ import AbstractComponent from '../components/AbstractComponent';

description(_editor: Editor, localizationTable: EditorLocalization): string;
protected serializeToString(): string;
protected serializeToJSON(): string[];
}

@@ -24,9 +24,8 @@ import describeComponentList from '../components/util/describeComponentList';

}
serializeToString() {
return JSON.stringify(this.toDuplicate.map(elem => elem.getId()));
serializeToJSON() {
return this.toDuplicate.map(elem => elem.getId());
}
}
(() => {
SerializableCommand.register('duplicate', (data, editor) => {
const json = JSON.parse(data);
SerializableCommand.register('duplicate', (json, editor) => {
const elems = json.map((id) => editor.image.lookupElement(id));

@@ -33,0 +32,0 @@ return new Duplicate(elems);

@@ -13,3 +13,3 @@ import AbstractComponent from '../components/AbstractComponent';

description(_editor: Editor, localizationTable: EditorLocalization): string;
protected serializeToString(): string;
protected serializeToJSON(): string[];
}

@@ -45,13 +45,14 @@ import describeComponentList from '../components/util/describeComponentList';

}
serializeToString() {
serializeToJSON() {
const elemIds = this.toRemove.map(elem => elem.getId());
return JSON.stringify(elemIds);
return elemIds;
}
}
(() => {
SerializableCommand.register('erase', (data, editor) => {
const json = JSON.parse(data);
const elems = json.map((elemId) => editor.image.lookupElement(elemId));
SerializableCommand.register('erase', (json, editor) => {
const elems = json
.map((elemId) => editor.image.lookupElement(elemId))
.filter((elem) => elem !== null);
return new Erase(elems);
});
})();

@@ -19,4 +19,5 @@ import Rect2 from '../math/Rect2';

duplicateAction: (elemDescription: string, count: number) => string;
inverseOf: (actionDescription: string) => string;
selectedElements: (count: number) => string;
}
export declare const defaultCommandLocalization: CommandLocalization;

@@ -8,2 +8,3 @@ export const defaultCommandLocalization = {

duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
inverseOf: (actionDescription) => `Inverse of ${actionDescription}`,
elements: 'Elements',

@@ -10,0 +11,0 @@ erasedNoElements: 'Erased nothing',

import Editor from '../Editor';
import Command from './Command';
declare type DeserializationCallback = (data: string, editor: Editor) => SerializableCommand;
export declare type DeserializationCallback = (data: Record<string, any> | any[], editor: Editor) => SerializableCommand;
export default abstract class SerializableCommand extends Command {
private commandTypeId;
constructor(commandTypeId: string);
protected abstract serializeToString(): string;
protected abstract serializeToJSON(): string | Record<string, any> | any[];
private static deserializationCallbacks;
serialize(): string;
static deserialize(data: string, editor: Editor): SerializableCommand;
serialize(): Record<string | symbol, any>;
static deserialize(data: string | Record<string, any>, editor: Editor): SerializableCommand;
static register(commandTypeId: string, deserialize: DeserializationCallback): void;
}
export {};

@@ -10,10 +10,16 @@ import Command from './Command';

}
// Convert this command to an object that can be passed to `JSON.stringify`.
//
// Do not rely on the stability of the optupt of this function — it can change
// form without a major version increase.
serialize() {
return JSON.stringify({
data: this.serializeToString(),
return {
data: this.serializeToJSON(),
commandType: this.commandTypeId,
});
};
}
// Convert a `string` containing JSON data (or the output of `JSON.parse`) into a
// `Command`.
static deserialize(data, editor) {
const json = JSON.parse(data);
const json = typeof data === 'string' ? JSON.parse(data) : data;
const commandType = json.commandType;

@@ -25,2 +31,4 @@ if (!(commandType in SerializableCommand.deserializationCallbacks)) {

}
// Register a deserialization callback. This must be called at least once for every subclass of
// `SerializableCommand`.
static register(commandTypeId, deserialize) {

@@ -27,0 +35,0 @@ SerializableCommand.deserializationCallbacks[commandTypeId] = deserialize;

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

import Command from '../commands/Command';
import SerializableCommand from '../commands/SerializableCommand';
import LineSegment2 from '../math/LineSegment2';

@@ -7,5 +7,5 @@ import Mat33 from '../math/Mat33';

import { ImageComponentLocalization } from './localization';
declare type LoadSaveData = (string[] | Record<symbol, string | number>);
export declare type LoadSaveData = (string[] | Record<symbol, string | number>);
export declare type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
declare type DeserializeCallback = (data: string) => AbstractComponent;
export declare type DeserializeCallback = (data: string) => AbstractComponent;
export default abstract class AbstractComponent {

@@ -29,5 +29,5 @@ private readonly componentKind;

abstract intersects(lineSegment: LineSegment2): boolean;
protected abstract serializeToString(): string | null;
protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
protected abstract applyTransformation(affineTransfm: Mat33): void;
transformBy(affineTransfm: Mat33): Command;
transformBy(affineTransfm: Mat33): SerializableCommand;
private static TransformElementCommand;

@@ -37,6 +37,11 @@ abstract description(localizationTable: ImageComponentLocalization): string;

clone(): AbstractComponent;
serialize(): string;
serialize(): {
name: string;
zIndex: number;
id: string;
loadSaveData: LoadSaveDataTable;
data: string | number | any[] | Record<string, any>;
};
private static isNotDeserializable;
static deserialize(data: string): AbstractComponent;
static deserialize(json: string | any): AbstractComponent;
}
export {};

@@ -20,2 +20,4 @@ var _a;

}
// Returns a unique ID for this element.
// @see { @link EditorImage!default.lookupElement }
getId() {

@@ -59,8 +61,13 @@ return this.id;

}
// Convert the component to an object that can be passed to
// `JSON.stringify`.
//
// Do not rely on the output of this function to take a particular form —
// this function's output can change form without a major version increase.
serialize() {
const data = this.serializeToString();
const data = this.serializeToJSON();
if (data === null) {
throw new Error(`${this} cannot be serialized.`);
}
return JSON.stringify({
return {
name: this.componentKind,

@@ -71,8 +78,10 @@ zIndex: this.zIndex,

data,
});
};
}
// Returns true if [data] is not deserializable. May return false even if [data]
// Returns true if `data` is not deserializable. May return false even if [data]
// is not deserializable.
static isNotDeserializable(data) {
const json = JSON.parse(data);
static isNotDeserializable(json) {
if (typeof json === 'string') {
json = JSON.parse(json);
}
if (typeof json !== 'object') {

@@ -89,7 +98,10 @@ return true;

}
static deserialize(data) {
if (AbstractComponent.isNotDeserializable(data)) {
throw new Error(`Element with data ${data} cannot be deserialized.`);
// Convert a string or an object produced by `JSON.parse` into an `AbstractComponent`.
static deserialize(json) {
if (typeof json === 'string') {
json = JSON.parse(json);
}
const json = JSON.parse(data);
if (AbstractComponent.isNotDeserializable(json)) {
throw new Error(`Element with data ${json} cannot be deserialized.`);
}
const instance = this.deserializationCallbacks[json.name](json.data);

@@ -142,12 +154,11 @@ instance.zIndex = json.zIndex;

}
serializeToString() {
return JSON.stringify({
serializeToJSON() {
return {
id: this.component.getId(),
transfm: this.affineTransfm.toArray(),
});
};
}
},
(() => {
SerializableCommand.register('transform-element', (data, editor) => {
const json = JSON.parse(data);
SerializableCommand.register('transform-element', (json, editor) => {
const elem = editor.image.lookupElement(json.id);

@@ -154,0 +165,0 @@ if (!elem) {

@@ -19,4 +19,14 @@ import LineSegment2 from '../math/LineSegment2';

protected createClone(): AbstractComponent;
protected serializeToString(): string | null;
static deserializeFromString(data: string): Stroke;
protected serializeToJSON(): {
style: {
fill: string;
stroke: {
color: string;
width: number;
} | undefined;
};
path: string;
}[];
/** @internal */
static deserializeFromJSON(json: any): Stroke;
}

@@ -90,4 +90,4 @@ import Path from '../math/Path';

}
serializeToString() {
return JSON.stringify(this.parts.map(part => {
serializeToJSON() {
return this.parts.map(part => {
return {

@@ -97,8 +97,11 @@ style: styleToJSON(part.style),

};
}));
});
}
static deserializeFromString(data) {
const json = JSON.parse(data);
/** @internal */
static deserializeFromJSON(json) {
if (typeof json === 'string') {
json = JSON.parse(json);
}
if (typeof json !== 'object' || typeof json.length !== 'number') {
throw new Error(`${data} is missing required field, parts, or parts is of the wrong type.`);
throw new Error(`${json} is missing required field, parts, or parts is of the wrong type.`);
}

@@ -112,2 +115,2 @@ const pathSpec = json.map((part) => {

}
AbstractComponent.registerComponent('stroke', Stroke.deserializeFromString);
AbstractComponent.registerComponent('stroke', Stroke.deserializeFromJSON);

@@ -17,5 +17,5 @@ import LineSegment2 from '../math/LineSegment2';

description(localization: ImageComponentLocalization): string;
protected serializeToString(): string | null;
protected serializeToJSON(): string | null;
static deserializeFromString(data: string): AbstractComponent;
}
export {};

@@ -0,1 +1,7 @@

//
// Used by `SVGLoader`s to store unrecognised global attributes
// (e.g. unrecognised XML namespace declarations).
// @internal
// @packageDocumentation
//
import Rect2 from '../math/Rect2';

@@ -32,3 +38,3 @@ import SVGRenderer from '../rendering/renderers/SVGRenderer';

}
serializeToString() {
serializeToJSON() {
return JSON.stringify(this.attrs);

@@ -35,0 +41,0 @@ }

@@ -34,5 +34,5 @@ import LineSegment2 from '../math/LineSegment2';

description(localizationTable: ImageComponentLocalization): string;
protected serializeToString(): string;
static deserializeFromString(data: string, getTextDimens?: GetTextDimensCallback): Text;
protected serializeToJSON(): Record<string, any>;
static deserializeFromString(json: any, getTextDimens?: GetTextDimensCallback): Text;
}
export {};

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

// If not given, an HtmlCanvasElement is used to determine text boundaries.
// @internal
getTextDimens = Text.getTextDimens) {

@@ -121,3 +122,3 @@ super(componentTypeId);

}
serializeToString() {
serializeToJSON() {
const serializableStyle = Object.assign(Object.assign({}, this.style), { renderingStyle: styleToJSON(this.style.renderingStyle) });

@@ -132,14 +133,13 @@ const textObjects = this.textObjects.map(text => {

return {
json: text.serializeToString(),
json: text.serializeToJSON(),
};
}
});
return JSON.stringify({
return {
textObjects,
transform: this.transform.toArray(),
style: serializableStyle,
});
};
}
static deserializeFromString(data, getTextDimens = Text.getTextDimens) {
const json = JSON.parse(data);
static deserializeFromString(json, getTextDimens = Text.getTextDimens) {
const style = {

@@ -146,0 +146,0 @@ renderingStyle: styleFromJSON(json.style.renderingStyle),

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

description(localization: ImageComponentLocalization): string;
protected serializeToString(): string | null;
protected serializeToJSON(): string | null;
}

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

//
// Stores objects loaded from an SVG that aren't recognised by the editor.
// @internal
// @packageDocumentation
//
import Rect2 from '../math/Rect2';

@@ -29,3 +34,3 @@ import SVGRenderer from '../rendering/renderers/SVGRenderer';

}
serializeToString() {
serializeToJSON() {
return JSON.stringify({

@@ -32,0 +37,0 @@ html: this.svgObject.outerHTML,

@@ -0,1 +1,18 @@

/**
* The main entrypoint for the full editor.
*
* @example
* To create an editor with a toolbar,
* ```
* const editor = new Editor(document.body);
*
* const toolbar = editor.addToolbar();
* toolbar.addActionButton('Save', () => {
* const saveData = editor.toSVG().outerHTML;
* // Do something with saveData...
* });
* ```
*
* @packageDocumentation
*/
import EditorImage from './EditorImage';

@@ -15,5 +32,13 @@ import ToolController from './tools/ToolController';

export interface EditorSettings {
/** Defaults to `RenderingMode.CanvasRenderer` */
renderingMode: RenderingMode;
/** Uses a default English localization if a translation is not given. */
localization: Partial<EditorLocalization>;
/**
* `true` if touchpad/mousewheel scrolling should scroll the editor instead of the document.
* This does not include pinch-zoom events.
* Defaults to true.
*/
wheelEventsEnabled: boolean | 'only-if-focused';
/** Minimum zoom fraction (e.g. 0.5 → 50% zoom). */
minZoom: number;

@@ -25,9 +50,46 @@ maxZoom: number;

private renderingRegion;
display: Display;
/**
* Handles undo/redo.
*
* @example
* ```
* const editor = new Editor(document.body);
*
* // Do something undoable.
* // ...
*
* // Undo the last action
* editor.history.undo();
* ```
*/
history: UndoRedoHistory;
display: Display;
/**
* Data structure for adding/removing/querying objects in the image.
*
* @example
* ```
* const editor = new Editor(document.body);
*
* // Create a path.
* const stroke = new Stroke([
* Path.fromString('M0,0 L30,30 z').toRenderable({ fill: Color4.black }),
* ]);
* const addElementCommand = editor.image.addElement(stroke);
*
* // Add the stroke to the editor
* editor.dispatch(addElementCommand);
* ```
*/
image: EditorImage;
/** Viewport for the exported/imported image. */
private importExportViewport;
/** @internal */
localization: EditorLocalization;
viewport: Viewport;
toolController: ToolController;
/**
* Global event dispatcher/subscriber.
* @see {@link types.EditorEventType}
*/
notifier: EditorNotifier;

@@ -38,4 +100,36 @@ private loadingWarning;

private settings;
/**
* @example
* ```
* const container = document.body;
*
* // Create an editor
* const editor = new Editor(container, {
* // 2e-10 and 1e12 are the default values for minimum/maximum zoom.
* minZoom: 2e-10,
* maxZoom: 1e12,
* });
*
* // Add the default toolbar
* const toolbar = editor.addToolbar();
* toolbar.addActionButton({
* label: 'Save'
* icon: createSaveIcon(),
* }, () => {
* const saveData = editor.toSVG().outerHTML;
* // Do something with saveData
* });
* ```
*/
constructor(parent: HTMLElement, settings?: Partial<EditorSettings>);
/**
* @returns a reference to the editor's container.
*
* @example
* ```
* editor.getRootElement().style.height = '500px';
* ```
*/
getRootElement(): HTMLElement;
/** @param fractionLoaded - should be a number from 0 to 1, where 1 represents completely loaded. */
showLoadingWarning(fractionLoaded: number): void;

@@ -45,8 +139,35 @@ hideLoadingWarning(): void;

announceForAccessibility(message: string): void;
/**
* Creates a toolbar. If `defaultLayout` is true, default buttons are used.
* @returns a reference to the toolbar.
*/
addToolbar(defaultLayout?: boolean): HTMLToolbar;
private registerListeners;
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
handleKeyEventsFrom(elem: HTMLElement): void;
/** `apply` a command. `command` will be announced for accessibility. */
dispatch(command: Command, addToHistory?: boolean): void;
/**
* Dispatches a command without announcing it. By default, does not add to history.
* Use this to show finalized commands that don't need to have `announceForAccessibility`
* 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
* ```
* const addToHistory = false;
* editor.dispatchNoAnnounce(editor.viewport.zoomTo(someRectangle), addToHistory);
* ```
*/
dispatchNoAnnounce(command: Command, addToHistory?: boolean): void;
private asyncApplyOrUnapplyCommands;
/**
* Apply a large transformation in chunks.
* If `apply` is `false`, the commands are unapplied.
* Triggers a re-render after each `updateChunkSize`-sized group of commands
* has been applied.
*/
asyncApplyOrUnapplyCommands(commands: Command[], apply: boolean, updateChunkSize: number): Promise<void>;
asyncApplyCommands(commands: Command[], chunkSize: number): Promise<void>;

@@ -71,4 +192,10 @@ asyncUnapplyCommands(commands: Command[], chunkSize: number): Promise<void>;

setImportExportRect(imageRect: Rect2): Command;
/**
* Alias for loadFrom(SVGLoader.fromString).
*
* This is particularly useful when accessing a bundled version of the editor,
* where `SVGLoader.fromString` is unavailable.
*/
loadFromSVG(svgData: string): Promise<void>;
}
export default Editor;

@@ -0,1 +1,18 @@

/**
* The main entrypoint for the full editor.
*
* @example
* To create an editor with a toolbar,
* ```
* const editor = new Editor(document.body);
*
* const toolbar = editor.addToolbar();
* toolbar.addActionButton('Save', () => {
* const saveData = editor.toSVG().outerHTML;
* // Do something with saveData...
* });
* ```
*
* @packageDocumentation
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

@@ -27,3 +44,27 @@ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }

import getLocalizationTable from './localizations/getLocalizationTable';
// { @inheritDoc Editor! }
export class Editor {
/**
* @example
* ```
* const container = document.body;
*
* // Create an editor
* const editor = new Editor(container, {
* // 2e-10 and 1e12 are the default values for minimum/maximum zoom.
* minZoom: 2e-10,
* maxZoom: 1e12,
* });
*
* // Add the default toolbar
* const toolbar = editor.addToolbar();
* toolbar.addActionButton({
* label: 'Save'
* icon: createSaveIcon(),
* }, () => {
* const saveData = editor.toSVG().outerHTML;
* // Do something with saveData
* });
* ```
*/
constructor(parent, settings = {}) {

@@ -100,9 +141,14 @@ var _a, _b, _c, _d;

}
// Returns a reference to this' container.
// Example usage:
// editor.getRootElement().style.height = '500px';
/**
* @returns a reference to the editor's container.
*
* @example
* ```
* editor.getRootElement().style.height = '500px';
* ```
*/
getRootElement() {
return this.container;
}
// [fractionLoaded] should be a number from 0 to 1, where 1 represents completely loaded.
/** @param fractionLoaded - should be a number from 0 to 1, where 1 represents completely loaded. */
showLoadingWarning(fractionLoaded) {

@@ -117,2 +163,4 @@ const loadingPercent = Math.round(fractionLoaded * 100);

}
// Announce `message` for screen readers. If `message` is the same as the previous
// message, it is re-announced.
announceForAccessibility(message) {

@@ -126,2 +174,6 @@ // Force re-announcing an announcement if announced again.

}
/**
* Creates a toolbar. If `defaultLayout` is true, default buttons are used.
* @returns a reference to the toolbar.
*/
addToolbar(defaultLayout = true) {

@@ -261,4 +313,3 @@ const toolbar = new HTMLToolbar(this, this.container, this.localization);

}
// Adds event listners for keypresses to [elem] and forwards those events to the
// editor.
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
handleKeyEventsFrom(elem) {

@@ -291,3 +342,3 @@ elem.addEventListener('keydown', evt => {

}
// Adds to history by default
/** `apply` a command. `command` will be announced for accessibility. */
dispatch(command, addToHistory = true) {

@@ -303,3 +354,17 @@ if (addToHistory) {

}
// Dispatches a command without announcing it. By default, does not add to history.
/**
* Dispatches a command without announcing it. By default, does not add to history.
* Use this to show finalized commands that don't need to have `announceForAccessibility`
* 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
* ```
* const addToHistory = false;
* editor.dispatchNoAnnounce(editor.viewport.zoomTo(someRectangle), addToHistory);
* ```
*/
dispatchNoAnnounce(command, addToHistory = false) {

@@ -313,6 +378,8 @@ if (addToHistory) {

}
// Apply a large transformation in chunks.
// If [apply] is false, the commands are unapplied.
// Triggers a re-render after each [updateChunkSize]-sized group of commands
// has been applied.
/**
* Apply a large transformation in chunks.
* If `apply` is `false`, the commands are unapplied.
* Triggers a re-render after each `updateChunkSize`-sized group of commands
* has been applied.
*/
asyncApplyOrUnapplyCommands(commands, apply, updateChunkSize) {

@@ -344,8 +411,12 @@ return __awaiter(this, void 0, void 0, function* () {

}
// @see {@link #asyncApplyOrUnapplyCommands }
asyncApplyCommands(commands, chunkSize) {
return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
}
// @see {@link #asyncApplyOrUnapplyCommands }
asyncUnapplyCommands(commands, chunkSize) {
return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
}
// Schedule a re-render for some time in the near future. Does not schedule an additional
// re-render if a re-render is already queued.
queueRerender() {

@@ -384,6 +455,8 @@ if (!this.rerenderQueued) {

}
// Focuses the region used for text input
// Focuses the region used for text input/key commands.
focus() {
this.renderingRegion.focus();
}
// Creates an element that will be positioned on top of the dry/wet ink
// renderers.
createHTMLOverlay(overlay) {

@@ -403,3 +476,3 @@ overlay.classList.add('overlay');

// Dispatch a pen event to the currently selected tool.
// Intented for unit tests.
// Intended primarially for unit tests.
sendPenEvent(eventType, point, allPointers) {

@@ -466,3 +539,3 @@ const mainPointer = Pointer.ofCanvasPoint(point, eventType !== InputEvtType.PointerUpEvt, this.viewport);

}
// Resize the output SVG
// Resize the output SVG to match `imageRect`.
setImportExportRect(imageRect) {

@@ -489,4 +562,8 @@ const origSize = this.importExportViewport.visibleRect.size;

}
// Alias for loadFrom(SVGLoader.fromString).
// This is particularly useful when accessing a bundled version of the editor.
/**
* Alias for loadFrom(SVGLoader.fromString).
*
* This is particularly useful when accessing a bundled version of the editor,
* where `SVGLoader.fromString` is unavailable.
*/
loadFromSVG(svgData) {

@@ -493,0 +570,0 @@ return __awaiter(this, void 0, void 0, function* () {

import AbstractRenderer from './rendering/renderers/AbstractRenderer';
import Command from './commands/Command';
import Viewport from './Viewport';

@@ -7,2 +6,3 @@ import AbstractComponent from './components/AbstractComponent';

import RenderingCache from './rendering/caching/RenderingCache';
import SerializableCommand from './commands/SerializableCommand';
export declare const sortLeavesByZIndex: (leaves: Array<ImageNode>) => void;

@@ -14,13 +14,18 @@ export default class EditorImage {

findParent(elem: AbstractComponent): ImageNode | null;
/** @internal */
renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport): void;
/** @internal */
render(renderer: AbstractRenderer, viewport: Viewport): void;
/** Renders all nodes, even ones not within the viewport. @internal */
renderAll(renderer: AbstractRenderer): void;
getElementsIntersectingRegion(region: Rect2): AbstractComponent[];
/** @internal */
onDestroyElement(elem: AbstractComponent): void;
lookupElement(id: string): AbstractComponent | null;
private addElementDirectly;
static addElement(elem: AbstractComponent, applyByFlattening?: boolean): Command;
static addElement(elem: AbstractComponent, applyByFlattening?: boolean): SerializableCommand;
private static AddElementCommand;
}
declare type TooSmallToRenderCheck = (rect: Rect2) => boolean;
/** Part of the Editor's image. @internal */
export declare class ImageNode {

@@ -27,0 +32,0 @@ private parent;

@@ -5,2 +5,3 @@ var _a;

import SerializableCommand from './commands/SerializableCommand';
// @internal
export const sortLeavesByZIndex = (leaves) => {

@@ -11,2 +12,3 @@ leaves.sort((a, b) => a.getContent().getZIndex() - b.getContent().getZIndex());

export default class EditorImage {
// @internal
constructor() {

@@ -26,9 +28,11 @@ this.root = new ImageNode();

}
/** @internal */
renderWithCache(screenRenderer, cache, viewport) {
cache.render(screenRenderer, this.root, viewport);
}
/** @internal */
render(renderer, viewport) {
this.root.render(renderer, viewport.visibleRect);
}
// Renders all nodes, even ones not within the viewport
/** Renders all nodes, even ones not within the viewport. @internal */
renderAll(renderer) {

@@ -46,2 +50,3 @@ const leaves = this.root.getLeaves();

}
/** @internal */
onDestroyElement(elem) {

@@ -90,15 +95,16 @@ delete this.componentsById[elem.getId()];

}
description(editor, localization) {
description(_editor, localization) {
return localization.addElementAction(this.element.description(localization));
}
serializeToString() {
return JSON.stringify({
serializeToJSON() {
return {
elemData: this.element.serialize(),
});
};
}
},
(() => {
SerializableCommand.register('add-element', (data, _editor) => {
const json = JSON.parse(data);
const elem = AbstractComponent.deserialize(json.elemData);
SerializableCommand.register('add-element', (json, editor) => {
const id = json.elemData.id;
const foundElem = editor.image.lookupElement(id);
const elem = foundElem !== null && foundElem !== void 0 ? foundElem : AbstractComponent.deserialize(json.elemData);
return new EditorImage.AddElementCommand(elem);

@@ -108,3 +114,3 @@ });

_a);
// TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
/** Part of the Editor's image. @internal */
export class ImageNode {

@@ -145,13 +151,19 @@ constructor(parent = null) {

const result = [];
// Don't render if too small
if (isTooSmall === null || isTooSmall === void 0 ? void 0 : isTooSmall(this.bbox)) {
return [];
let current;
const workList = [];
workList.push(this);
const toNext = () => {
current = undefined;
const next = workList.pop();
if (next && !(isTooSmall === null || isTooSmall === void 0 ? void 0 : isTooSmall(next.bbox))) {
current = next;
if (current.content !== null && current.getBBox().intersection(region)) {
result.push(current);
}
workList.push(...current.getChildrenIntersectingRegion(region));
}
};
while (workList.length > 0) {
toNext();
}
if (this.content !== null && this.getBBox().intersects(region)) {
result.push(this);
}
const children = this.getChildrenIntersectingRegion(region);
for (const child of children) {
result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
}
return result;

@@ -190,9 +202,13 @@ }

if (leafBBox.containsRect(this.getBBox())) {
// Create a node for this' children and for the new content..
const nodeForNewLeaf = new ImageNode(this);
const nodeForChildren = new ImageNode(this);
nodeForChildren.children = this.children;
this.children = [nodeForNewLeaf, nodeForChildren];
nodeForChildren.recomputeBBox(true);
nodeForChildren.updateParents();
if (this.children.length < this.targetChildCount) {
this.children.push(nodeForNewLeaf);
}
else {
const nodeForChildren = new ImageNode(this);
nodeForChildren.children = this.children;
this.children = [nodeForNewLeaf, nodeForChildren];
nodeForChildren.recomputeBBox(true);
nodeForChildren.updateParents();
}
return nodeForNewLeaf.addLeaf(leaf);

@@ -199,0 +215,0 @@ }

@@ -0,1 +1,18 @@

/**
* Handles notifying listeners of events.
*
* `EventKeyType` is used to distinguish events (e.g. a `ClickEvent` vs a `TouchEvent`)
* while `EventMessageType` is the type of the data sent with an event (can be `void`).
*
* @example
* ```
* const dispatcher = new EventDispatcher<'event1'|'event2'|'event3', void>();
* dispatcher.on('event1', () => {
* console.log('Event 1 triggered.');
* });
* dispatcher.dispatch('event1');
* ```
*
* @packageDocumentation
*/
declare type CallbackHandler<EventType> = (data: EventType) => void;

@@ -9,4 +26,5 @@ export default class EventDispatcher<EventKeyType extends string | symbol | number, EventMessageType> {

};
/** Removes an event listener. This is equivalent to calling `.remove()` on the object returned by `.on`. */
off(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): void;
}
export {};

@@ -1,4 +0,19 @@

// Code shared with Joplin
// EventKeyType is used to distinguish events (e.g. a 'ClickEvent' vs a 'TouchEvent')
// while EventMessageType is the type of the data sent with an event (can be `void`)
/**
* Handles notifying listeners of events.
*
* `EventKeyType` is used to distinguish events (e.g. a `ClickEvent` vs a `TouchEvent`)
* while `EventMessageType` is the type of the data sent with an event (can be `void`).
*
* @example
* ```
* const dispatcher = new EventDispatcher<'event1'|'event2'|'event3', void>();
* dispatcher.on('event1', () => {
* console.log('Event 1 triggered.');
* });
* dispatcher.dispatch('event1');
* ```
*
* @packageDocumentation
*/
// { @inheritDoc EventDispatcher! }
export default class EventDispatcher {

@@ -29,3 +44,3 @@ constructor() {

}
// Equivalent to calling .remove() on the object returned by .on
/** Removes an event listener. This is equivalent to calling `.remove()` on the object returned by `.on`. */
off(eventName, callback) {

@@ -32,0 +47,0 @@ const listeners = this.listeners[eventName];

import { Point2, Vec2 } from './Vec2';
import Vec3 from './Vec3';
/**
* Represents a three dimensional linear transformation or
* a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
* **and** translates while a linear transformation just scales/rotates/shears).
*/
export default class Mat33 {

@@ -14,5 +19,28 @@ readonly a1: number;

private readonly rows;
/**
* Creates a matrix from inputs in the form,
* ```
* ⎡ a1 a2 a3 ⎤
* ⎢ b1 b2 b3 ⎥
* ⎣ c1 c2 c3 ⎦
* ```
*/
constructor(a1: number, a2: number, a3: number, b1: number, b2: number, b3: number, c1: number, c2: number, c3: number);
/**
* Creates a matrix from the given rows:
* ```
* ⎡ r1.x r1.y r1.z ⎤
* ⎢ r2.x r2.y r2.z ⎥
* ⎣ r3.x r3.y r3.z ⎦
* ```
*/
static ofRows(r1: Vec3, r2: Vec3, r3: Vec3): Mat33;
static identity: Mat33;
/**
* Either returns the inverse of this, or, if this matrix is singular/uninvertable,
* returns Mat33.identity.
*
* This may cache the computed inverse and return the cached version instead of recomputing
* it.
*/
inverse(): Mat33;

@@ -24,11 +52,29 @@ invertable(): boolean;

rightMul(other: Mat33): Mat33;
transformVec2(other: Vec3): Vec2;
/**
* Applies this as an affine transformation to the given vector.
* Returns a transformed version of `other`.
*/
transformVec2(other: Vec2): Vec2;
/**
* Applies this as a linear transformation to the given vector (doesn't translate).
* This is the standard way of transforming vectors in ℝ³.
*/
transformVec3(other: Vec3): Vec3;
/** Returns true iff this = other ± fuzz */
eq(other: Mat33, fuzz?: number): boolean;
toString(): string;
/**
* ```
* result[0] = top left element
* result[1] = element at row zero, column 1
* ...
* ```
*/
toArray(): number[];
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
static translation(amount: Vec2): Mat33;
static zRotation(radians: number, center?: Point2): Mat33;
static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
/** Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. */
static fromCSSMatrix(cssString: string): Mat33;
}
import { Vec2 } from './Vec2';
import Vec3 from './Vec3';
// Represents a three dimensional linear transformation or
// a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
// **and** translates while a linear transformation just scales/rotates/shears).
/**
* Represents a three dimensional linear transformation or
* a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
* **and** translates while a linear transformation just scales/rotates/shears).
*/
export default class Mat33 {
// ⎡ a1 a2 a3 ⎤
// ⎢ b1 b2 b3 ⎥
// ⎣ c1 c2 c3 ⎦
/**
* Creates a matrix from inputs in the form,
* ```
* ⎡ a1 a2 a3 ⎤
* ⎢ b1 b2 b3 ⎥
* ⎣ c1 c2 c3 ⎦
* ```
*/
constructor(a1, a2, a3, b1, b2, b3, c1, c2, c3) {

@@ -27,7 +34,20 @@ this.a1 = a1;

}
/**
* Creates a matrix from the given rows:
* ```
* ⎡ r1.x r1.y r1.z ⎤
* ⎢ r2.x r2.y r2.z ⎥
* ⎣ r3.x r3.y r3.z ⎦
* ```
*/
static ofRows(r1, r2, r3) {
return new Mat33(r1.x, r1.y, r1.z, r2.x, r2.y, r2.z, r3.x, r3.y, r3.z);
}
// Either returns the inverse of this, or, if this matrix is singular/uninvertable,
// returns Mat33.identity.
/**
* Either returns the inverse of this, or, if this matrix is singular/uninvertable,
* returns Mat33.identity.
*
* This may cache the computed inverse and return the cached version instead of recomputing
* it.
*/
inverse() {

@@ -113,4 +133,6 @@ var _a;

}
// Applies this as an affine transformation to the given vector.
// Returns a transformed version of [other].
/**
* Applies this as an affine transformation to the given vector.
* Returns a transformed version of `other`.
*/
transformVec2(other) {

@@ -129,8 +151,10 @@ // When transforming a Vec2, we want to use the z transformation

}
// Applies this as a linear transformation to the given vector (doesn't translate).
// This is the standard way of transforming vectors in ℝ³.
/**
* Applies this as a linear transformation to the given vector (doesn't translate).
* This is the standard way of transforming vectors in ℝ³.
*/
transformVec3(other) {
return Vec3.of(this.rows[0].dot(other), this.rows[1].dot(other), this.rows[2].dot(other));
}
// Returns true iff this = other ± fuzz
/** Returns true iff this = other ± fuzz */
eq(other, fuzz = 0) {

@@ -149,7 +173,11 @@ for (let i = 0; i < 3; i++) {

⎣ ${this.c1},\t ${this.c2},\t ${this.c3}\t ⎦
`.trimRight();
`.trimEnd().trimStart();
}
// result[0] = top left element
// result[1] = element at row zero, column 1
// ...
/**
* ```
* result[0] = top left element
* result[1] = element at row zero, column 1
* ...
* ```
*/
toArray() {

@@ -162,3 +190,3 @@ return [

}
// Constructs a 3x3 translation matrix (for translating Vec2s)
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
static translation(amount) {

@@ -194,3 +222,3 @@ // When transforming Vec2s by a 3x3 matrix, we give the input

}
// Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
/** Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. */
static fromCSSMatrix(cssString) {

@@ -197,0 +225,0 @@ if (cssString === '' || cssString === 'none') {

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

}
// [onlyAbsCommands]: True if we should avoid converting absolute coordinates to relative offsets -- such
// conversions can lead to smaller output strings, but also take time.
// @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
// conversions can lead to smaller output strings, but also take time.
static toString(startPoint, parts, onlyAbsCommands = true) {

@@ -290,3 +290,3 @@ const result = [];

// TODO: Support a larger subset of SVG paths.
// TODO: Support s,t shorthands.
// TODO: Support `s`,`t` commands shorthands.
static fromString(pathString) {

@@ -293,0 +293,0 @@ // See the MDN reference:

import LineSegment2 from './LineSegment2';
import Mat33 from './Mat33';
import { Point2, Vec2 } from './Vec2';
interface RectTemplate {
/** An object that can be converted to a Rect2. */
export interface RectTemplate {
x: number;

@@ -51,2 +52,1 @@ y: number;

}
export {};

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

// @packageDocumentation @internal
// Clean up stringified numbers

@@ -2,0 +3,0 @@ const cleanUpNumber = (text) => {

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

/**
* A vector with three components. Can also be used to represent a two-component vector.
*
* A `Vec3` is immutable.
*/
export default class Vec3 {

@@ -6,2 +11,3 @@ readonly x: number;

private constructor();
/** Returns the x, y components of this. */
get xy(): {

@@ -12,8 +18,21 @@ x: number;

static of(x: number, y: number, z: number): Vec3;
/** Returns this' `idx`th component. For example, `Vec3.of(1, 2, 3).at(1) → 2`. */
at(idx: number): number;
/** Alias for this.magnitude. */
length(): number;
magnitude(): number;
magnitudeSquared(): number;
/**
* Return this' angle in the XY plane (treats this as a Vec2).
*
* This is equivalent to `Math.atan2(vec.y, vec.x)`.
*/
angle(): number;
/**
* Returns a unit vector in the same direction as this.
*
* If `this` has zero length, the resultant vector has `NaN` components.
*/
normalized(): Vec3;
/** @returns A copy of `this` multiplied by a scalar. */
times(c: number): Vec3;

@@ -24,8 +43,51 @@ plus(v: Vec3): Vec3;

cross(other: Vec3): Vec3;
/**
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
* 90 degrees counter-clockwise.
*/
orthog(): Vec3;
/** Returns this plus a vector of length `distance` in `direction`. */
extend(distance: number, direction: Vec3): Vec3;
/** Returns a vector `fractionTo` of the way to target from this. */
lerp(target: Vec3, fractionTo: number): Vec3;
/**
* `zip` Maps a component of this and a corresponding component of
* `other` to a component of the output vector.
*
* @example
* ```
* const a = Vec3.of(1, 2, 3);
* const b = Vec3.of(0.5, 2.1, 2.9);
*
* const zipped = a.zip(b, (aComponent, bComponent) => {
* return Math.min(aComponent, bComponent);
* });
*
* console.log(zipped.toString()); // → Vec(0.5, 2, 2.9)
* ```
*/
zip(other: Vec3, zip: (componentInThis: number, componentInOther: number) => number): Vec3;
/**
* Returns a vector with each component acted on by `fn`.
*
* @example
* ```
* console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
* ```
*/
map(fn: (component: number, index: number) => number): Vec3;
asArray(): number[];
/**
* [fuzz] The maximum difference between two components for this and [other]
* to be considered equal.
*
* @example
* ```
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 100); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 0.1); // → false
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3.01); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
* ```
*/
eq(other: Vec3, fuzz: number): boolean;

@@ -32,0 +94,0 @@ toString(): string;

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

// A vector with three components. Can also be used to represent a two-component vector
/**
* A vector with three components. Can also be used to represent a two-component vector.
*
* A `Vec3` is immutable.
*/
export default class Vec3 {

@@ -8,3 +12,3 @@ constructor(x, y, z) {

}
// Returns the x, y components of this
/** Returns the x, y components of this. */
get xy() {

@@ -20,3 +24,3 @@ // Useful for APIs that behave differently if .z is present.

}
// Returns this' [idx]th component
/** Returns this' `idx`th component. For example, `Vec3.of(1, 2, 3).at(1) → 2`. */
at(idx) {

@@ -31,3 +35,3 @@ if (idx === 0)

}
// Alias for this.magnitude
/** Alias for this.magnitude. */
length() {

@@ -42,6 +46,15 @@ return this.magnitude();

}
// Return this' angle in the XY plane (treats this as a Vec2)
/**
* Return this' angle in the XY plane (treats this as a Vec2).
*
* This is equivalent to `Math.atan2(vec.y, vec.x)`.
*/
angle() {
return Math.atan2(this.y, this.x);
}
/**
* Returns a unit vector in the same direction as this.
*
* If `this` has zero length, the resultant vector has `NaN` components.
*/
normalized() {

@@ -51,2 +64,3 @@ const norm = this.magnitude();

}
/** @returns A copy of `this` multiplied by a scalar. */
times(c) {

@@ -70,4 +84,6 @@ return Vec3.of(this.x * c, this.y * c, this.z * c);

}
// Returns a vector orthogonal to this. If this is a Vec2, returns [this] rotated
// 90 degrees counter-clockwise.
/**
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
* 90 degrees counter-clockwise.
*/
orthog() {

@@ -80,16 +96,37 @@ // If parallel to the z-axis

}
// Returns this plus a vector of length [distance] in [direction]
/** Returns this plus a vector of length `distance` in `direction`. */
extend(distance, direction) {
return this.plus(direction.normalized().times(distance));
}
// Returns a vector [fractionTo] of the way to target from this.
/** Returns a vector `fractionTo` of the way to target from this. */
lerp(target, fractionTo) {
return this.times(1 - fractionTo).plus(target.times(fractionTo));
}
// [zip] Maps a component of this and a corresponding component of
// [other] to a component of the output vector.
/**
* `zip` Maps a component of this and a corresponding component of
* `other` to a component of the output vector.
*
* @example
* ```
* const a = Vec3.of(1, 2, 3);
* const b = Vec3.of(0.5, 2.1, 2.9);
*
* const zipped = a.zip(b, (aComponent, bComponent) => {
* return Math.min(aComponent, bComponent);
* });
*
* console.log(zipped.toString()); // → Vec(0.5, 2, 2.9)
* ```
*/
zip(other, zip) {
return Vec3.of(zip(other.x, this.x), zip(other.y, this.y), zip(other.z, this.z));
}
// Returns a vector with each component acted on by [fn]
/**
* Returns a vector with each component acted on by `fn`.
*
* @example
* ```
* console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
* ```
*/
map(fn) {

@@ -101,4 +138,15 @@ return Vec3.of(fn(this.x, 0), fn(this.y, 1), fn(this.z, 2));

}
// [fuzz] The maximum difference between two components for this and [other]
// to be considered equal.
/**
* [fuzz] The maximum difference between two components for this and [other]
* to be considered equal.
*
* @example
* ```
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 100); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 0.1); // → false
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3.01); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
* ```
*/
eq(other, fuzz) {

@@ -105,0 +153,0 @@ for (let i = 0; i < 3; i++) {

@@ -12,3 +12,3 @@ import { Vec2 } from './math/Vec2';

// Provides a snapshot containing information about a pointer. A Pointer
// object is immutable --- it will not be updated when the pointer's information changes.
// object is immutable — it will not be updated when the pointer's information changes.
export default class Pointer {

@@ -24,3 +24,3 @@ constructor(

id,
// Numeric timestamp (milliseconds, as from (new Date).getTime())
// Numeric timestamp (milliseconds, as from `(new Date).getTime()`)
timeStamp) {

@@ -36,2 +36,3 @@ this.screenPos = screenPos;

}
// Creates a Pointer from a DOM event.
static ofEvent(evt, isDown, viewport) {

@@ -38,0 +39,0 @@ var _a, _b;

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

private allocd;
allocCount: number;
constructor(onBeforeDeallocCallback: BeforeDeallocCallback | null, cacheState: CacheState);

@@ -13,0 +14,0 @@ startRender(): AbstractRenderer;

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

this.allocd = false;
// For debugging
this.allocCount = 0;
this.renderer = cacheState.props.createRenderer();

@@ -38,2 +40,3 @@ this.lastUsedCycle = -1;

this.lastUsedCycle = this.cacheState.currentRenderingCycle;
this.allocCount++;
}

@@ -40,0 +43,0 @@ getLastUsedCycle() {

@@ -1,11 +0,12 @@

import { BeforeDeallocCallback, PartialCacheState } from './types';
import { BeforeDeallocCallback, CacheProps, CacheState } from './types';
import CacheRecord from './CacheRecord';
import Rect2 from '../../math/Rect2';
export declare class CacheRecordManager {
private readonly cacheState;
private cacheRecords;
private maxCanvases;
constructor(cacheState: PartialCacheState);
private cacheState;
constructor(cacheProps: CacheProps);
setSharedState(state: CacheState): void;
allocCanvas(drawTo: Rect2, onDealloc: BeforeDeallocCallback): CacheRecord;
private getLeastRecentlyUsedRecord;
}
import CacheRecord from './CacheRecord';
const debugMode = false;
export class CacheRecordManager {
constructor(cacheState) {
this.cacheState = cacheState;
constructor(cacheProps) {
// Fixed-size array: Cache blocks are assigned indicies into [cachedCanvases].
this.cacheRecords = [];
const cacheProps = cacheState.props;
this.maxCanvases = Math.ceil(

@@ -12,7 +11,13 @@ // Assuming four components per pixel:

}
setSharedState(state) {
this.cacheState = state;
}
allocCanvas(drawTo, onDealloc) {
if (this.cacheRecords.length < this.maxCanvases) {
const record = new CacheRecord(onDealloc, Object.assign(Object.assign({}, this.cacheState), { recordManager: this }));
const record = new CacheRecord(onDealloc, this.cacheState);
record.setRenderingRegion(drawTo);
this.cacheRecords.push(record);
if (debugMode) {
console.log('[Cache] Cache spaces used: ', this.cacheRecords.length, ' of ', this.maxCanvases);
}
return record;

@@ -22,4 +27,11 @@ }

const lru = this.getLeastRecentlyUsedRecord();
if (debugMode) {
console.log('[Cache] Re-alloc. Times allocated: ', lru.allocCount, '\nLast used cycle: ', lru.getLastUsedCycle(), '\nCurrent cycle: ', this.cacheState.currentRenderingCycle);
}
lru.realloc(onDealloc);
lru.setRenderingRegion(drawTo);
if (debugMode) {
console.log('[Cache] Now re-alloc\'d. Last used cycle: ', lru.getLastUsedCycle());
console.assert(lru['cacheState'] === this.cacheState, '[Cache] Unequal cache states! cacheState should be a shared object!');
}
return lru;

@@ -26,0 +38,0 @@ }

import { ImageNode } from '../../EditorImage';
import Viewport from '../../Viewport';
import AbstractRenderer from '../renderers/AbstractRenderer';
import { CacheProps, CacheState } from './types';
import { CacheProps } from './types';
export default class RenderingCache {
private partialSharedState;
private sharedState;
private recordManager;
private rootNode;
constructor(cacheProps: CacheProps);
getSharedState(): CacheState;
render(screenRenderer: AbstractRenderer, image: ImageNode, viewport: Viewport): void;
}

@@ -6,17 +6,16 @@ import Rect2 from '../../math/Rect2';

constructor(cacheProps) {
this.partialSharedState = {
this.recordManager = new CacheRecordManager(cacheProps);
this.sharedState = {
props: cacheProps,
currentRenderingCycle: 0,
recordManager: this.recordManager,
};
this.recordManager = new CacheRecordManager(this.partialSharedState);
this.recordManager.setSharedState(this.sharedState);
}
getSharedState() {
return Object.assign(Object.assign({}, this.partialSharedState), { recordManager: this.recordManager });
}
render(screenRenderer, image, viewport) {
var _a;
const visibleRect = viewport.visibleRect;
this.partialSharedState.currentRenderingCycle++;
this.sharedState.currentRenderingCycle++;
// If we can't use the cache,
if (!this.partialSharedState.props.isOfCorrectType(screenRenderer)) {
if (!this.sharedState.props.isOfCorrectType(screenRenderer)) {
image.render(screenRenderer, visibleRect);

@@ -27,5 +26,5 @@ return;

// Adjust the node so that it has the correct aspect ratio
const res = this.partialSharedState.props.blockResolution;
const res = this.sharedState.props.blockResolution;
const topLeft = visibleRect.topLeft;
this.rootNode = new RenderingCacheNode(new Rect2(topLeft.x, topLeft.y, res.x, res.y), this.getSharedState());
this.rootNode = new RenderingCacheNode(new Rect2(topLeft.x, topLeft.y, res.x, res.y), this.sharedState);
}

@@ -37,3 +36,3 @@ while (!this.rootNode.region.containsRect(visibleRect)) {

const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
if (visibleLeaves.length > this.sharedState.props.minComponentsToUseCache) {
this.rootNode.renderItems(screenRenderer, [image], viewport);

@@ -40,0 +39,0 @@ }

@@ -15,8 +15,6 @@ import { Vec2 } from '../../math/Vec2';

}
export interface PartialCacheState {
export interface CacheState {
currentRenderingCycle: number;
props: CacheProps;
}
export interface CacheState extends PartialCacheState {
recordManager: CacheRecordManager;
}

@@ -0,1 +1,15 @@

/**
* Handles `HTMLCanvasElement`s (or other drawing surfaces if being used) used to display the editor's contents.
*
* @example
* ```
* const editor = new Editor(document.body);
* const w = editor.display.width;
* const h = editor.display.height;
* const center = Vec2.of(w / 2, h / 2);
* const colorAtCenter = editor.display.getColorAt(center);
* ```
*
* @packageDocumentation
*/
import AbstractRenderer from './renderers/AbstractRenderer';

@@ -20,15 +34,45 @@ import { Editor } from '../Editor';

private flattenCallback?;
/** @internal */
constructor(editor: Editor, mode: RenderingMode, parent: HTMLElement | null);
/**
* @returns the visible width of the display (e.g. how much
* space the display's element takes up in the x direction
* in the DOM).
*/
get width(): number;
get height(): number;
/** @internal */
getCache(): RenderingCache;
/**
* @returns the color at the given point on the dry ink renderer, or `null` if `screenPos`
* is not on the display.
*/
getColorAt: (_screenPos: Point2) => Color4 | null;
private initializeCanvasRendering;
private initializeTextRendering;
/**
* Rerenders the text-based display.
* The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
*/
rerenderAsText(): void;
/**
* Clears the drawing surfaces and otherwise prepares for a rerender.
*
* @returns the dry ink renderer.
*/
startRerender(): AbstractRenderer;
/**
* If `draftMode`, the dry ink renderer is configured to render
* low-quality output.
*/
setDraftMode(draftMode: boolean): void;
/** @internal */
getDryInkRenderer(): AbstractRenderer;
/**
* @returns The renderer used for showing action previews (e.g. an unfinished stroke).
* The `wetInkRenderer`'s surface is stacked above the `dryInkRenderer`'s.
*/
getWetInkRenderer(): AbstractRenderer;
/** Re-renders the contents of the wetInkRenderer onto the dryInkRenderer. */
flatten(): void;
}

@@ -0,1 +1,15 @@

/**
* Handles `HTMLCanvasElement`s (or other drawing surfaces if being used) used to display the editor's contents.
*
* @example
* ```
* const editor = new Editor(document.body);
* const w = editor.display.width;
* const h = editor.display.height;
* const center = Vec2.of(w / 2, h / 2);
* const colorAtCenter = editor.display.getColorAt(center);
* ```
*
* @packageDocumentation
*/
import CanvasRenderer from './renderers/CanvasRenderer';

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

export default class Display {
/** @internal */
constructor(editor, mode, parent) {

@@ -20,2 +35,6 @@ this.editor = editor;

this.textRerenderOutput = null;
/**
* @returns the color at the given point on the dry ink renderer, or `null` if `screenPos`
* is not on the display.
*/
this.getColorAt = (_screenPos) => {

@@ -57,3 +76,3 @@ return null;

blockResolution: cacheBlockResolution,
cacheSize: 500 * 500 * 4 * 220,
cacheSize: 500 * 500 * 4 * 150,
maxScale: 1.5,

@@ -71,5 +90,7 @@ minComponentsPerCache: 45,

}
// Returns the visible width of the display (e.g. how much
// space the display's element takes up in the x direction
// in the DOM).
/**
* @returns the visible width of the display (e.g. how much
* space the display's element takes up in the x direction
* in the DOM).
*/
get width() {

@@ -81,2 +102,3 @@ return this.dryInkRenderer.displaySize().x;

}
/** @internal */
getCache() {

@@ -143,2 +165,6 @@ return this.cache;

}
/**
* Rerenders the text-based display.
* The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
*/
rerenderAsText() {

@@ -151,3 +177,7 @@ this.textRenderer.clear();

}
// Clears the drawing surfaces and otherwise prepares for a rerender.
/**
* Clears the drawing surfaces and otherwise prepares for a rerender.
*
* @returns the dry ink renderer.
*/
startRerender() {

@@ -160,12 +190,21 @@ var _a;

}
/**
* If `draftMode`, the dry ink renderer is configured to render
* low-quality output.
*/
setDraftMode(draftMode) {
this.dryInkRenderer.setDraftMode(draftMode);
}
/** @internal */
getDryInkRenderer() {
return this.dryInkRenderer;
}
/**
* @returns The renderer used for showing action previews (e.g. an unfinished stroke).
* The `wetInkRenderer`'s surface is stacked above the `dryInkRenderer`'s.
*/
getWetInkRenderer() {
return this.wetInkRenderer;
}
// Re-renders the contents of the wetInkRenderer onto the dryInkRenderer
/** Re-renders the contents of the wetInkRenderer onto the dryInkRenderer. */
flatten() {

@@ -172,0 +211,0 @@ var _a;

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

this.minSquareCurveApproxDist = 1;
this.minRenderSizeBothDimens = 1;
this.minRenderSizeBothDimens = 0.5;
this.minRenderSizeAnyDimen = 0;

@@ -49,0 +49,0 @@ }

@@ -32,2 +32,3 @@ import { ToolType } from '../tools/ToolController';

}
// @internal
setupColorPickers() {

@@ -34,0 +35,0 @@ const closePickerOverlay = document.createElement('div');

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

import { makePipetteIcon } from './icons';
// Returns [ input, container ].
// Returns [ color input, input container ].
export const makeColorInput = (editor, onColorChange) => {

@@ -8,0 +8,0 @@ const colorInputContainer = document.createElement('span');

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

// @internal @packageDocumentation
import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';

@@ -2,0 +3,0 @@ import { makeFreehandLineBuilder } from '../../components/builders/FreehandLineBuilder';

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

import { ComponentBuilderFactory } from '../components/builders/types';
interface PenStyle {
export interface PenStyle {
color: Color4;

@@ -36,2 +36,1 @@ thickness: number;

}
export {};

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

const minPressure = 0.3;
const pressure = Math.max((_a = pointer.pressure) !== null && _a !== void 0 ? _a : 1.0, minPressure);
let pressure = Math.max((_a = pointer.pressure) !== null && _a !== void 0 ? _a : 1.0, minPressure);
if (!isFinite(pressure)) {
console.warn('Non-finite pressure!', pointer);
pressure = minPressure;
}
console.assert(isFinite(pointer.canvasPos.length()), 'Non-finite canvas position!');
console.assert(isFinite(pointer.screenPos.length()), 'Non-finite screen position!');
console.assert(isFinite(pointer.timeStamp), 'Non-finite timeStamp on pointer!');
return {

@@ -26,0 +33,0 @@ pos: pointer.canvasPos,

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

// @internal @packageDocumentation
import BaseTool from './BaseTool';

@@ -2,0 +3,0 @@ import { ToolType } from './ToolController';

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

};
import Command from '../commands/Command';
var _a;
import Duplicate from '../commands/Duplicate';

@@ -22,2 +22,3 @@ import Erase from '../commands/Erase';

import { ToolType } from './ToolController';
import SerializableCommand from '../commands/SerializableCommand';
const handleScreenSize = 30;

@@ -123,2 +124,3 @@ const styles = `

const updateChunkSize = 100;
// @internal
class Selection {

@@ -234,3 +236,3 @@ constructor(startPoint, editor) {

// Make the commands undo-able
this.editor.dispatch(new Selection.ApplyTransformationCommand(this, currentTransfmCommands, fullTransform, inverseTransform, deltaBoxRotation));
this.editor.dispatch(new Selection.ApplyTransformationCommand(this, currentTransfmCommands, fullTransform, deltaBoxRotation));
}

@@ -359,32 +361,53 @@ // Preview the effects of the current transformation on the selection

}
Selection.ApplyTransformationCommand = class extends Command {
constructor(selection, currentTransfmCommands, fullTransform, inverseTransform, deltaBoxRotation) {
super();
_a = Selection;
(() => {
SerializableCommand.register('selection-tool-transform', (json, editor) => {
// The selection box is lost when serializing/deserializing. No need to store box rotation
const guiBoxRotation = 0;
const fullTransform = new Mat33(...json.transform);
const commands = json.commands.map(data => SerializableCommand.deserialize(data, editor));
return new _a.ApplyTransformationCommand(null, commands, fullTransform, guiBoxRotation);
});
})();
Selection.ApplyTransformationCommand = class extends SerializableCommand {
constructor(selection, currentTransfmCommands, fullTransform, deltaBoxRotation) {
super('selection-tool-transform');
this.selection = selection;
this.currentTransfmCommands = currentTransfmCommands;
this.fullTransform = fullTransform;
this.inverseTransform = inverseTransform;
this.deltaBoxRotation = deltaBoxRotation;
}
apply(editor) {
var _b, _c;
return __awaiter(this, void 0, void 0, function* () {
// Approximate the new selection
this.selection.region = this.selection.region.transformedBoundingBox(this.fullTransform);
this.selection.boxRotation += this.deltaBoxRotation;
this.selection.updateUI();
if (this.selection) {
this.selection.region = this.selection.region.transformedBoundingBox(this.fullTransform);
this.selection.boxRotation += this.deltaBoxRotation;
this.selection.updateUI();
}
yield editor.asyncApplyCommands(this.currentTransfmCommands, updateChunkSize);
this.selection.recomputeRegion();
this.selection.updateUI();
(_b = this.selection) === null || _b === void 0 ? void 0 : _b.recomputeRegion();
(_c = this.selection) === null || _c === void 0 ? void 0 : _c.updateUI();
});
}
unapply(editor) {
var _b, _c;
return __awaiter(this, void 0, void 0, function* () {
this.selection.region = this.selection.region.transformedBoundingBox(this.inverseTransform);
this.selection.boxRotation -= this.deltaBoxRotation;
this.selection.updateUI();
if (this.selection) {
this.selection.region = this.selection.region.transformedBoundingBox(this.fullTransform.inverse());
this.selection.boxRotation -= this.deltaBoxRotation;
this.selection.updateUI();
}
yield editor.asyncUnapplyCommands(this.currentTransfmCommands, updateChunkSize);
this.selection.recomputeRegion();
this.selection.updateUI();
(_b = this.selection) === null || _b === void 0 ? void 0 : _b.recomputeRegion();
(_c = this.selection) === null || _c === void 0 ? void 0 : _c.updateUI();
});
}
serializeToJSON() {
return {
commands: this.currentTransfmCommands.map(command => command.serialize()),
transform: this.fullTransform.toArray(),
};
}
description(_editor, localizationTable) {

@@ -405,5 +428,5 @@ return localizationTable.transformedElements(this.currentTransfmCommands.length);

editor.notifier.on(EditorEventType.ViewportChanged, _data => {
var _a, _b;
(_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.recomputeRegion();
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.updateUI();
var _b, _c;
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.recomputeRegion();
(_c = this.selectionBox) === null || _c === void 0 ? void 0 : _c.updateUI();
});

@@ -456,7 +479,7 @@ this.editor.handleKeyEventsFrom(this.handleOverlay);

onGestureCancel() {
var _a, _b;
var _b, _c;
// Revert to the previous selection, if any.
(_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.cancelSelection();
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.cancelSelection();
this.selectionBox = this.prevSelectionBox;
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.appendBackgroundBoxTo(this.handleOverlay);
(_c = this.selectionBox) === null || _c === void 0 ? void 0 : _c.appendBackgroundBoxTo(this.handleOverlay);
}

@@ -463,0 +486,0 @@ onKeyPress(event) {

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

import Color4 from './Color4';
import Command from './commands/Command';
export interface PointerEvtListener {

@@ -65,7 +66,9 @@ onPointerDown(event: PointerEvt): boolean;

UndoRedoStackUpdated = 3,
ObjectAdded = 4,
ViewportChanged = 5,
DisplayResized = 6,
ColorPickerToggled = 7,
ColorPickerColorSelected = 8
CommandDone = 4,
CommandUndone = 5,
ObjectAdded = 6,
ViewportChanged = 7,
DisplayResized = 8,
ColorPickerToggled = 9,
ColorPickerColorSelected = 10
}

@@ -95,2 +98,10 @@ declare type EditorToolEventType = EditorEventType.ToolEnabled | EditorEventType.ToolDisabled | EditorEventType.ToolUpdated;

}
export interface CommandDoneEvent {
readonly kind: EditorEventType.CommandDone;
readonly command: Command;
}
export interface CommandUndoneEvent {
readonly kind: EditorEventType.CommandUndone;
readonly command: Command;
}
export interface ColorPickerToggled {

@@ -104,3 +115,3 @@ readonly kind: EditorEventType.ColorPickerToggled;

}
export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | ColorPickerToggled | ColorPickerColorSelected;
export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent | ColorPickerToggled | ColorPickerColorSelected;
export declare type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;

@@ -107,0 +118,0 @@ export declare type ComponentAddedListener = (component: AbstractComponent) => void;

@@ -18,7 +18,9 @@ // Types related to the image editor

EditorEventType[EditorEventType["UndoRedoStackUpdated"] = 3] = "UndoRedoStackUpdated";
EditorEventType[EditorEventType["ObjectAdded"] = 4] = "ObjectAdded";
EditorEventType[EditorEventType["ViewportChanged"] = 5] = "ViewportChanged";
EditorEventType[EditorEventType["DisplayResized"] = 6] = "DisplayResized";
EditorEventType[EditorEventType["ColorPickerToggled"] = 7] = "ColorPickerToggled";
EditorEventType[EditorEventType["ColorPickerColorSelected"] = 8] = "ColorPickerColorSelected";
EditorEventType[EditorEventType["CommandDone"] = 4] = "CommandDone";
EditorEventType[EditorEventType["CommandUndone"] = 5] = "CommandUndone";
EditorEventType[EditorEventType["ObjectAdded"] = 6] = "ObjectAdded";
EditorEventType[EditorEventType["ViewportChanged"] = 7] = "ViewportChanged";
EditorEventType[EditorEventType["DisplayResized"] = 8] = "DisplayResized";
EditorEventType[EditorEventType["ColorPickerToggled"] = 9] = "ColorPickerToggled";
EditorEventType[EditorEventType["ColorPickerColorSelected"] = 10] = "ColorPickerColorSelected";
})(EditorEventType || (EditorEventType = {}));
import { EditorEventType } from './types';
class UndoRedoHistory {
// @internal
constructor(editor, announceRedoCallback, announceUndoCallback) {

@@ -28,2 +29,6 @@ this.editor = editor;

this.fireUpdateEvent();
this.editor.notifier.dispatch(EditorEventType.CommandDone, {
kind: EditorEventType.CommandDone,
command,
});
}

@@ -37,4 +42,8 @@ // Remove the last command from this' undo stack and apply it.

this.announceUndoCallback(command);
this.fireUpdateEvent();
this.editor.notifier.dispatch(EditorEventType.CommandUndone, {
kind: EditorEventType.CommandUndone,
command,
});
}
this.fireUpdateEvent();
}

@@ -47,4 +56,8 @@ redo() {

this.announceRedoCallback(command);
this.fireUpdateEvent();
this.editor.notifier.dispatch(EditorEventType.CommandDone, {
kind: EditorEventType.CommandDone,
command,
});
}
this.fireUpdateEvent();
}

@@ -51,0 +64,0 @@ get undoStackSize() {

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

export class Viewport {
// @internal
constructor(notifier) {

@@ -28,2 +29,3 @@ this.notifier = notifier;

}
// @internal
updateScreenSize(screenSize) {

@@ -45,3 +47,3 @@ this.screenRect = this.screenRect.resizedTo(screenSize);

}
// Updates the transformation directly. Using transformBy is preferred.
// Updates the transformation directly. Using `transformBy` is preferred.
// [newTransform] should map from canvas coordinates to screen coordinates.

@@ -72,2 +74,3 @@ resetTransform(newTransform = Mat33.identity) {

}
// Returns the size of one screen pixel in canvas units.
getSizeOfPixelOnCanvas() {

@@ -74,0 +77,0 @@ return 1 / this.getScaleFactor();

{
"name": "js-draw",
"version": "0.1.12",
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
"main": "dist/src/Editor.js",
"types": "dist/src/Editor.d.ts",
"exports": {
".": {
"types": "./dist/src/Editor.d.ts",
"default": "./dist/src/Editor.js"
},
"./localizations/getLocalizationTable": {
"types": "./dist/src/localizations/getLocalizationTable.d.ts",
"default": "./dist/src/localizations/getLocalizationTable.js"
},
"./getLocalizationTable": {
"types": "./dist/src/localizations/getLocalizationTable.d.ts",
"default": "./dist/src/localizations/getLocalizationTable.js"
},
"./styles": {
"default": "./src/styles.js"
},
"./Editor": {
"types": "./dist/src/Editor.d.ts",
"default": "./dist/src/Editor.js"
},
"./localization": {
"types": "./dist/src/localization.d.ts",
"default": "./dist/src/localization.js"
},
"./toolbar/HTMLToolbar": {
"types": "./dist/src/toolbar/HTMLToolbar.d.ts",
"default": "./dist/src/toolbar/HTMLToolbar.js"
},
"./Editor.css": {
"default": "./src/Editor.css"
},
"./toolbar/toolbar.css": {
"default": "./src/toolbar/toolbar.css"
},
"./bundle": {
"default": "./dist/bundle.js"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/personalizedrefrigerator/js-draw.git"
},
"author": "Henry Heino",
"license": "MIT",
"private": false,
"scripts": {
"test": "jest",
"build": "rm -rf ./dist; mkdir dist && yarn tsc && ts-node ./build_tools/bundle.ts",
"lint": "eslint .",
"linter-precommit": "eslint --fix --ext .js --ext .ts",
"lint-staged": "lint-staged",
"_postinstall": "husky install",
"prepack": "yarn build && pinst --disable",
"postpack": "pinst --enable"
},
"dependencies": {
"@melloware/coloris": "^0.16.0",
"bezier-js": "^6.1.0"
},
"devDependencies": {
"@types/bezier-js": "^4.1.0",
"@types/jest": "^28.1.7",
"@types/jsdom": "^20.0.0",
"@types/node": "^18.7.15",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"css-loader": "^6.7.1",
"eslint": "^8.23.0",
"husky": "^8.0.1",
"jest": "^28.1.3",
"jest-environment-jsdom": "^29.0.2",
"jsdom": "^20.0.0",
"lint-staged": "^13.0.3",
"pinst": "^3.0.0",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.5",
"ts-jest": "^28.0.8",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"typescript": "^4.8.2",
"webpack": "^5.74.0"
},
"bugs": {
"url": "https://github.com/personalizedrefrigerator/js-draw/issues"
},
"homepage": "https://github.com/personalizedrefrigerator/js-draw#readme",
"directories": {
"doc": "docs"
},
"keywords": [
"ink",
"drawing",
"pen",
"freehand",
"svg"
]
"name": "js-draw",
"version": "0.2.0",
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
"main": "./dist/src/lib.d.ts",
"types": "./dist/src/lib.js",
"exports": {
".": {
"types": "./dist/src/lib.d.ts",
"default": "./dist/src/lib.js"
},
"./getLocalizationTable": {
"types": "./dist/src/localizations/getLocalizationTable.d.ts",
"default": "./dist/src/localizations/getLocalizationTable.js"
},
"./styles": {
"default": "./src/styles.js"
},
"./Editor": {
"types": "./dist/src/Editor.d.ts",
"default": "./dist/src/Editor.js"
},
"./types": {
"types": "./dist/src/types.d.ts",
"default": "./dist/src/types.js"
},
"./localization": {
"types": "./dist/src/localization.d.ts",
"default": "./dist/src/localization.js"
},
"./toolbar/HTMLToolbar": {
"types": "./dist/src/toolbar/HTMLToolbar.d.ts",
"default": "./dist/src/toolbar/HTMLToolbar.js"
},
"./Editor.css": {
"default": "./src/Editor.css"
},
"./math": {
"types": "./dist/src/math/lib.d.ts",
"default": "./dist/src/math/lib.js"
},
"./Color4": {
"types": "./dist/src/Color4.d.ts",
"default": "./dist/src/Color4.js"
},
"./components": {
"types": "./dist/src/components/lib.d.ts",
"default": "./dist/src/components/lib.js"
},
"./commands": {
"types": "./dist/src/commands/lib.d.ts",
"default": "./dist/src/commands/lib.js"
},
"./bundle": {
"types": "./dist/src/bundle/bundled.d.ts",
"default": "./dist/bundle.js"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/personalizedrefrigerator/js-draw.git"
},
"author": "Henry Heino",
"license": "MIT",
"private": false,
"scripts": {
"test": "jest",
"build": "rm -rf ./dist; mkdir dist && yarn tsc && ts-node ./build_tools/bundle.ts",
"doc": "typedoc --options typedoc.json",
"watch-docs": "typedoc --watch --options typedoc.json",
"lint": "eslint .",
"linter-precommit": "eslint --fix --ext .js --ext .ts",
"lint-staged": "lint-staged",
"prepare": "husky install && yarn build",
"prepack": "yarn build && pinst --disable",
"postpack": "pinst --enable"
},
"dependencies": {
"@melloware/coloris": "^0.16.0",
"bezier-js": "^6.1.0"
},
"devDependencies": {
"@types/bezier-js": "^4.1.0",
"@types/jest": "^28.1.7",
"@types/jsdom": "^20.0.0",
"@types/node": "^18.7.15",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"css-loader": "^6.7.1",
"eslint": "^8.23.0",
"husky": "^8.0.1",
"jest": "^28.1.3",
"jest-environment-jsdom": "^29.0.2",
"jsdom": "^20.0.0",
"lint-staged": "^13.0.3",
"pinst": "^3.0.0",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.5",
"ts-jest": "^28.0.8",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"typedoc": "^0.23.14",
"typescript": "^4.8.2",
"webpack": "^5.74.0"
},
"bugs": {
"url": "https://github.com/personalizedrefrigerator/js-draw/issues"
},
"homepage": "https://github.com/personalizedrefrigerator/js-draw#readme",
"directories": {
"doc": "docs"
},
"keywords": [
"ink",
"drawing",
"pen",
"freehand",
"svg"
]
}
# js-draw
[NPM package](https://www.npmjs.com/package/js-draw) | [GitHub](https://github.com/personalizedrefrigerator/js-draw) | [Try it!](https://personalizedrefrigerator.github.io/js-draw/example/example.html)
[NPM package](https://www.npmjs.com/package/js-draw) | [GitHub](https://github.com/personalizedrefrigerator/js-draw) | [Documentation](https://personalizedrefrigerator.github.io/js-draw/typedoc/) | [Try it!](https://personalizedrefrigerator.github.io/js-draw/example/example.html)
![](docs/img/js-draw.jpg)
For example usage, see [docs/example/example.ts](docs/example/example.ts).
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).
At present, not all documented modules are `import`-able.
# API

@@ -16,2 +18,3 @@

### With a bundler that supports importing `.css` files
To create a new `Editor` and add it as a child of `document.body`,

@@ -28,2 +31,3 @@ ```ts

### With a bundler that doesn't support importing `.css` files
Import the pre-bundled version of the editor to apply CSS after loading the page.

@@ -39,6 +43,7 @@ ```ts

### Without a bundler
If you're not using a bundler, consider using the pre-bundled editor:
```html
<!-- Replace 0.1.7 with the latest version of js-draw -->
<script src="https://cdn.jsdelivr.net/npm/js-draw@0.1.7/dist/bundle.js"></script>
<!-- Replace 0.2.0 with the latest version of js-draw -->
<script src="https://cdn.jsdelivr.net/npm/js-draw@0.2.0/dist/bundle.js"></script>
<script>

@@ -85,3 +90,3 @@ const editor = new jsdraw.Editor(document.body);

For example, although `js-draw` doesn't support `<circle/>` elements,
```svg
```xml
<svg

@@ -100,3 +105,3 @@ viewBox="156 74 200 150"

but exports to
```svg
```xml
<svg viewBox="156 74 200 150" width="200" height="150" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"><g><path d="M156,150M156,150Q190,190 209,217L213,215Q193,187 160,148M209,217M209,217Q212,218 236,178L232,176Q210,215 213,215M236,178M236,178Q240,171 307,95L305,93Q237,168 232,176M307,95M307,95Q312,90 329,78L327,74Q309,87 305,93" fill="#07a837"></path></g><circle cx="200" cy="100" r="40" fill="red"></circle></svg>

@@ -103,0 +108,0 @@ ```

@@ -5,5 +5,4 @@ // Main entrypoint for Webpack when building a bundle for release.

import Editor from '../Editor';
import getLocalizationTable from '../localizations/getLocalizationTable';
export * from '../lib';
export default Editor;
export { Editor, getLocalizationTable };
export default class Color4 {
private constructor(
/** Red component. Should be in the range [0, 1]. */
public readonly r: number,
/** Green component. `g` ∈ [0, 1] */
public readonly g: number,
/** Blue component. `b` ∈ [0, 1] */
public readonly b: number,
/** Alpha/transparent component. `a` ∈ [0, 1] */
public readonly a: number

@@ -11,3 +18,7 @@ ) {

// Each component should be in the range [0, 1]
/**
* Create a color from red, green, blue components. The color is fully opaque (`a = 1.0`).
*
* Each component should be in the range [0, 1].
*/
public static ofRGB(red: number, green: number, blue: number): Color4 {

@@ -62,3 +73,3 @@ return Color4.ofRGBA(red, green, blue, 1.0);

// Like fromHex, but can handle additional colors if an HTML5Canvas is available.
/** Like fromHex, but can handle additional colors if an `HTMLCanvasElement` is available. */
public static fromString(text: string): Color4 {

@@ -87,2 +98,3 @@ if (text.startsWith('#')) {

/** @returns true if `this` and `other` are approximately equal. */
public eq(other: Color4|null|undefined): boolean {

@@ -97,2 +109,11 @@ if (other == null) {

private hexString: string|null = null;
/**
* @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
*
* @example
* ```
* Color4.red.toHexString(); // -> #ff0000ff
* ```
*/
public toHexString(): string {

@@ -99,0 +120,0 @@ if (this.hexString) {

@@ -38,9 +38,8 @@ import AbstractComponent from '../components/AbstractComponent';

protected serializeToString(): string {
return JSON.stringify(this.toDuplicate.map(elem => elem.getId()));
protected serializeToJSON() {
return this.toDuplicate.map(elem => elem.getId());
}
static {
SerializableCommand.register('duplicate', (data: string, editor: Editor) => {
const json = JSON.parse(data);
SerializableCommand.register('duplicate', (json: any, editor: Editor) => {
const elems = json.map((id: string) => editor.image.lookupElement(id));

@@ -47,0 +46,0 @@ return new Duplicate(elems);

@@ -61,11 +61,12 @@ import AbstractComponent from '../components/AbstractComponent';

protected serializeToString() {
protected serializeToJSON() {
const elemIds = this.toRemove.map(elem => elem.getId());
return JSON.stringify(elemIds);
return elemIds;
}
static {
SerializableCommand.register('erase', (data: string, editor: Editor) => {
const json = JSON.parse(data);
const elems = json.map((elemId: string) => editor.image.lookupElement(elemId));
SerializableCommand.register('erase', (json: any, editor) => {
const elems = json
.map((elemId: string) => editor.image.lookupElement(elemId))
.filter((elem: AbstractComponent|null) => elem !== null);
return new Erase(elems);

@@ -72,0 +73,0 @@ });

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

duplicateAction: (elemDescription: string, count: number)=> string;
inverseOf: (actionDescription: string)=> string;

@@ -32,2 +33,3 @@ selectedElements: (count: number)=>string;

duplicateAction: (componentDescription: string, numElems: number) => `Duplicated ${numElems} ${componentDescription}`,
inverseOf: (actionDescription: string) => `Inverse of ${actionDescription}`,
elements: 'Elements',

@@ -34,0 +36,0 @@ erasedNoElements: 'Erased nothing',

import Editor from '../Editor';
import Command from './Command';
type DeserializationCallback = (data: string, editor: Editor) => SerializableCommand;
export type DeserializationCallback = (data: Record<string, any>|any[], editor: Editor) => SerializableCommand;

@@ -17,14 +17,20 @@ export default abstract class SerializableCommand extends Command {

protected abstract serializeToString(): string;
protected abstract serializeToJSON(): string|Record<string, any>|any[];
private static deserializationCallbacks: Record<string, DeserializationCallback> = {};
public serialize(): string {
return JSON.stringify({
data: this.serializeToString(),
// Convert this command to an object that can be passed to `JSON.stringify`.
//
// Do not rely on the stability of the optupt of this function — it can change
// form without a major version increase.
public serialize(): Record<string|symbol, any> {
return {
data: this.serializeToJSON(),
commandType: this.commandTypeId,
});
};
}
public static deserialize(data: string, editor: Editor): SerializableCommand {
const json = JSON.parse(data);
// Convert a `string` containing JSON data (or the output of `JSON.parse`) into a
// `Command`.
public static deserialize(data: string|Record<string, any>, editor: Editor): SerializableCommand {
const json = typeof data === 'string' ? JSON.parse(data) : data;
const commandType = json.commandType as string;

@@ -36,5 +42,7 @@

return SerializableCommand.deserializationCallbacks[commandType](json.data as string, editor);
return SerializableCommand.deserializationCallbacks[commandType](json.data, editor);
}
// Register a deserialization callback. This must be called at least once for every subclass of
// `SerializableCommand`.
public static register(commandTypeId: string, deserialize: DeserializationCallback) {

@@ -41,0 +49,0 @@ SerializableCommand.deserializationCallbacks[commandTypeId] = deserialize;

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

import Command from '../commands/Command';
import SerializableCommand from '../commands/SerializableCommand';

@@ -12,5 +11,5 @@ import Editor from '../Editor';

type LoadSaveData = (string[]|Record<symbol, string|number>);
export type LoadSaveData = (string[]|Record<symbol, string|number>);
export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
type DeserializeCallback = (data: string)=>AbstractComponent;
export type DeserializeCallback = (data: string)=>AbstractComponent;
type ComponentId = string;

@@ -42,2 +41,4 @@

// Returns a unique ID for this element.
// @see { @link EditorImage!default.lookupElement }
public getId() {

@@ -82,3 +83,3 @@ return this.id;

// Return null iff this object cannot be safely serialized/deserialized.
protected abstract serializeToString(): string|null;
protected abstract serializeToJSON(): any[]|Record<string, any>|number|string|null;

@@ -90,3 +91,3 @@ // Private helper for transformBy: Apply the given transformation to all points of this.

// updates the editor.
public transformBy(affineTransfm: Mat33): Command {
public transformBy(affineTransfm: Mat33): SerializableCommand {
return new AbstractComponent.TransformElementCommand(affineTransfm, this);

@@ -140,4 +141,3 @@ }

static {
SerializableCommand.register('transform-element', (data: string, editor: Editor) => {
const json = JSON.parse(data);
SerializableCommand.register('transform-element', (json: any, editor: Editor) => {
const elem = editor.image.lookupElement(json.id);

@@ -162,7 +162,7 @@

protected serializeToString(): string {
return JSON.stringify({
protected serializeToJSON() {
return {
id: this.component.getId(),
transfm: this.affineTransfm.toArray(),
});
};
}

@@ -187,4 +187,9 @@ };

// Convert the component to an object that can be passed to
// `JSON.stringify`.
//
// Do not rely on the output of this function to take a particular form —
// this function's output can change form without a major version increase.
public serialize() {
const data = this.serializeToString();
const data = this.serializeToJSON();

@@ -195,3 +200,3 @@ if (data === null) {

return JSON.stringify({
return {
name: this.componentKind,

@@ -202,9 +207,11 @@ zIndex: this.zIndex,

data,
});
};
}
// Returns true if [data] is not deserializable. May return false even if [data]
// Returns true if `data` is not deserializable. May return false even if [data]
// is not deserializable.
private static isNotDeserializable(data: string) {
const json = JSON.parse(data);
private static isNotDeserializable(json: any|string) {
if (typeof json === 'string') {
json = JSON.parse(json);
}

@@ -226,8 +233,12 @@ if (typeof json !== 'object') {

public static deserialize(data: string): AbstractComponent {
if (AbstractComponent.isNotDeserializable(data)) {
throw new Error(`Element with data ${data} cannot be deserialized.`);
// Convert a string or an object produced by `JSON.parse` into an `AbstractComponent`.
public static deserialize(json: string|any): AbstractComponent {
if (typeof json === 'string') {
json = JSON.parse(json);
}
const json = JSON.parse(data);
if (AbstractComponent.isNotDeserializable(json)) {
throw new Error(`Element with data ${json} cannot be deserialized.`);
}
const instance = this.deserializationCallbacks[json.name]!(json.data);

@@ -234,0 +245,0 @@ instance.zIndex = json.zIndex;

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

it('strokes should deserialize from JSON data', () => {
const deserialized = Stroke.deserializeFromString(`[
const deserialized = Stroke.deserializeFromJSON(`[
{

@@ -64,0 +64,0 @@ "style": { "fill": "#f00" },

@@ -114,4 +114,4 @@ import LineSegment2 from '../math/LineSegment2';

protected serializeToString(): string | null {
return JSON.stringify(this.parts.map(part => {
protected serializeToJSON() {
return this.parts.map(part => {
return {

@@ -121,9 +121,13 @@ style: styleToJSON(part.style),

};
}));
});
}
public static deserializeFromString(data: string): Stroke {
const json = JSON.parse(data);
/** @internal */
public static deserializeFromJSON(json: any): Stroke {
if (typeof json === 'string') {
json = JSON.parse(json);
}
if (typeof json !== 'object' || typeof json.length !== 'number') {
throw new Error(`${data} is missing required field, parts, or parts is of the wrong type.`);
throw new Error(`${json} is missing required field, parts, or parts is of the wrong type.`);
}

@@ -139,2 +143,2 @@

AbstractComponent.registerComponent('stroke', Stroke.deserializeFromString);
AbstractComponent.registerComponent('stroke', Stroke.deserializeFromJSON);

@@ -0,1 +1,8 @@

//
// Used by `SVGLoader`s to store unrecognised global attributes
// (e.g. unrecognised XML namespace declarations).
// @internal
// @packageDocumentation
//
import LineSegment2 from '../math/LineSegment2';

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

protected serializeToString(): string | null {
protected serializeToJSON(): string | null {
return JSON.stringify(this.attrs);

@@ -50,0 +57,0 @@ }

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

// If not given, an HtmlCanvasElement is used to determine text boundaries.
// @internal
private readonly getTextDimens: GetTextDimensCallback = Text.getTextDimens,

@@ -156,3 +157,3 @@ ) {

protected serializeToString(): string {
protected serializeToJSON(): Record<string, any> {
const serializableStyle = {

@@ -170,3 +171,3 @@ ...this.style,

return {
json: text.serializeToString(),
json: text.serializeToJSON(),
};

@@ -176,12 +177,10 @@ }

return JSON.stringify({
return {
textObjects,
transform: this.transform.toArray(),
style: serializableStyle,
});
};
}
public static deserializeFromString(data: string, getTextDimens: GetTextDimensCallback = Text.getTextDimens): Text {
const json = JSON.parse(data);
public static deserializeFromString(json: any, getTextDimens: GetTextDimensCallback = Text.getTextDimens): Text {
const style: TextStyle = {

@@ -188,0 +187,0 @@ renderingStyle: styleFromJSON(json.style.renderingStyle),

@@ -0,1 +1,7 @@

//
// Stores objects loaded from an SVG that aren't recognised by the editor.
// @internal
// @packageDocumentation
//
import LineSegment2 from '../math/LineSegment2';

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

protected serializeToString(): string | null {
protected serializeToJSON(): string | null {
return JSON.stringify({

@@ -45,0 +51,0 @@ html: this.svgObject.outerHTML,

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

/**
* The main entrypoint for the full editor.
*
* @example
* To create an editor with a toolbar,
* ```
* const editor = new Editor(document.body);
*
* const toolbar = editor.addToolbar();
* toolbar.addActionButton('Save', () => {
* const saveData = editor.toSVG().outerHTML;
* // Do something with saveData...
* });
* ```
*
* @packageDocumentation
*/
import EditorImage from './EditorImage';

@@ -24,13 +42,16 @@ import ToolController from './tools/ToolController';

export interface EditorSettings {
// Defaults to RenderingMode.CanvasRenderer
/** Defaults to `RenderingMode.CanvasRenderer` */
renderingMode: RenderingMode,
// Uses a default English localization if a translation is not given.
/** Uses a default English localization if a translation is not given. */
localization: Partial<EditorLocalization>,
// True if touchpad/mousewheel scrolling should scroll the editor instead of the document.
// This does not include pinch-zoom events.
// Defaults to true.
/**
* `true` if touchpad/mousewheel scrolling should scroll the editor instead of the document.
* This does not include pinch-zoom events.
* Defaults to true.
*/
wheelEventsEnabled: boolean|'only-if-focused';
/** Minimum zoom fraction (e.g. 0.5 → 50% zoom). */
minZoom: number,

@@ -40,2 +61,3 @@ maxZoom: number,

// { @inheritDoc Editor! }
export class Editor {

@@ -46,8 +68,43 @@ // Wrapper around the viewport and toolbar

public display: Display;
/**
* Handles undo/redo.
*
* @example
* ```
* const editor = new Editor(document.body);
*
* // Do something undoable.
* // ...
*
* // Undo the last action
* editor.history.undo();
* ```
*/
public history: UndoRedoHistory;
public display: Display;
/**
* Data structure for adding/removing/querying objects in the image.
*
* @example
* ```
* const editor = new Editor(document.body);
*
* // Create a path.
* const stroke = new Stroke([
* Path.fromString('M0,0 L30,30 z').toRenderable({ fill: Color4.black }),
* ]);
* const addElementCommand = editor.image.addElement(stroke);
*
* // Add the stroke to the editor
* editor.dispatch(addElementCommand);
* ```
*/
public image: EditorImage;
// Viewport for the exported/imported image
/** Viewport for the exported/imported image. */
private importExportViewport: Viewport;
/** @internal */
public localization: EditorLocalization;

@@ -57,2 +114,7 @@

public toolController: ToolController;
/**
* Global event dispatcher/subscriber.
* @see {@link types.EditorEventType}
*/
public notifier: EditorNotifier;

@@ -66,2 +128,25 @@

/**
* @example
* ```
* const container = document.body;
*
* // Create an editor
* const editor = new Editor(container, {
* // 2e-10 and 1e12 are the default values for minimum/maximum zoom.
* minZoom: 2e-10,
* maxZoom: 1e12,
* });
*
* // Add the default toolbar
* const toolbar = editor.addToolbar();
* toolbar.addActionButton({
* label: 'Save'
* icon: createSaveIcon(),
* }, () => {
* const saveData = editor.toSVG().outerHTML;
* // Do something with saveData
* });
* ```
*/
public constructor(

@@ -153,5 +238,10 @@ parent: HTMLElement,

// Returns a reference to this' container.
// Example usage:
// editor.getRootElement().style.height = '500px';
/**
* @returns a reference to the editor's container.
*
* @example
* ```
* editor.getRootElement().style.height = '500px';
* ```
*/
public getRootElement(): HTMLElement {

@@ -161,3 +251,3 @@ return this.container;

// [fractionLoaded] should be a number from 0 to 1, where 1 represents completely loaded.
/** @param fractionLoaded - should be a number from 0 to 1, where 1 represents completely loaded. */
public showLoadingWarning(fractionLoaded: number) {

@@ -176,2 +266,5 @@ const loadingPercent = Math.round(fractionLoaded * 100);

private previousAccessibilityAnnouncement: string = '';
// Announce `message` for screen readers. If `message` is the same as the previous
// message, it is re-announced.
public announceForAccessibility(message: string) {

@@ -186,2 +279,6 @@ // Force re-announcing an announcement if announced again.

/**
* Creates a toolbar. If `defaultLayout` is true, default buttons are used.
* @returns a reference to the toolbar.
*/
public addToolbar(defaultLayout: boolean = true): HTMLToolbar {

@@ -352,4 +449,3 @@ const toolbar = new HTMLToolbar(this, this.container, this.localization);

// Adds event listners for keypresses to [elem] and forwards those events to the
// editor.
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
public handleKeyEventsFrom(elem: HTMLElement) {

@@ -382,3 +478,3 @@ elem.addEventListener('keydown', evt => {

// Adds to history by default
/** `apply` a command. `command` will be announced for accessibility. */
public dispatch(command: Command, addToHistory: boolean = true) {

@@ -395,3 +491,17 @@ if (addToHistory) {

// Dispatches a command without announcing it. By default, does not add to history.
/**
* Dispatches a command without announcing it. By default, does not add to history.
* Use this to show finalized commands that don't need to have `announceForAccessibility`
* 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
* ```
* const addToHistory = false;
* editor.dispatchNoAnnounce(editor.viewport.zoomTo(someRectangle), addToHistory);
* ```
*/
public dispatchNoAnnounce(command: Command, addToHistory: boolean = false) {

@@ -405,7 +515,9 @@ if (addToHistory) {

// Apply a large transformation in chunks.
// If [apply] is false, the commands are unapplied.
// Triggers a re-render after each [updateChunkSize]-sized group of commands
// has been applied.
private async asyncApplyOrUnapplyCommands(
/**
* Apply a large transformation in chunks.
* If `apply` is `false`, the commands are unapplied.
* Triggers a re-render after each `updateChunkSize`-sized group of commands
* has been applied.
*/
public async asyncApplyOrUnapplyCommands(
commands: Command[], apply: boolean, updateChunkSize: number

@@ -439,2 +551,3 @@ ) {

// @see {@link #asyncApplyOrUnapplyCommands }
public asyncApplyCommands(commands: Command[], chunkSize: number) {

@@ -444,2 +557,3 @@ return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);

// @see {@link #asyncApplyOrUnapplyCommands }
public asyncUnapplyCommands(commands: Command[], chunkSize: number) {

@@ -458,2 +572,4 @@ return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);

private rerenderQueued: boolean = false;
// Schedule a re-render for some time in the near future. Does not schedule an additional
// re-render if a re-render is already queued.
public queueRerender() {

@@ -505,3 +621,3 @@ if (!this.rerenderQueued) {

// Focuses the region used for text input
// Focuses the region used for text input/key commands.
public focus() {

@@ -511,2 +627,4 @@ this.renderingRegion.focus();

// Creates an element that will be positioned on top of the dry/wet ink
// renderers.
public createHTMLOverlay(overlay: HTMLElement) {

@@ -530,3 +648,3 @@ overlay.classList.add('overlay');

// Dispatch a pen event to the currently selected tool.
// Intented for unit tests.
// Intended primarially for unit tests.
public sendPenEvent(

@@ -611,3 +729,3 @@ eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,

// Resize the output SVG
// Resize the output SVG to match `imageRect`.
public setImportExportRect(imageRect: Rect2): Command {

@@ -638,4 +756,8 @@ const origSize = this.importExportViewport.visibleRect.size;

// Alias for loadFrom(SVGLoader.fromString).
// This is particularly useful when accessing a bundled version of the editor.
/**
* Alias for loadFrom(SVGLoader.fromString).
*
* This is particularly useful when accessing a bundled version of the editor,
* where `SVGLoader.fromString` is unavailable.
*/
public async loadFromSVG(svgData: string) {

@@ -642,0 +764,0 @@ const loader = SVGLoader.fromString(svgData);

import Editor from './Editor';
import AbstractRenderer from './rendering/renderers/AbstractRenderer';
import Command from './commands/Command';
import Viewport from './Viewport';

@@ -11,2 +10,3 @@ import AbstractComponent from './components/AbstractComponent';

// @internal
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {

@@ -21,2 +21,3 @@ leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());

// @internal
public constructor() {

@@ -38,2 +39,3 @@ this.root = new ImageNode();

/** @internal */
public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {

@@ -43,2 +45,3 @@ cache.render(screenRenderer, this.root, viewport);

/** @internal */
public render(renderer: AbstractRenderer, viewport: Viewport) {

@@ -48,3 +51,3 @@ this.root.render(renderer, viewport.visibleRect);

// Renders all nodes, even ones not within the viewport
/** Renders all nodes, even ones not within the viewport. @internal */
public renderAll(renderer: AbstractRenderer) {

@@ -66,2 +69,3 @@ const leaves = this.root.getLeaves();

/** @internal */
public onDestroyElement(elem: AbstractComponent) {

@@ -80,3 +84,3 @@ delete this.componentsById[elem.getId()];

public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): Command {
public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): SerializableCommand {
return new EditorImage.AddElementCommand(elem, applyByFlattening);

@@ -118,16 +122,17 @@ }

public description(editor: Editor, localization: EditorLocalization) {
public description(_editor: Editor, localization: EditorLocalization) {
return localization.addElementAction(this.element.description(localization));
}
protected serializeToString() {
return JSON.stringify({
protected serializeToJSON() {
return {
elemData: this.element.serialize(),
});
};
}
static {
SerializableCommand.register('add-element', (data: string, _editor: Editor) => {
const json = JSON.parse(data);
const elem = AbstractComponent.deserialize(json.elemData);
SerializableCommand.register('add-element', (json: any, editor: Editor) => {
const id = json.elemData.id;
const foundElem = editor.image.lookupElement(id);
const elem = foundElem ?? AbstractComponent.deserialize(json.elemData);
return new EditorImage.AddElementCommand(elem);

@@ -141,3 +146,3 @@ });

// TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
/** Part of the Editor's image. @internal */
export class ImageNode {

@@ -194,15 +199,25 @@ private content: AbstractComponent|null;

const result: ImageNode[] = [];
let current: ImageNode|undefined;
const workList: ImageNode[] = [];
// Don't render if too small
if (isTooSmall?.(this.bbox)) {
return [];
}
workList.push(this);
const toNext = () => {
current = undefined;
if (this.content !== null && this.getBBox().intersects(region)) {
result.push(this);
}
const next = workList.pop();
if (next && !isTooSmall?.(next.bbox)) {
current = next;
const children = this.getChildrenIntersectingRegion(region);
for (const child of children) {
result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
if (current.content !== null && current.getBBox().intersection(region)) {
result.push(current);
}
workList.push(
...current.getChildrenIntersectingRegion(region)
);
}
};
while (workList.length > 0) {
toNext();
}

@@ -252,11 +267,14 @@

if (leafBBox.containsRect(this.getBBox())) {
// Create a node for this' children and for the new content..
const nodeForNewLeaf = new ImageNode(this);
const nodeForChildren = new ImageNode(this);
nodeForChildren.children = this.children;
this.children = [nodeForNewLeaf, nodeForChildren];
nodeForChildren.recomputeBBox(true);
nodeForChildren.updateParents();
if (this.children.length < this.targetChildCount) {
this.children.push(nodeForNewLeaf);
} else {
const nodeForChildren = new ImageNode(this);
nodeForChildren.children = this.children;
this.children = [nodeForNewLeaf, nodeForChildren];
nodeForChildren.recomputeBBox(true);
nodeForChildren.updateParents();
}
return nodeForNewLeaf.addLeaf(leaf);

@@ -263,0 +281,0 @@ }

@@ -1,11 +0,26 @@

// Code shared with Joplin
/**
* Handles notifying listeners of events.
*
* `EventKeyType` is used to distinguish events (e.g. a `ClickEvent` vs a `TouchEvent`)
* while `EventMessageType` is the type of the data sent with an event (can be `void`).
*
* @example
* ```
* const dispatcher = new EventDispatcher<'event1'|'event2'|'event3', void>();
* dispatcher.on('event1', () => {
* console.log('Event 1 triggered.');
* });
* dispatcher.dispatch('event1');
* ```
*
* @packageDocumentation
*/
// Code shared with Joplin (js-draw was originally intended to be part of Joplin).
type Listener<Value> = (data: Value)=> void;
type CallbackHandler<EventType> = (data: EventType)=> void;
// EventKeyType is used to distinguish events (e.g. a 'ClickEvent' vs a 'TouchEvent')
// while EventMessageType is the type of the data sent with an event (can be `void`)
// { @inheritDoc EventDispatcher! }
export default class EventDispatcher<EventKeyType extends string|symbol|number, EventMessageType> {
// Partial marks all fields as optional. To initialize with an empty object, this is required.
// See https://stackoverflow.com/a/64526384
private listeners: Partial<Record<EventKeyType, Array<Listener<EventMessageType>>>>;

@@ -41,3 +56,3 @@ public constructor() {

// Equivalent to calling .remove() on the object returned by .on
/** Removes an event listener. This is equivalent to calling `.remove()` on the object returned by `.on`. */
public off(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>) {

@@ -44,0 +59,0 @@ const listeners = this.listeners[eventName];

import { Point2, Vec2 } from './Vec2';
import Vec3 from './Vec3';
// Represents a three dimensional linear transformation or
// a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
// **and** translates while a linear transformation just scales/rotates/shears).
/**
* Represents a three dimensional linear transformation or
* a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
* **and** translates while a linear transformation just scales/rotates/shears).
*/
export default class Mat33 {
private readonly rows: Vec3[];
// ⎡ a1 a2 a3 ⎤
// ⎢ b1 b2 b3 ⎥
// ⎣ c1 c2 c3 ⎦
/**
* Creates a matrix from inputs in the form,
* ```
* ⎡ a1 a2 a3 ⎤
* ⎢ b1 b2 b3 ⎥
* ⎣ c1 c2 c3 ⎦
* ```
*/
public constructor(

@@ -33,2 +40,10 @@ public readonly a1: number,

/**
* Creates a matrix from the given rows:
* ```
* ⎡ r1.x r1.y r1.z ⎤
* ⎢ r2.x r2.y r2.z ⎥
* ⎣ r3.x r3.y r3.z ⎦
* ```
*/
public static ofRows(r1: Vec3, r2: Vec3, r3: Vec3): Mat33 {

@@ -48,4 +63,9 @@ return new Mat33(

// Either returns the inverse of this, or, if this matrix is singular/uninvertable,
// returns Mat33.identity.
/**
* Either returns the inverse of this, or, if this matrix is singular/uninvertable,
* returns Mat33.identity.
*
* This may cache the computed inverse and return the cached version instead of recomputing
* it.
*/
public inverse(): Mat33 {

@@ -167,5 +187,7 @@ return this.computeInverse() ?? Mat33.identity;

// Applies this as an affine transformation to the given vector.
// Returns a transformed version of [other].
public transformVec2(other: Vec3): Vec2 {
/**
* Applies this as an affine transformation to the given vector.
* Returns a transformed version of `other`.
*/
public transformVec2(other: Vec2): Vec2 {
// When transforming a Vec2, we want to use the z transformation

@@ -185,4 +207,6 @@ // components of this for translation:

// Applies this as a linear transformation to the given vector (doesn't translate).
// This is the standard way of transforming vectors in ℝ³.
/**
* Applies this as a linear transformation to the given vector (doesn't translate).
* This is the standard way of transforming vectors in ℝ³.
*/
public transformVec3(other: Vec3): Vec3 {

@@ -196,3 +220,3 @@ return Vec3.of(

// Returns true iff this = other ± fuzz
/** Returns true iff this = other ± fuzz */
public eq(other: Mat33, fuzz: number = 0): boolean {

@@ -213,8 +237,12 @@ for (let i = 0; i < 3; i++) {

⎣ ${this.c1},\t ${this.c2},\t ${this.c3}\t ⎦
`.trimRight();
`.trimEnd().trimStart();
}
// result[0] = top left element
// result[1] = element at row zero, column 1
// ...
/**
* ```
* result[0] = top left element
* result[1] = element at row zero, column 1
* ...
* ```
*/
public toArray(): number[] {

@@ -228,3 +256,3 @@ return [

// Constructs a 3x3 translation matrix (for translating Vec2s)
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
public static translation(amount: Vec2): Mat33 {

@@ -279,3 +307,3 @@ // When transforming Vec2s by a 3x3 matrix, we give the input

// Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
/** Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. */
public static fromCSSMatrix(cssString: string): Mat33 {

@@ -282,0 +310,0 @@ if (cssString === '' || cssString === 'none') {

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

// [onlyAbsCommands]: True if we should avoid converting absolute coordinates to relative offsets -- such
// conversions can lead to smaller output strings, but also take time.
// @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
// conversions can lead to smaller output strings, but also take time.
public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands: boolean = true): string {

@@ -381,3 +381,3 @@ const result: string[] = [];

// TODO: Support a larger subset of SVG paths.
// TODO: Support s,t shorthands.
// TODO: Support `s`,`t` commands shorthands.
public static fromString(pathString: string): Path {

@@ -384,0 +384,0 @@ // See the MDN reference:

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

// An object that can be converted to a Rect2.
interface RectTemplate {
/** An object that can be converted to a Rect2. */
export interface RectTemplate {
x: number;

@@ -9,0 +9,0 @@ y: number;

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

// @packageDocumentation @internal
// Clean up stringified numbers

@@ -2,0 +4,0 @@ const cleanUpNumber = (text: string) => {

// A vector with three components. Can also be used to represent a two-component vector
/**
* A vector with three components. Can also be used to represent a two-component vector.
*
* A `Vec3` is immutable.
*/
export default class Vec3 {

@@ -12,3 +16,3 @@ private constructor(

// Returns the x, y components of this
/** Returns the x, y components of this. */
public get xy(): { x: number; y: number } {

@@ -26,3 +30,3 @@ // Useful for APIs that behave differently if .z is present.

// Returns this' [idx]th component
/** Returns this' `idx`th component. For example, `Vec3.of(1, 2, 3).at(1) → 2`. */
public at(idx: number): number {

@@ -36,3 +40,3 @@ if (idx === 0) return this.x;

// Alias for this.magnitude
/** Alias for this.magnitude. */
public length(): number {

@@ -50,3 +54,7 @@ return this.magnitude();

// Return this' angle in the XY plane (treats this as a Vec2)
/**
* Return this' angle in the XY plane (treats this as a Vec2).
*
* This is equivalent to `Math.atan2(vec.y, vec.x)`.
*/
public angle(): number {

@@ -56,2 +64,7 @@ return Math.atan2(this.y, this.x);

/**
* Returns a unit vector in the same direction as this.
*
* If `this` has zero length, the resultant vector has `NaN` components.
*/
public normalized(): Vec3 {

@@ -62,2 +75,3 @@ const norm = this.magnitude();

/** @returns A copy of `this` multiplied by a scalar. */
public times(c: number): Vec3 {

@@ -90,4 +104,6 @@ return Vec3.of(this.x * c, this.y * c, this.z * c);

// Returns a vector orthogonal to this. If this is a Vec2, returns [this] rotated
// 90 degrees counter-clockwise.
/**
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
* 90 degrees counter-clockwise.
*/
public orthog(): Vec3 {

@@ -102,3 +118,3 @@ // If parallel to the z-axis

// Returns this plus a vector of length [distance] in [direction]
/** Returns this plus a vector of length `distance` in `direction`. */
public extend(distance: number, direction: Vec3): Vec3 {

@@ -108,3 +124,3 @@ return this.plus(direction.normalized().times(distance));

// Returns a vector [fractionTo] of the way to target from this.
/** Returns a vector `fractionTo` of the way to target from this. */
public lerp(target: Vec3, fractionTo: number): Vec3 {

@@ -114,4 +130,18 @@ return this.times(1 - fractionTo).plus(target.times(fractionTo));

// [zip] Maps a component of this and a corresponding component of
// [other] to a component of the output vector.
/**
* `zip` Maps a component of this and a corresponding component of
* `other` to a component of the output vector.
*
* @example
* ```
* const a = Vec3.of(1, 2, 3);
* const b = Vec3.of(0.5, 2.1, 2.9);
*
* const zipped = a.zip(b, (aComponent, bComponent) => {
* return Math.min(aComponent, bComponent);
* });
*
* console.log(zipped.toString()); // → Vec(0.5, 2, 2.9)
* ```
*/
public zip(

@@ -127,3 +157,10 @@ other: Vec3, zip: (componentInThis: number, componentInOther: number)=> number

// Returns a vector with each component acted on by [fn]
/**
* Returns a vector with each component acted on by `fn`.
*
* @example
* ```
* console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
* ```
*/
public map(fn: (component: number, index: number)=> number): Vec3 {

@@ -139,4 +176,15 @@ return Vec3.of(

// [fuzz] The maximum difference between two components for this and [other]
// to be considered equal.
/**
* [fuzz] The maximum difference between two components for this and [other]
* to be considered equal.
*
* @example
* ```
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 100); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 0.1); // → false
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3.01); // → true
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
* ```
*/
public eq(other: Vec3, fuzz: number): boolean {

@@ -143,0 +191,0 @@ for (let i = 0; i < 3; i++) {

@@ -14,3 +14,3 @@ import { Point2, Vec2 } from './math/Vec2';

// Provides a snapshot containing information about a pointer. A Pointer
// object is immutable --- it will not be updated when the pointer's information changes.
// object is immutable — it will not be updated when the pointer's information changes.
export default class Pointer {

@@ -35,3 +35,3 @@ private constructor(

// Numeric timestamp (milliseconds, as from (new Date).getTime())
// Numeric timestamp (milliseconds, as from `(new Date).getTime()`)
public readonly timeStamp: number,

@@ -41,2 +41,3 @@ ) {

// Creates a Pointer from a DOM event.
public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer {

@@ -43,0 +44,0 @@ const screenPos = Vec2.of(evt.offsetX, evt.offsetY);

@@ -14,3 +14,3 @@ /* @jest-environment jsdom */

});
const state = cache.getSharedState();
const state = cache['sharedState'];

@@ -17,0 +17,0 @@ const record = new CacheRecord(() => {}, state);

@@ -14,2 +14,5 @@ import Mat33 from '../../math/Mat33';

// For debugging
public allocCount: number = 0;
public constructor(

@@ -50,2 +53,3 @@ private onBeforeDeallocCallback: BeforeDeallocCallback|null,

this.lastUsedCycle = this.cacheState.currentRenderingCycle;
this.allocCount ++;
}

@@ -52,0 +56,0 @@

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

import { BeforeDeallocCallback, PartialCacheState } from './types';
import { BeforeDeallocCallback, CacheProps, CacheState } from './types';
import CacheRecord from './CacheRecord';
import Rect2 from '../../math/Rect2';
const debugMode = false;

@@ -10,5 +11,5 @@ export class CacheRecordManager {

private maxCanvases: number;
private cacheState: CacheState;
public constructor(private readonly cacheState: PartialCacheState) {
const cacheProps = cacheState.props;
public constructor(cacheProps: CacheProps) {
this.maxCanvases = Math.ceil(

@@ -20,2 +21,6 @@ // Assuming four components per pixel:

public setSharedState(state: CacheState) {
this.cacheState = state;
}
public allocCanvas(drawTo: Rect2, onDealloc: BeforeDeallocCallback): CacheRecord {

@@ -25,6 +30,3 @@ if (this.cacheRecords.length < this.maxCanvases) {

onDealloc,
{
...this.cacheState,
recordManager: this,
},
this.cacheState,
);

@@ -34,7 +36,31 @@ record.setRenderingRegion(drawTo);

if (debugMode) {
console.log('[Cache] Cache spaces used: ', this.cacheRecords.length, ' of ', this.maxCanvases);
}
return record;
} else {
const lru = this.getLeastRecentlyUsedRecord()!;
if (debugMode) {
console.log(
'[Cache] Re-alloc. Times allocated: ', lru.allocCount,
'\nLast used cycle: ', lru.getLastUsedCycle(),
'\nCurrent cycle: ', this.cacheState.currentRenderingCycle
);
}
lru.realloc(onDealloc);
lru.setRenderingRegion(drawTo);
if (debugMode) {
console.log(
'[Cache] Now re-alloc\'d. Last used cycle: ', lru.getLastUsedCycle()
);
console.assert(
lru['cacheState'] === this.cacheState,
'[Cache] Unequal cache states! cacheState should be a shared object!'
);
}
return lru;

@@ -41,0 +67,0 @@ }

@@ -7,6 +7,6 @@ import { ImageNode } from '../../EditorImage';

import { CacheRecordManager } from './CacheRecordManager';
import { CacheProps, CacheState, PartialCacheState } from './types';
import { CacheProps, CacheState } from './types';
export default class RenderingCache {
private partialSharedState: PartialCacheState;
private sharedState: CacheState;
private recordManager: CacheRecordManager;

@@ -16,14 +16,9 @@ private rootNode: RenderingCacheNode|null;

public constructor(cacheProps: CacheProps) {
this.partialSharedState = {
this.recordManager = new CacheRecordManager(cacheProps);
this.sharedState = {
props: cacheProps,
currentRenderingCycle: 0,
};
this.recordManager = new CacheRecordManager(this.partialSharedState);
}
public getSharedState(): CacheState {
return {
...this.partialSharedState,
recordManager: this.recordManager,
};
this.recordManager.setSharedState(this.sharedState);
}

@@ -33,6 +28,6 @@

const visibleRect = viewport.visibleRect;
this.partialSharedState.currentRenderingCycle ++;
this.sharedState.currentRenderingCycle ++;
// If we can't use the cache,
if (!this.partialSharedState.props.isOfCorrectType(screenRenderer)) {
if (!this.sharedState.props.isOfCorrectType(screenRenderer)) {
image.render(screenRenderer, visibleRect);

@@ -44,3 +39,3 @@ return;

// Adjust the node so that it has the correct aspect ratio
const res = this.partialSharedState.props.blockResolution;
const res = this.sharedState.props.blockResolution;

@@ -50,3 +45,3 @@ const topLeft = visibleRect.topLeft;

new Rect2(topLeft.x, topLeft.y, res.x, res.y),
this.getSharedState()
this.sharedState
);

@@ -62,3 +57,3 @@ }

const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
if (visibleLeaves.length > this.sharedState.props.minComponentsToUseCache) {
this.rootNode!.renderItems(screenRenderer, [ image ], viewport);

@@ -65,0 +60,0 @@ } else {

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

// CacheRecordManager relies on a partial copy of the shared state. Thus,
// we need to separate partial/non-partial state.
export interface PartialCacheState {
export interface CacheState {
currentRenderingCycle: number;
props: CacheProps;
}
export interface CacheState extends PartialCacheState {
recordManager: CacheRecordManager;
}

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

/**
* Handles `HTMLCanvasElement`s (or other drawing surfaces if being used) used to display the editor's contents.
*
* @example
* ```
* const editor = new Editor(document.body);
* const w = editor.display.width;
* const h = editor.display.height;
* const center = Vec2.of(w / 2, h / 2);
* const colorAtCenter = editor.display.getColorAt(center);
* ```
*
* @packageDocumentation
*/
import AbstractRenderer from './renderers/AbstractRenderer';

@@ -26,2 +41,3 @@ import CanvasRenderer from './renderers/CanvasRenderer';

/** @internal */
public constructor(

@@ -64,3 +80,3 @@ private editor: Editor, mode: RenderingMode, private parent: HTMLElement|null

blockResolution: cacheBlockResolution,
cacheSize: 500 * 500 * 4 * 220,
cacheSize: 500 * 500 * 4 * 150,
maxScale: 1.5,

@@ -80,5 +96,7 @@ minComponentsPerCache: 45,

// Returns the visible width of the display (e.g. how much
// space the display's element takes up in the x direction
// in the DOM).
/**
* @returns the visible width of the display (e.g. how much
* space the display's element takes up in the x direction
* in the DOM).
*/
public get width(): number {

@@ -92,2 +110,3 @@ return this.dryInkRenderer.displaySize().x;

/** @internal */
public getCache() {

@@ -97,2 +116,6 @@ return this.cache;

/**
* @returns the color at the given point on the dry ink renderer, or `null` if `screenPos`
* is not on the display.
*/
public getColorAt = (_screenPos: Point2): Color4|null => {

@@ -178,2 +201,6 @@ return null;

/**
* Rerenders the text-based display.
* The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
*/
public rerenderAsText() {

@@ -188,3 +215,7 @@ this.textRenderer.clear();

// Clears the drawing surfaces and otherwise prepares for a rerender.
/**
* Clears the drawing surfaces and otherwise prepares for a rerender.
*
* @returns the dry ink renderer.
*/
public startRerender(): AbstractRenderer {

@@ -198,2 +229,6 @@ this.resizeSurfacesCallback?.();

/**
* If `draftMode`, the dry ink renderer is configured to render
* low-quality output.
*/
public setDraftMode(draftMode: boolean) {

@@ -203,2 +238,3 @@ this.dryInkRenderer.setDraftMode(draftMode);

/** @internal */
public getDryInkRenderer(): AbstractRenderer {

@@ -208,2 +244,6 @@ return this.dryInkRenderer;

/**
* @returns The renderer used for showing action previews (e.g. an unfinished stroke).
* The `wetInkRenderer`'s surface is stacked above the `dryInkRenderer`'s.
*/
public getWetInkRenderer(): AbstractRenderer {

@@ -213,3 +253,3 @@ return this.wetInkRenderer;

// Re-renders the contents of the wetInkRenderer onto the dryInkRenderer
/** Re-renders the contents of the wetInkRenderer onto the dryInkRenderer. */
public flatten() {

@@ -216,0 +256,0 @@ this.flattenCallback?.();

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

this.minSquareCurveApproxDist = 1;
this.minRenderSizeBothDimens = 1;
this.minRenderSizeBothDimens = 0.5;
this.minRenderSizeAnyDimen = 0;

@@ -70,0 +70,0 @@ }

@@ -46,2 +46,3 @@ import Editor from '../Editor';

// @internal
public setupColorPickers() {

@@ -48,0 +49,0 @@ const closePickerOverlay = document.createElement('div');

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

// Returns [ input, container ].
// Returns [ color input, input container ].
export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListener): [ HTMLInputElement, HTMLElement ] => {

@@ -14,0 +14,0 @@ const colorInputContainer = document.createElement('span');

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

// @internal @packageDocumentation
import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';

@@ -2,0 +4,0 @@ import { makeFreehandLineBuilder } from '../../components/builders/FreehandLineBuilder';

@@ -19,3 +19,2 @@

export enum PanZoomMode {

@@ -22,0 +21,0 @@ OneFingerTouchGestures = 0x1,

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

interface PenStyle {
export interface PenStyle {
color: Color4;

@@ -38,3 +38,12 @@ thickness: number;

const minPressure = 0.3;
const pressure = Math.max(pointer.pressure ?? 1.0, minPressure);
let pressure = Math.max(pointer.pressure ?? 1.0, minPressure);
if (!isFinite(pressure)) {
console.warn('Non-finite pressure!', pointer);
pressure = minPressure;
}
console.assert(isFinite(pointer.canvasPos.length()), 'Non-finite canvas position!');
console.assert(isFinite(pointer.screenPos.length()), 'Non-finite screen position!');
console.assert(isFinite(pointer.timeStamp), 'Non-finite timeStamp on pointer!');
return {

@@ -41,0 +50,0 @@ pos: pointer.canvasPos,

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

// @internal @packageDocumentation
import Color4 from '../Color4';

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

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

import { ToolType } from './ToolController';
import SerializableCommand from '../commands/SerializableCommand';

@@ -128,2 +129,3 @@ const handleScreenSize = 30;

// @internal
class Selection {

@@ -136,3 +138,3 @@ public region: Rect2;

private transform: Mat33;
private transformationCommands: Command[];
private transformationCommands: SerializableCommand[];

@@ -236,3 +238,3 @@ public constructor(

private computeTransformCommands() {
private computeTransformCommands(): SerializableCommand[] {
return this.selectedElems.map(elem => {

@@ -282,14 +284,29 @@ return elem.transformBy(this.transform);

this.editor.dispatch(new Selection.ApplyTransformationCommand(
this, currentTransfmCommands, fullTransform, inverseTransform, deltaBoxRotation
this, currentTransfmCommands, fullTransform, deltaBoxRotation
));
}
private static ApplyTransformationCommand = class extends Command {
static {
SerializableCommand.register('selection-tool-transform', (json: any, editor) => {
// The selection box is lost when serializing/deserializing. No need to store box rotation
const guiBoxRotation = 0;
const fullTransform: Mat33 = new Mat33(...(json.transform as [
number, number, number,
number, number, number,
number, number, number,
]));
const commands = (json.commands as any[]).map(data => SerializableCommand.deserialize(data, editor));
return new this.ApplyTransformationCommand(null, commands, fullTransform, guiBoxRotation);
});
}
private static ApplyTransformationCommand = class extends SerializableCommand {
public constructor(
private selection: Selection,
private currentTransfmCommands: Command[],
private fullTransform: Mat33, private inverseTransform: Mat33,
private selection: Selection|null,
private currentTransfmCommands: SerializableCommand[],
private fullTransform: Mat33,
private deltaBoxRotation: number,
) {
super();
super('selection-tool-transform');
}

@@ -299,21 +316,32 @@

// Approximate the new selection
this.selection.region = this.selection.region.transformedBoundingBox(this.fullTransform);
this.selection.boxRotation += this.deltaBoxRotation;
this.selection.updateUI();
if (this.selection) {
this.selection.region = this.selection.region.transformedBoundingBox(this.fullTransform);
this.selection.boxRotation += this.deltaBoxRotation;
this.selection.updateUI();
}
await editor.asyncApplyCommands(this.currentTransfmCommands, updateChunkSize);
this.selection.recomputeRegion();
this.selection.updateUI();
this.selection?.recomputeRegion();
this.selection?.updateUI();
}
public async unapply(editor: Editor) {
this.selection.region = this.selection.region.transformedBoundingBox(this.inverseTransform);
this.selection.boxRotation -= this.deltaBoxRotation;
this.selection.updateUI();
if (this.selection) {
this.selection.region = this.selection.region.transformedBoundingBox(this.fullTransform.inverse());
this.selection.boxRotation -= this.deltaBoxRotation;
this.selection.updateUI();
}
await editor.asyncUnapplyCommands(this.currentTransfmCommands, updateChunkSize);
this.selection.recomputeRegion();
this.selection.updateUI();
this.selection?.recomputeRegion();
this.selection?.updateUI();
}
protected serializeToJSON() {
return {
commands: this.currentTransfmCommands.map(command => command.serialize()),
transform: this.fullTransform.toArray(),
};
}
public description(_editor: Editor, localizationTable: EditorLocalization) {

@@ -320,0 +348,0 @@ return localizationTable.transformedElements(this.currentTransfmCommands.length);

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

import Color4 from './Color4';
import Command from './commands/Command';

@@ -95,8 +96,13 @@

ToolUpdated,
UndoRedoStackUpdated,
CommandDone,
CommandUndone,
ObjectAdded,
ViewportChanged,
DisplayResized,
ColorPickerToggled,
ColorPickerColorSelected
ColorPickerColorSelected,
}

@@ -136,2 +142,12 @@

export interface CommandDoneEvent {
readonly kind: EditorEventType.CommandDone;
readonly command: Command;
}
export interface CommandUndoneEvent {
readonly kind: EditorEventType.CommandUndone;
readonly command: Command;
}
export interface ColorPickerToggled {

@@ -149,4 +165,4 @@ readonly kind: EditorEventType.ColorPickerToggled;

| EditorViewportChangedEvent | DisplayResizedEvent
| EditorUndoStackUpdated
| ColorPickerToggled| ColorPickerColorSelected;
| EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent
| ColorPickerToggled | ColorPickerColorSelected;

@@ -153,0 +169,0 @@

@@ -12,2 +12,3 @@ import Editor from './Editor';

// @internal
public constructor(

@@ -41,3 +42,8 @@ private readonly editor: Editor,

this.redoStack = [];
this.fireUpdateEvent();
this.editor.notifier.dispatch(EditorEventType.CommandDone, {
kind: EditorEventType.CommandDone,
command,
});
}

@@ -52,4 +58,9 @@

this.announceUndoCallback(command);
this.fireUpdateEvent();
this.editor.notifier.dispatch(EditorEventType.CommandUndone, {
kind: EditorEventType.CommandUndone,
command,
});
}
this.fireUpdateEvent();
}

@@ -63,4 +74,9 @@

this.announceRedoCallback(command);
this.fireUpdateEvent();
this.editor.notifier.dispatch(EditorEventType.CommandDone, {
kind: EditorEventType.CommandDone,
command,
});
}
this.fireUpdateEvent();
}

@@ -67,0 +83,0 @@

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

// @internal
public constructor(private notifier: EditorNotifier) {

@@ -90,2 +91,3 @@ this.resetTransform(Mat33.identity);

// @internal
public updateScreenSize(screenSize: Vec2) {

@@ -112,3 +114,3 @@ this.screenRect = this.screenRect.resizedTo(screenSize);

// Updates the transformation directly. Using transformBy is preferred.
// Updates the transformation directly. Using `transformBy` is preferred.
// [newTransform] should map from canvas coordinates to screen coordinates.

@@ -144,2 +146,3 @@ public resetTransform(newTransform: Mat33 = Mat33.identity) {

// Returns the size of one screen pixel in canvas units.
public getSizeOfPixelOnCanvas(): number {

@@ -154,3 +157,3 @@ return 1/this.getScaleFactor();

// Rounds the given [point] to a multiple of 10 such that it is within [tolerance] of
// Rounds the given `point` to a multiple of 10 such that it is within `tolerance` of
// its original location. This is useful for preparing data for base-10 conversion.

@@ -157,0 +160,0 @@ public static roundPoint<T extends Point2|number>(

@@ -27,4 +27,7 @@ {

"**/*.test.ts",
"__mocks__/*"
"__mocks__/*",
// Output files
"./dist/**"
],
}

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