UNPKG

js-draw

Version:

Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.

185 lines (184 loc) 6.95 kB
import { Vec2, Path, PathCommandType } from '@js-draw/math'; import { stylesEqual } from '../RenderingStyle.mjs'; import { pathToRenderable } from '../RenderablePathSpec.mjs'; /** * Abstract base class for renderers. * * @see {@link EditorImage.render} */ export default class AbstractRenderer { constructor(viewport) { this.viewport = viewport; // If null, this' transformation is linked to the Viewport this.selfTransform = null; this.transformStack = []; this.objectLevel = 0; this.currentPaths = null; } /** * this.canvasToScreen, etc. should be used instead of the corresponding * methods on `Viewport`, because the viewport may not accurately reflect * what is rendered. */ getViewport() { return this.viewport; } setDraftMode(_draftMode) { } flushPath() { if (!this.currentPaths) { return; } let lastStyle = null; for (const path of this.currentPaths) { const { startPoint, commands, style } = path; if (!lastStyle || !stylesEqual(lastStyle, style)) { if (lastStyle) { this.endPath(lastStyle); } this.beginPath(startPoint); lastStyle = style; } else { this.moveTo(startPoint); } for (const command of commands) { if (command.kind === PathCommandType.LineTo) { this.lineTo(command.point); } else if (command.kind === PathCommandType.MoveTo) { this.moveTo(command.point); } else if (command.kind === PathCommandType.CubicBezierTo) { this.traceCubicBezierCurve(command.controlPoint1, command.controlPoint2, command.endPoint); } else if (command.kind === PathCommandType.QuadraticBezierTo) { this.traceQuadraticBezierCurve(command.controlPoint, command.endPoint); } } } if (lastStyle) { this.endPath(lastStyle); } this.currentPaths = []; } /** * Draws a styled path. If within an object started by {@link startObject}, * the resultant path may not be visible until {@link endObject} is called. */ drawPath(path) { // If we're being called outside of an object, // we can't delay rendering if (this.objectLevel === 0 || this.currentPaths === null) { this.currentPaths = [path]; this.flushPath(); this.currentPaths = null; } else { // Otherwise, don't render paths all at once. This prevents faint lines between // segments of the same stroke from being visible. this.currentPaths.push(path); } } // Strokes a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]. // This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`. drawRect(rect, lineWidth, lineFill) { const path = Path.fromRect(rect, lineWidth); this.drawPath(pathToRenderable(path, lineFill)); } /** Draws a filled rectangle. */ fillRect(rect, fill) { const path = Path.fromRect(rect); this.drawPath(pathToRenderable(path, { fill })); } /** * This should be called whenever a new object is being drawn. * * @param _boundingBox The bounding box of the object to be drawn. * @param _clip Whether content outside `_boundingBox` should be drawn. Renderers * that override this method are not required to support `_clip`. */ startObject(_boundingBox, _clip) { if (this.objectLevel > 0) { this.flushPath(); } this.currentPaths = []; this.objectLevel++; } /** * Notes the end of an object. * @param _loaderData - a map from strings to JSON-ifyable objects * and contains properties attached to the object by whatever loader loaded the image. This * is used to preserve attributes not supported by js-draw when loading/saving an image. * Renderers may ignore this. * * @param _objectTags - a list of labels (e.g. `className`s) to be attached to the object. * Renderers may ignore this. */ endObject(_loaderData, _objectTags) { // Render the paths all at once this.flushPath(); this.currentPaths = null; this.objectLevel--; if (this.objectLevel < 0) { throw new Error('More objects have ended than have been started (negative object nesting level)!'); } } getNestingLevel() { return this.objectLevel; } // Returns true iff other can be rendered onto this without data loss. canRenderFromWithoutDataLoss(_other) { return false; } // MUST throw if other and this are not of the same base class. renderFromOtherOfSameType(_renderTo, other) { throw new Error(`Unable to render from ${other}: Not implemented`); } // Set a transformation to apply to things before rendering, // replacing the viewport's transform. setTransform(transform) { this.selfTransform = transform; } pushTransform(transform) { // Draw all pending paths that used the previous transform (if any). this.flushPath(); this.transformStack.push(this.selfTransform); this.setTransform(this.getCanvasToScreenTransform().rightMul(transform)); } popTransform() { if (this.transformStack.length === 0) { throw new Error('Unable to pop more transforms than have been pushed!'); } // Draw all pending paths that used the old transform (if any): this.flushPath(); this.setTransform(this.transformStack.pop() ?? null); } // Get the matrix that transforms a vector on the canvas to a vector on this' // rendering target. getCanvasToScreenTransform() { if (this.selfTransform) { return this.selfTransform; } return this.viewport.canvasToScreenTransform; } canvasToScreen(vec) { return this.getCanvasToScreenTransform().transformVec2(vec); } getSizeOfCanvasPixelOnScreen() { return this.getCanvasToScreenTransform().transformVec3(Vec2.unitX).length(); } /** * @internal */ overrideVisibleRect(rect) { this.visibleRectOverride = rect; } // Returns the region in canvas space that is visible within the viewport this // canvas is rendering to. // // Note that in some cases this might not be the same as the `visibleRect` given // to components in their `render` method. getVisibleRect() { return this.visibleRectOverride ?? this.viewport.visibleRect; } }