Comparing version 0.1.12 to 0.2.0
@@ -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. |
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(); |
219
package.json
{ | ||
"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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1057736
298
22682
211
22