UNPKG

js-draw

Version:

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

122 lines (121 loc) 4.34 kB
import { Rect2, Color4, PathCommandType, Vec2, LineSegment2, } from '@js-draw/math'; import Stroke from '../Stroke.mjs'; import Viewport from '../../Viewport.mjs'; import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs'; /** * Creates a freehand line builder that creates strokes from line segments * rather than Bézier curves. * * Example: * [[include:doc-pages/inline-examples/changing-pen-types.md]] */ export const makePolylineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => { // Fit to a value slightly smaller than the pixel size. A larger value can // cause the stroke to appear jagged at some zoom levels. const minFit = viewport.getSizeOfPixelOnCanvas() * 0.65; return new PolylineBuilder(initialPoint, minFit, viewport); }); export default class PolylineBuilder { constructor(startPoint, minFitAllowed, viewport) { this.minFitAllowed = minFitAllowed; this.viewport = viewport; this.parts = []; this.widthAverageNumSamples = 1; this.lastLineSegment = null; this.averageWidth = startPoint.width; this.startPoint = { ...startPoint, pos: this.roundPoint(startPoint.pos), }; this.lastPoint = this.startPoint.pos; this.bbox = new Rect2(this.startPoint.pos.x, this.startPoint.pos.y, 0, 0); this.parts = [ { kind: PathCommandType.MoveTo, point: this.startPoint.pos, }, ]; } getBBox() { return this.bbox.grownBy(this.averageWidth); } getRenderingStyle() { return { fill: Color4.transparent, stroke: { color: this.startPoint.color, width: this.roundDistance(this.averageWidth), }, }; } previewCurrentPath() { const startPoint = this.startPoint.pos; const commands = [...this.parts]; // TODO: For now, this is necesary for the path to be visible. if (commands.length <= 1) { commands.push({ kind: PathCommandType.LineTo, point: startPoint.plus(Vec2.of(this.averageWidth / 4, 0)), }); } return { startPoint, commands, style: this.getRenderingStyle(), }; } previewFullPath() { return [this.previewCurrentPath()]; } 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() { return new Stroke(this.previewFullPath()); } getMinFit() { let minFit = Math.min(this.minFitAllowed, this.averageWidth / 4); 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); } addPoint(newPoint) { this.widthAverageNumSamples++; this.averageWidth = (this.averageWidth * (this.widthAverageNumSamples - 1)) / this.widthAverageNumSamples + newPoint.width / this.widthAverageNumSamples; const roundedPoint = this.roundPoint(newPoint.pos); if (!roundedPoint.eq(this.lastPoint)) { // If almost exactly in the same line as the previous if (this.lastLineSegment && this.lastLineSegment.direction.dot(roundedPoint.minus(this.lastPoint).normalized()) > 0.997) { this.parts.pop(); this.lastPoint = this.lastLineSegment.p1; } this.parts.push({ kind: PathCommandType.LineTo, point: this.roundPoint(newPoint.pos), }); this.bbox = this.bbox.grownToPoint(roundedPoint); this.lastLineSegment = new LineSegment2(this.lastPoint, roundedPoint); this.lastPoint = roundedPoint; } } }