UNPKG

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
"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(); } }