Parse

File Parse split-v2.js

This runs the server-side parser and regenerates the documentation tree for this source file.

Source

            /*
title: Split
files:
    functions/clamp.js
---

_Split_ a point, line or curve into many smaller points.

    const p = new Point({radius: 100})
    let points = p.split(4)

Lines and curves share one `split(count, options)` signature:

    // both ends included
    curve.split(20)                          
    // ends excluded
    curve.split(20, { inner: true })         
    // equal arc-length spacing
    curve.split(20, { even: true })          
    // a point every 10 units
    curve.split({ every: 10 })               
    // lerp radius/rotation a -> b
    curve.split(20, { hinted: true })        
    // face a point
    curve.split(20, { lookAt: point })       
    // rotate each point (radians)
    curve.split(20, { angle: Math.PI / 2 })  

A bare number as the second argument is treated as `angle`.
*/

/* splitToPointList(point, count).pen.indicators(ctx)*/
const splitToPointList = function(point, count, radius, rotation, angle=undefined) {
    let p1 = point
    let r = radius || point.radius
    p1 = p1.subtract(r)
    let _radius = radius || p1.radius
    // p1.rotation = rotation || point.rotation
    /* p1 does not recieve r.radians - use the original rads.
    rotation ==0 is falsy.*/
    let rot = rotation == undefined? point.radians: rotation
    return PointList.from(
                getPolyDistributedPoints(count, p1, _radius, rot, angle)
                // splitRadius(p1, count)
            )
}


const lerp = (x, y, a) => x * (1 - a) + y * a;


const animatedSegmentOffset = function(delta, speed, segmentSize, scale=.005) {
    let travel = delta * speed * scale
    // Keep animation inside one segment width/angle to avoid hard clipping pops.
    return ((travel % segmentSize) + segmentSize) % segmentSize
}


const EMPTY_SPLIT_OPTIONS = Object.freeze({})

const splitArgs = function(count, options) {
    /* Normalise `split(count, options)` arguments.
    Accepts `split({ every: 10 })` and the legacy `split(count, angle)`. */
    if(count != null && typeof count == 'object') {
        options = count
        count = options.count
    }
    if(typeof options == 'number') {
        return [count, { angle: options }]
    }
    return [count, options || EMPTY_SPLIT_OPTIONS]
}


const splitFractions = function(count, inner) {
    /* Linear 0..1 fractions along a path for `count` points.
    Ends are included unless `inner`; a single point sits at the middle. */
    if(!(count >= 1)) { return [] }
    if(count == 1) { return [.5] }
    let r = new Array(count)
    if(inner) {
        let step = 1 / (count + 1)
        for (var i = 0; i < count; i++) { r[i] = (i + 1) * step }
        return r
    }
    let step = 1 / (count - 1)
    for (var i = 0; i < count; i++) { r[i] = i * step }
    return r
}


const everyFractions = function(total, every, inner) {
    /* Fractions for a point every `every` units along a `total` length. */
    let r = []
    if(!(every > 0) || !(total > 0)) { return r }
    for (let d = inner ? every : 0; d <= total; d += every) {
        if(inner && d >= total) { break }
        r.push(d / total)
    }
    return r
}


function bLerp(a,b,t){
    return (1-t)*a+t*b;
}

// AWESOME! https://acegikmo.com/bezier/
// https://acegikmo.medium.com/the-ever-so-lovely-bézier-curve-eb27514da3bf
function lerpV2(a,b,t){
    return {
        x: bLerp(a.x,b.x,t),
        y: bLerp(a.y,b.y,t)
    };
}


function get_bezier_point(p0, p1, p2, p3, t ){
    var a = lerpV2(p0,p1,t);
    var b = lerpV2(p1,p2,t);
    var c = lerpV2(p2,p3,t);
    var d = lerpV2(a,b,t);
    var e = lerpV2(b,c,t);
    return lerpV2(d,e,t);
}


function get_bezier_derivative(p0, p1, p2, p3, t) {
    var oneMinusT = 1 - t;

    var dx = 3 * oneMinusT * oneMinusT * (p1.x - p0.x) +
             6 * oneMinusT * t * (p2.x - p1.x) +
             3 * t * t * (p3.x - p2.x);

    var dy = 3 * oneMinusT * oneMinusT * (p1.y - p0.y) +
             6 * oneMinusT * t * (p2.y - p1.y) +
             3 * t * t * (p3.y - p2.y);

    return { dx, dy };
}


class BezierArcTable {
    /* Cumulative chord lengths at `samples` evenly spaced `t` values,
    used to map an arc-length distance back to `t`. Reused across frames;
    `update()` only resamples when the control points or sample count change. */
    constructor() {
        this.key = new Float64Array(8)
        this.samples = 0
        this.lengths = null
        this.total = 0
        this.cursor = 0
    }

    update(p0, p1, p2, p3, samples) {
        let k = this.key
        if(samples == this.samples
            && k[0] == p0.x && k[1] == p0.y && k[2] == p1.x && k[3] == p1.y
            && k[4] == p2.x && k[5] == p2.y && k[6] == p3.x && k[7] == p3.y) {
            return this
        }
        k[0] = p0.x; k[1] = p0.y; k[2] = p1.x; k[3] = p1.y
        k[4] = p2.x; k[5] = p2.y; k[6] = p3.x; k[7] = p3.y

        if(samples != this.samples) {
            this.samples = samples
            this.lengths = new Float64Array(samples + 1)
        }

        let lengths = this.lengths
        let prevX = p0.x, prevY = p0.y
        let total = 0
        for (var i = 1; i <= samples; i++) {
            let p = get_bezier_point(p0, p1, p2, p3, i / samples)
            total += Math.hypot(p.x - prevX, p.y - prevY)
            lengths[i] = total
            prevX = p.x; prevY = p.y
        }
        this.total = total
        return this
    }

    tAt(distance) {
        /* `t` for an arc-length `distance`. Queries are expected in ascending
        order; call `rewind()` before a new walk. */
        let lengths = this.lengths
        let samples = this.samples
        let seg = this.cursor
        while(seg < samples - 1 && lengths[seg + 1] < distance) { seg++ }
        this.cursor = seg
        let segLength = lengths[seg + 1] - lengths[seg]
        let f = segLength > 0 ? (distance - lengths[seg]) / segLength : 0
        return clamp((seg + f) / samples, 0, 1)
    }

    rewind() {
        this.cursor = 0
        return this
    }
}


const lerpRadius = function(a, b, v) {
    /* Process the width from the _first_ to the _last_ of a line.*/
    // let av = ((asLast.radius - asFirst.radius) * (i/l))+asFirst.radius
    return ((b - a) * v) + a
}

const radiusManual = function(a, b, i) {}


class PointListSplit {
    /* Buildin split functionality for a pointlist. */
    segment(index=0, count=10) {
        /* Split a segment */
        let pl = this.parent
        // return splitToPointList(
        //         point
        //         , count
        //         , point.radius
        //         , point.radians + outerAngle
        //         , angle
        //     )
    }
}


Polypoint.head.installFunctions('Point', {
  
    /* A "split" function to divide the point circumference to many points.
    Return a list of points.

        let pointList = point.split(4)
        let pointList = point.split(4, { angle: 0, outerAngle: Math.PI })

    A `point.project()` is the same as `point.split(1)`
    */
    split(count, options=undefined, outerAngle=0) {
        let point = this
        let angle = options
        if(options != null && typeof options == 'object') {
            angle = options.angle
            outerAngle = options.outerAngle == undefined ? outerAngle : options.outerAngle
        }
        return splitToPointList(point, count, point.radius, point.radians + outerAngle, angle)
    }

    , _splitTick: 0
    , splitAnimated(count, angle=undefined, speed=.2, delta=this._splitTick) {
        let point = this
        let safeCount = Math.max(1, count)

        this._splitTick += 1
        let segmentAngle = (Math.PI * 2) / safeCount
        let outerAngle = animatedSegmentOffset(delta, speed, segmentAngle)

        return splitToPointList(
            point,
            count,
            point.radius,
            point.radians + outerAngle,
            angle
        )
    }
});



Polypoint.head.installFunctions('BezierCurve', {
    /* Split the curve into points.

        curve.split(20)                          // both ends included
        curve.split(20, { inner: true })         // ends excluded
        curve.split(20, { even: true })          // equal arc-length spacing
        curve.split({ every: 10 })               // a point every 10 units
        curve.split(20, { hinted: true })        // lerp radius/rotation a -> b
        curve.split(20, { lookAt: point })       // face a point
        curve.split(20, { angle: rads })         // rotate each point

    Rotation precedence: `lookAt` > `hinted` > curve tangent. `angle` is
    added in all cases. `samples` overrides the arc-length resolution used
    by `even` and `every`.
    */
    split(count, options=undefined) {
        [count, options] = splitArgs(count, options)
        let { angle=0, inner=false, even=false, every, hinted=false, lookAt, samples } = options

        let p0 = this.a
        let p3 = this.b
        let [p1, p2] = this.getControlPoints()
        let r = new PointList

        let byArc = even || every > 0
        let table = byArc ? this._arcTable(p0, p1, p2, p3, samples).rewind() : undefined
        let fractions = every > 0
            ? everyFractions(table.total, every, inner)
            : splitFractions(count, inner)

        for (var i = 0; i < fractions.length; i++) {
            let u = fractions[i]
            let t = byArc ? table.tAt(u * table.total) : u

            let p = new Point(get_bezier_point(p0, p1, p2, p3, t))
            if(hinted) { p.radius = lerp(p0.radius, p3.radius, u) }

            if(lookAt) {
                p.lookAt(lookAt)
                p.radians += angle
            } else if(hinted) {
                p.rotation = lerp(p0.rotation, p3.rotation, u)
                p.radians += angle
            } else {
                let { dx, dy } = get_bezier_derivative(p0, p1, p2, p3, t)
                p.radians = Math.atan2(-dx, dy) + angle
            }
            r.push(p)
        }
        return r
    }

    , arcLength(samples=undefined) {
        /* Approximate length of the curve, in units. */
        let [p1, p2] = this.getControlPoints()
        return this._arcTable(this.a, p1, p2, this.b, samples).total
    }

    , _arcTable(p0, p1, p2, p3, samples=undefined) {
        if(samples == undefined) {
            // Control polygon length bounds the arc; ~1 sample per 2 units,
            // rounded to 64s so small movements don't reallocate the table.
            let polyLength = Math.hypot(p1.x - p0.x, p1.y - p0.y)
                           + Math.hypot(p2.x - p1.x, p2.y - p1.y)
                           + Math.hypot(p3.x - p2.x, p3.y - p2.y)
            samples = clamp(Math.ceil(polyLength / 128) * 64, 64, 1024)
        }
        let table = this._bezierArcTable || (this._bezierArcTable = new BezierArcTable)
        return table.update(p0, p1, p2, p3, samples)
    }

    /* Aliases for the option flags. */
    , splitInner(count, angle=0) { return this.split(count, { inner: true, angle }) }
    , splitEven(count, angle=0) { return this.split(count, { even: true, angle }) }
    , splitExact(every, angle=0) { return this.split({ every, angle }) }
    , splitHinted(count) { return this.split(count, { hinted: true }) }

    , _splitTick: 0
    , splitAnimated(count, angle=undefined, speed=.2, delta=this._splitTick) {

        let p0 = this.a
        let p3 = this.b
        let [p1, p2] = this.getControlPoints()

        let r = new PointList

        let safeCount = Math.max(1, count)
        let splitVal = 1 / safeCount
        let rotationOffset = angle == undefined ? 0 : angle

        this._splitTick += 1
        let _s = animatedSegmentOffset(delta, speed, splitVal)

        for (var i = 0; i < count+1; i++) {
            let t = i * splitVal + _s
            if(t > 1 || t < 0) { continue }

            let p = new Point(get_bezier_point(p0, p1, p2, p3, t))
            let { dx, dy } = get_bezier_derivative(p0, p1, p2, p3, t)
            p.radians = Math.atan2(-dx, dy) + rotationOffset
            r.push(p)
        }

        return r
    }
});


Polypoint.head.installFunctions('Line', {
    /* Split the line into points. Same options as `BezierCurve.split`
    (`even` is meaningless on a straight line and ignored).

        line.split(5)                     // both ends included
        line.split(5, { inner: true })    // ends excluded
        line.split({ every: 10 })

    `angle` is in degrees relative to the line direction; when omitted the
    points keep the default rotation.
    */
    split(count, options=undefined) {
        [count, options] = splitArgs(count, options)
        let { angle, inner=false, every, hinted=false, lookAt } = options

        let a = this.a
        let b = this.b
        let r = new PointList

        let fractions = every > 0
            ? everyFractions(a.distanceTo(b), every, inner)
            : splitFractions(count, inner)

        let degs = undefined
        if(angle != undefined) {
            degs = calculateAngle(a, b) - angle
        }

        for (var i = 0; i < fractions.length; i++) {
            let u = fractions[i]
            let p = new Point(lerp(a.x, b.x, u), lerp(a.y, b.y, u), a.radius, degs)
            if(hinted) {
                p.radius = lerp(a.radius, b.radius, u)
                p.rotation = lerp(a.rotation, b.rotation, u)
            }
            if(lookAt) { p.lookAt(lookAt) }
            r.push(p)
        }
        return r
    }

    , splitInner(count, angle=undefined) { return this.split(count, { inner: true, angle }) }
    , splitExact(every, angle=undefined) { return this.split({ every, angle }) }

    , _splitTick: 0
    , splitAnimated(count, angle=undefined, speed=.2, delta=this._splitTick) {

        let a = this.a
        let b = this.b
        let r = new PointList

        let safeCount = Math.max(1, count)
        let splitVal = 1 / safeCount
        let degs = undefined
        if(angle != undefined) {
            degs = calculateAngle(a, b) - angle;
        }

        this._splitTick += 1
        let _s = animatedSegmentOffset(delta, speed, splitVal)

        for (var i = 0; i < count+1; i++) {
            let slideOffset = i * (splitVal) + _s;
            if(slideOffset > 1 || slideOffset < 0) { continue }
            r.push(new Point(
                    lerp(a.x, b.x, slideOffset)
                    , lerp(a.y, b.y, slideOffset)
                    , a.radius
                    , degs
                )
            )
        }
        return r
    }
});

copy