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
JavaScript
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;
}
}