js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
177 lines (176 loc) • 6.76 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeFreehandLineBuilder = void 0;
const math_1 = require("@js-draw/math");
const Stroke_1 = __importDefault(require("../Stroke"));
const Viewport_1 = __importDefault(require("../../Viewport"));
const StrokeSmoother_1 = require("../util/StrokeSmoother");
const makeShapeFitAutocorrect_1 = __importDefault(require("./autocorrect/makeShapeFitAutocorrect"));
/**
* Creates a stroke builder that draws freehand lines.
*
* Example:
* [[include:doc-pages/inline-examples/changing-pen-types.md]]
*/
exports.makeFreehandLineBuilder = (0, makeShapeFitAutocorrect_1.default)((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.
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_1.StrokeSmoother(startPoint, minFitAllowed, maxFitAllowed, (curve) => this.addCurve(curve));
this.averageWidth = startPoint.width;
this.bbox = new math_1.Rect2(this.startPoint.pos.x, this.startPoint.pos.y, 0, 0);
}
getBBox() {
return this.bbox;
}
getRenderingStyle() {
return {
fill: math_1.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_1.default(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_1.default.roundPoint(point, minFit);
}
roundDistance(dist) {
const minFit = this.getMinFit();
return Viewport_1.default.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_1.default.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: math_1.PathCommandType.QuadraticBezierTo,
controlPoint: center.plus(math_1.Vec2.of(width, width)),
// Bottom of the circle
// |
// -----
// |
// ↑
endPoint: center.plus(math_1.Vec2.of(0, width)),
},
{
kind: math_1.PathCommandType.QuadraticBezierTo,
controlPoint: center.plus(math_1.Vec2.of(-width, width)),
endPoint: center.plus(math_1.Vec2.of(-width, 0)),
},
{
kind: math_1.PathCommandType.QuadraticBezierTo,
controlPoint: center.plus(math_1.Vec2.of(-width, -width)),
endPoint: center.plus(math_1.Vec2.of(0, -width)),
},
{
kind: math_1.PathCommandType.QuadraticBezierTo,
controlPoint: center.plus(math_1.Vec2.of(width, -width)),
endPoint: center.plus(math_1.Vec2.of(width, 0)),
},
];
}
const result = [];
if (this.isFirstSegment) {
result.push({
kind: math_1.PathCommandType.MoveTo,
point: this.roundPoint(curve.startPoint),
});
}
result.push({
kind: math_1.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;
}
}
exports.default = FreehandLineBuilder;