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