js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
166 lines (165 loc) • 7.84 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_1 = require("@js-draw/math");
const makeShapeFitAutocorrect = (sourceFactory) => {
return (startPoint, viewport) => {
return new ShapeFitBuilder(sourceFactory, startPoint, viewport);
};
};
exports.default = makeShapeFitAutocorrect;
const makeLineTemplate = (startPoint, points, _bbox) => {
const templatePoints = [startPoint, points[points.length - 1]];
return { points: templatePoints };
};
const makeRectangleTemplate = (_startPoint, _points, bbox) => {
return { points: [...bbox.corners, bbox.corners[0]] };
};
class ShapeFitBuilder {
constructor(sourceFactory, startPoint, viewport) {
this.sourceFactory = sourceFactory;
this.startPoint = startPoint;
this.viewport = viewport;
this.builder = sourceFactory(startPoint, viewport);
this.points = [startPoint];
}
getBBox() {
return this.builder.getBBox();
}
build() {
return this.builder.build();
}
preview(renderer) {
this.builder.preview(renderer);
}
addPoint(point) {
this.points.push(point);
this.builder.addPoint(point);
}
async autocorrectShape() {
// Use screen points so that autocorrected shapes rotate with the screen.
const startPoint = this.viewport.canvasToScreen(this.startPoint.pos);
const points = this.points.map((point) => this.viewport.canvasToScreen(point.pos));
const bbox = math_1.Rect2.bboxOf(points);
const snappedStartPoint = this.viewport.canvasToScreen(this.viewport.snapToGrid(this.startPoint.pos));
const snappedPoints = this.points.map((point) => this.viewport.canvasToScreen(this.viewport.snapToGrid(point.pos)));
const snappedBBox = math_1.Rect2.bboxOf(snappedPoints);
// Only fit larger shapes
if (bbox.maxDimension < 32) {
return null;
}
const maxError = Math.min(30, bbox.maxDimension / 4);
// Create templates
const templates = [
{
...makeLineTemplate(snappedStartPoint, snappedPoints, snappedBBox),
toleranceMultiplier: 0.5,
},
makeLineTemplate(startPoint, points, bbox),
{
...makeRectangleTemplate(snappedStartPoint, snappedPoints, snappedBBox),
toleranceMultiplier: 0.6,
},
makeRectangleTemplate(startPoint, points, bbox),
];
// Find a good fit fit
const selectTemplate = (maximumAllowedError) => {
for (const template of templates) {
const templatePoints = template.points;
// Maximum square error to accept the template
const acceptMaximumSquareError = maximumAllowedError * maximumAllowedError * (template.toleranceMultiplier ?? 1);
// Gets the point at index, wrapping the the start of the template if
// outside the array of points.
const templateAt = (index) => {
while (index < 0) {
index += templatePoints.length;
}
index %= templatePoints.length;
return templatePoints[index];
};
let closestToFirst = null;
let closestToFirstSqrDist = Infinity;
let templateStartIndex = 0;
// Find the closest point to the startPoint
for (let i = 0; i < templatePoints.length; i++) {
const current = templatePoints[i];
const currentSqrDist = current.squareDistanceTo(startPoint);
if (!closestToFirst || currentSqrDist < closestToFirstSqrDist) {
closestToFirstSqrDist = currentSqrDist;
closestToFirst = current;
templateStartIndex = i;
}
}
// Walk through the points and find the maximum error
let maximumSqrError = 0;
let templateIndex = templateStartIndex;
for (const point of points) {
let minimumCurrentSqrError = Infinity;
let minimumErrorAtIndex = templateIndex;
const windowRadius = 6;
for (let i = -windowRadius; i <= windowRadius; i++) {
const index = templateIndex + i;
const prevTemplatePoint = templateAt(index - 1);
const currentTemplatePoint = templateAt(index);
const nextTemplatePoint = templateAt(index + 1);
const prevToCurrent = new math_1.LineSegment2(prevTemplatePoint, currentTemplatePoint);
const currentToNext = new math_1.LineSegment2(currentTemplatePoint, nextTemplatePoint);
const prevToCurrentDist = prevToCurrent.distance(point);
const nextToCurrentDist = currentToNext.distance(point);
const error = Math.min(prevToCurrentDist, nextToCurrentDist);
const squareError = error * error;
if (squareError < minimumCurrentSqrError) {
minimumCurrentSqrError = squareError;
minimumErrorAtIndex = index;
}
}
templateIndex = minimumErrorAtIndex;
maximumSqrError = Math.max(minimumCurrentSqrError, maximumSqrError);
if (maximumSqrError > acceptMaximumSquareError) {
break;
}
}
if (maximumSqrError < acceptMaximumSquareError) {
return templatePoints;
}
}
return null;
};
const template = selectTemplate(maxError);
if (!template) {
return null;
}
const lastDataPoint = this.points[this.points.length - 1];
const startWidth = this.startPoint.width;
const endWidth = lastDataPoint.width;
const startColor = this.startPoint.color;
const endColor = lastDataPoint.color;
const startTime = this.startPoint.time;
const endTime = lastDataPoint.time;
const templateIndexToStrokeDataPoint = (index) => {
const prevPoint = template[Math.max(0, Math.floor(index))];
const nextPoint = template[Math.min(Math.ceil(index), template.length - 1)];
const point = prevPoint.lerp(nextPoint, index - Math.floor(index));
const fractionToEnd = index / template.length;
return {
pos: this.viewport.screenToCanvas(point),
width: startWidth * (1 - fractionToEnd) + endWidth * fractionToEnd,
color: startColor.mix(endColor, fractionToEnd),
time: startTime * (1 - fractionToEnd) + endTime * fractionToEnd,
};
};
const builder = this.sourceFactory(templateIndexToStrokeDataPoint(0), this.viewport);
// Prevent the original builder from doing stroke smoothing if the template is short
// enough to likely have sharp corners.
const preventSmoothing = template.length < 10;
for (let i = 0; i < template.length; i++) {
if (preventSmoothing) {
builder.addPoint(templateIndexToStrokeDataPoint(i - 0.001));
}
builder.addPoint(templateIndexToStrokeDataPoint(i));
if (preventSmoothing) {
builder.addPoint(templateIndexToStrokeDataPoint(i + 0.001));
}
}
return builder.build();
}
}