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