UNPKG

js-draw

Version:

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

169 lines (168 loc) 6.72 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.visualEquivalent = exports.simplifyPathToFullScreenOrEmpty = exports.pathToRenderable = exports.pathFromRenderable = void 0; const math_1 = require("@js-draw/math"); /** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */ const pathFromRenderable = (renderable) => { if (renderable.path) { return renderable.path; } return new math_1.Path(renderable.startPoint, renderable.commands); }; exports.pathFromRenderable = pathFromRenderable; /** * Converts `path` into a format that can be rendered (by passing to a {@link Stroke} constructor * or directly to an {@link AbstractRenderer.drawPath}). */ const pathToRenderable = (path, style) => { return { startPoint: path.startPoint, style, commands: path.parts, path, }; }; exports.pathToRenderable = pathToRenderable; /** * Fills the optional `path` field in `RenderablePathSpec` * with `path` if not already filled */ const pathIncluded = (renderablePath, path) => { if (renderablePath.path) { return renderablePath; } return { ...renderablePath, path, }; }; /** * Tries to simplify the given path to a fullscreen rectangle. * Returns `null` on failure. * * @internal */ const simplifyPathToFullScreenOrEmpty = (renderablePath, visibleRect, options = { fastCheck: true, expensiveCheck: true, }) => { const path = (0, exports.pathFromRenderable)(renderablePath); const strokeWidth = renderablePath.style.stroke?.width ?? 0; const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0; const styledPathBBox = path.bbox.grownBy(strokeWidth); // Are we close enough to the path that it fills the entire screen? const isOnlyStrokedAndCouldFillScreen = onlyStroked && strokeWidth > visibleRect.maxDimension && styledPathBBox.containsRect(visibleRect); if (options.fastCheck && isOnlyStrokedAndCouldFillScreen && renderablePath.style.stroke) { const strokeRadius = strokeWidth / 2; // Are we completely within the stroke? // Do a fast, but with many false negatives, check. for (const point of path.startEndPoints()) { // If within the strokeRadius of any point if (visibleRect.isWithinRadiusOf(strokeRadius, point)) { return { rectangle: visibleRect, path: (0, exports.pathToRenderable)(math_1.Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color, }), fullScreen: true, }; } } } // Try filtering again, but with slightly more expensive checks if (options.expensiveCheck && isOnlyStrokedAndCouldFillScreen && renderablePath.style.stroke && strokeWidth > visibleRect.maxDimension * 3) { const signedDist = path.signedDistance(visibleRect.center, strokeWidth / 2); const margin = strokeWidth / 6; if (signedDist < -visibleRect.maxDimension / 2 - margin) { return { path: (0, exports.pathToRenderable)(math_1.Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color, }), rectangle: visibleRect, fullScreen: true, }; } else if (signedDist > visibleRect.maxDimension / 2 + margin) { return { path: (0, exports.pathToRenderable)(math_1.Path.empty, { fill: math_1.Color4.transparent }), rectangle: math_1.Rect2.empty, fullScreen: false, }; } } return null; }; exports.simplifyPathToFullScreenOrEmpty = simplifyPathToFullScreenOrEmpty; /** * @returns a Path that, when rendered, looks roughly equivalent to the given path. */ const visualEquivalent = (renderablePath, visibleRect) => { const path = (0, exports.pathFromRenderable)(renderablePath); const strokeWidth = renderablePath.style.stroke?.width ?? 0; const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0; const styledPathBBox = path.bbox.grownBy(strokeWidth); let rectangleSimplification = (0, exports.simplifyPathToFullScreenOrEmpty)(renderablePath, visibleRect, { fastCheck: true, expensiveCheck: false, }); if (rectangleSimplification) { return rectangleSimplification.path; } // Scale the expanded rect --- the visual equivalent is only close for huge strokes. const expandedRect = visibleRect .grownBy(strokeWidth) .transformedBoundingBox(math_1.Mat33.scaling2D(4, visibleRect.center)); // TODO: Handle simplifying very small paths. if (expandedRect.containsRect(styledPathBBox)) { return pathIncluded(renderablePath, path); } const parts = []; let startPoint = path.startPoint; for (const part of path.parts) { const partBBox = math_1.Path.computeBBoxForSegment(startPoint, part).grownBy(strokeWidth); let endPoint; if (part.kind === math_1.PathCommandType.LineTo || part.kind === math_1.PathCommandType.MoveTo) { endPoint = part.point; } else { endPoint = part.endPoint; } const intersectsVisible = partBBox.intersects(visibleRect); if (intersectsVisible) { // TODO: Can we trim parts of paths that intersect the visible rectangle? parts.push(part); } else if (onlyStroked || part.kind === math_1.PathCommandType.MoveTo) { // We're stroking (not filling) and the path doesn't intersect the bounding box. // Don't draw it, but preserve the endpoints. parts.push({ kind: math_1.PathCommandType.MoveTo, point: endPoint, }); } else { // Otherwise, we may be filling. Try to roughly preserve the filled region. parts.push({ kind: math_1.PathCommandType.LineTo, point: endPoint, }); } startPoint = endPoint; } const newPath = new math_1.Path(path.startPoint, parts); const newStyle = renderablePath.style; rectangleSimplification = (0, exports.simplifyPathToFullScreenOrEmpty)(renderablePath, visibleRect, { fastCheck: false, expensiveCheck: true, }); if (rectangleSimplification) { return rectangleSimplification.path; } return (0, exports.pathToRenderable)(newPath, newStyle); }; exports.visualEquivalent = visualEquivalent;