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