UNPKG

js-draw

Version:

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

170 lines (169 loc) 6.27 kB
import { Vec2, Rect2, Color4, PathCommandType } from '@js-draw/math'; import Stroke from '../Stroke.mjs'; import Viewport from '../../Viewport.mjs'; import { StrokeSmoother } from '../util/StrokeSmoother.mjs'; import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs'; /** * Creates a stroke builder that draws freehand lines. * * Example: * [[include:doc-pages/inline-examples/changing-pen-types.md]] */ export const makeFreehandLineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => { // Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if // less than ±1 px from the curve. const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3; const minSmoothingDist = viewport.getSizeOfPixelOnCanvas(); return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport); }); // Handles stroke smoothing and creates Strokes from user/stylus input. export default class FreehandLineBuilder { constructor(startPoint, minFitAllowed, maxFitAllowed, viewport) { this.startPoint = startPoint; this.minFitAllowed = minFitAllowed; this.viewport = viewport; this.isFirstSegment = true; this.parts = []; this.widthAverageNumSamples = 1; this.curveFitter = new StrokeSmoother(startPoint, minFitAllowed, maxFitAllowed, (curve) => this.addCurve(curve)); this.averageWidth = startPoint.width; this.bbox = new Rect2(this.startPoint.pos.x, this.startPoint.pos.y, 0, 0); } getBBox() { return this.bbox; } getRenderingStyle() { return { fill: Color4.transparent, stroke: { color: this.startPoint.color, width: this.roundDistance(this.averageWidth), }, }; } previewCurrentPath() { const path = this.parts.slice(); const commands = [...path, ...this.curveToPathCommands(this.curveFitter.preview())]; const startPoint = this.startPoint.pos; return { startPoint, commands, style: this.getRenderingStyle(), }; } previewFullPath() { const preview = this.previewCurrentPath(); if (preview) { return [preview]; } return null; } previewStroke() { const pathPreview = this.previewFullPath(); if (pathPreview) { return new Stroke(pathPreview); } return null; } preview(renderer) { const paths = this.previewFullPath(); if (paths) { const approxBBox = this.viewport.visibleRect; renderer.startObject(approxBBox); for (const path of paths) { renderer.drawPath(path); } renderer.endObject(); } } build() { this.curveFitter.finalizeCurrentCurve(); return this.previewStroke(); } getMinFit() { let minFit = Math.min(this.minFitAllowed, this.averageWidth / 3); if (minFit < 1e-10) { minFit = this.minFitAllowed; } return minFit; } roundPoint(point) { const minFit = this.getMinFit(); return Viewport.roundPoint(point, minFit); } roundDistance(dist) { const minFit = this.getMinFit(); return Viewport.roundPoint(dist, minFit); } curveToPathCommands(curve) { // Case where no points have been added if (!curve) { // Don't create a circle around the initial point if the stroke has more than one point. if (!this.isFirstSegment) { return []; } // Make the circle small -- because of the stroke style, we'll be drawing a stroke around it. const width = Viewport.roundPoint(this.averageWidth / 10, Math.min(this.minFitAllowed, this.averageWidth / 10)); const center = this.roundPoint(this.startPoint.pos); // Start on the right, cycle clockwise: // | // ----- ← // | // Draw a circle-ish shape around the start point return [ { kind: PathCommandType.QuadraticBezierTo, controlPoint: center.plus(Vec2.of(width, width)), // Bottom of the circle // | // ----- // | // ↑ endPoint: center.plus(Vec2.of(0, width)), }, { kind: PathCommandType.QuadraticBezierTo, controlPoint: center.plus(Vec2.of(-width, width)), endPoint: center.plus(Vec2.of(-width, 0)), }, { kind: PathCommandType.QuadraticBezierTo, controlPoint: center.plus(Vec2.of(-width, -width)), endPoint: center.plus(Vec2.of(0, -width)), }, { kind: PathCommandType.QuadraticBezierTo, controlPoint: center.plus(Vec2.of(width, -width)), endPoint: center.plus(Vec2.of(width, 0)), }, ]; } const result = []; if (this.isFirstSegment) { result.push({ kind: PathCommandType.MoveTo, point: this.roundPoint(curve.startPoint), }); } result.push({ kind: PathCommandType.QuadraticBezierTo, controlPoint: this.roundPoint(curve.controlPoint), endPoint: this.roundPoint(curve.endPoint), }); return result; } addCurve(curve) { const parts = this.curveToPathCommands(curve); this.parts.push(...parts); if (this.isFirstSegment) { this.isFirstSegment = false; } } addPoint(newPoint) { this.curveFitter.addPoint(newPoint); this.widthAverageNumSamples++; this.averageWidth = (this.averageWidth * (this.widthAverageNumSamples - 1)) / this.widthAverageNumSamples + newPoint.width / this.widthAverageNumSamples; } }