modules_spline_spline.js

import Line from "@pencil.js/line";
import Position from "@pencil.js/position";
import { equals } from "@pencil.js/math";

/**
 * @module Spline
 */

/**
 * Spline class
 * <br><img src="./media/examples/spline.png" alt="spline demo"/>
 * @class
 * @extends {module:Line}
 */
export default class Spline extends Line {
    /**
     * Spline constructor
     * @param {PositionDefinition} positionDefinition - First point
     * @param {Array<PositionDefinition>|PositionDefinition} points - Set of points to go through or a single target point
     * @param {Number} [tension=Spline.defaultTension] - Ratio of tension between points (0 means straight line, can take any value, but with weird results above 1)
     * @param {LineOptions} [options] - Drawing options
     */
    constructor (positionDefinition, points, tension = Spline.defaultTension, options) {
        super(positionDefinition, points, options);

        /**
         * @type {Number}
         */
        this.tension = tension;
    }

    /**
     * Draw the spline
     * @param {Path2D} path - Current drawing path
     * @return {Spline} Itself
     */
    trace (path) {
        if (this.points.length === 1 || equals(this.tension, 0)) {
            super.trace(path);
        }
        else {
            path.moveTo(0, 0);
            const correction = this.options.absolute ? this.position : new Position();
            Spline.splineThrough(path, [new Position(), ...this.points], this.tension, correction);
        }
        return this;
    }

    /**
     * @inheritDoc
     */
    toJSON () {
        const { tension } = this;
        return {
            ...super.toJSON(),
            tension,
        };
    }

    /**
     * @inheritDoc
     * @param {Object} definition - Spline definition
     * @return {Spline}
     */
    static from (definition) {
        return new Spline(definition.position, definition.points, definition.tension, definition.options);
    }

    /**
     * Default ratio of tension
     * @type {Number}
     */
    static get defaultTension () {
        return 0.2;
    }

    /**
     * Draw a spline through points using a tension (first point should be current position)
     * @param {Path2D|CanvasRenderingContext2D} path - Current drawing path
     * @param {Array<PositionDefinition>} points - Points to use (need at least 2 points)
     * @param {Number} [tension=Spline.defaultTension] - Ratio of tension
     * @param {PositionDefinition} [_correction] - Apply position correction to all points (used by the library)
     */
    static splineThrough (path, points, tension = Spline.defaultTension, _correction) {
        if (points.length < 2) {
            throw new RangeError(`Need at least 2 points to spline, but only ${points.length} given.`);
        }

        const shift = Position.from(_correction);

        if (points.length === 2) {
            const target = Position.from(points[1]);
            path.lineTo(target.x - shift.x, target.y - shift.y);
            return;
        }

        const positions = points.map(point => Position.from(point).clone().subtract(shift));

        for (let i = 1; i < positions.length; ++i) {
            const slice = [...new Array(4)].map((_, n) => positions[i + n - 2]);
            // eslint-disable-next-line no-use-before-define
            const [before, after] = getControlPoint(slice, tension);

            path.bezierCurveTo(
                before.x, before.y,
                after.x, after.y,
                positions[i].x, positions[i].y,
            );
        }
    }
}

/**
 * Returns control points for a point in a spline (needs before and after, 3 points in total)
 * @param {Array<Position>} points - 4 points to use (before, one, two, after)
 * @param {Number} [tension=Spline.defaultTension] - Ratio of tension
 * @return {Array<Position>}
 */
function getControlPoint (points, tension = Spline.defaultTension) {
    const [previous, current, target, next] = points;

    let diffBefore;
    let diffAfter;
    if (previous) {
        diffBefore = target.clone().subtract(previous).multiply(tension);
    }
    if (next) {
        diffAfter = current.clone().subtract(next).multiply(tension);
    }
    return [
        points[1].clone().add(diffBefore),
        points[2].clone().add(diffAfter),
    ];
}