Parse

File Parse cone.js

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

Source

            /*
Project and render a point-owned cone shape.

    Object.assign(point.cone.settings, {
        distance: 200,
        container: stage.dimensions
    })

    let cone = point.cone.renderData()
    point.cone.fill(ctx, cone)
    point.cone.renderOutline(ctx, cone)

Reusable settings helpers in this file can also be used directly when the
class wrapper is not needed.
*/


const CONE_DEFAULT_SETTINGS = {
    distance: undefined
    , viewportLimit: false
    , innerOffset: 0
    , inner: false
    , curve: 0
    , invertCurve: false
    , innerCurve: 0
    , outline: false
    // , fillColor: 'rebeccapurple'
    // , color: 'rebeccapurple'
    , outlineColor: 'purple'
    , outlineWidth: 2
}


const CONE_OUTLINE_MODES = {
    true: {
        enabled: true
        , leading: true
        , trailing: true
        , inner: true
        , outer: true
    }
    , edge: {
        enabled: true
        , leading: true
        , trailing: true
        , inner: false
        , outer: false
    }
    , outer: {
        enabled: true
        , leading: true
        , trailing: true
        , inner: false
        , outer: true
    }
}


function resolveConeSettings(point, baseSettings={}, settings={}) {
    if(typeof settings === 'number') {
        settings = { distance: settings }
    }

    let conf = Object.assign({}, baseSettings, settings)

    let coneDeg = Number(conf.coneDeg)
    if(Number.isNaN(coneDeg)) {
        coneDeg = Number(conf.cone)
    }

    conf.coneDeg = Number.isNaN(coneDeg)? point.coneDeg: coneDeg
    conf.cone = conf.coneDeg

    let rotation = Number(conf.rotation)
    conf.rotation = Number.isNaN(rotation)? point.rotation: rotation

    return conf
}


function getConeOutlineSettings(settings={}) {
    let outline = settings.outline
    let defaults = {
        enabled: false
        , leading: false
        , trailing: false
        , inner: false
        , outer: false
        , color: settings.outlineColor
        , width: settings.outlineWidth
    }

    if(outline && typeof outline === 'object') {
        let result = Object.assign({}, defaults, outline)
        if(outline.enabled == undefined) {
            result.enabled = Boolean(result.leading || result.trailing || result.inner || result.outer)
        }
        return result
    }

    let selected = CONE_OUTLINE_MODES[outline] || {}
    return Object.assign({}, defaults, selected)
}


function normalizeConeCurveValue(value, invert=false) {
    let curve = Number(value)
    curve = Number.isNaN(curve)? 0: curve
    return curve * (invert? -1: 1)
}


function withinCone(point, cone) {
    if(point == undefined || cone == undefined) {
        return false
    }

    let coneTool = cone.hitPolygon != undefined
        ? cone
        : cone.cone 

    if(coneTool == undefined || coneTool.hitPolygon == undefined) {
        return false
    }

    let polygon = coneTool.hitPolygon()
    if(polygon == undefined || polygon.length < 3) {
        return false
    }

    const x = point.x
        , y = point.y;
    let inside = false;

    for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
        const xi = polygon[i].x
            , yi = polygon[i].y;
        const xj = polygon[j].x
            , yj = polygon[j].y;

        const intersect = (
                    (yi > y) !== (yj > y)
                ) && (
                    x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
                );

        if (intersect) inside = !inside;
    }

    return inside;
}


class PointCone {

    constructor(point, settings={}) {
        this.parent = point
        this.settings = Object.assign({}, CONE_DEFAULT_SETTINGS, settings)
    }

    update(settings) {
        /* Apply changes for the persistent info*/
        
        return Object.assign(this.settings, settings)
    }

    resolveSettings(settings={}) {
        return resolveConeSettings(this.parent, this.settings, settings)
    }

    renderData(settings={}) {
        if(this.isRenderData(settings)) {
            return settings
        }

        let conf = this.resolveSettings(settings)
        let points = this.points(conf)
        let hasInnerEdge = this.getInnerOffset(conf) !== 0

        return {
            originPoint: this.parent
            , settings: conf
            , points
            , startAnchor: points[0]
            , endAnchor: hasInnerEdge? points.last(): points[0]
            , edgePoints: hasInnerEdge? points.slice(1, -1): points.slice(1)
            , hasInnerEdge
            , hasOuterEdge: conf.distance != undefined
            , curves: this.getCurveSettings(conf)
        }
    }

    points(settings={}) {
        if(this.isRenderData(settings)) {
            return settings.points
        }

        let point = this.parent
        let conf = this.resolveSettings(settings)
        let startAngle = conf.rotation - conf.coneDeg
        let endAngle = conf.rotation + conf.coneDeg
        let innerOffset = this.getInnerOffset(conf)
        let innerDistance = point.radius * innerOffset
        let origin = point.copy().update({radius: undefined})
        let innerStart = this.projectDistance(point, startAngle, innerDistance)
        let innerEnd = this.projectDistance(point, endAngle, innerDistance)
        let start = this.getEdgePoint(startAngle, conf)
        let end = this.getEdgePoint(endAngle, conf)
        let corners = this.getConeCorners(startAngle, endAngle, conf)
        let points = [innerOffset !== 0? innerStart: origin]

        this.pushUniquePoint(points, start)
        points.push(...corners)
        this.pushUniquePoint(points, end)

        if(innerOffset !== 0 && this.samePoint(points[points.length - 1], innerEnd) === false) {
            points.push(innerEnd)
        }

        return new PointList(...points).cast()
    }

    hitPolygon(settings={}) {
        let data = this.ensureRenderData(settings)
        let start = data.edgePoints[0]
        let end = data.edgePoints.last()
        let polygon = []

        if(start == undefined || end == undefined) {
            return new PointList().cast()
        }

        this.pushUniquePoint(polygon, data.startAnchor)
        this.pushUniquePoint(polygon, start)
        this.appendHitOuterPoints(polygon, data)

        if(this.samePoint(data.endAnchor, end) === false) {
            this.pushUniquePoint(polygon, data.endAnchor)
        }

        if(data.hasInnerEdge && data.curves.inner !== 0) {
            this.appendHitInnerPoints(polygon, data)
        }

        if(this.samePoint(polygon[0], polygon[polygon.length - 1])) {
            polygon.pop()
        }

        return new PointList(...polygon).cast()
    }

    fill(ctx, settings={}) {
        let data = this.tracePath(ctx, settings)
        if(data == false) { return false }

        let before = ctx.fillStyle
        ctx.fillStyle = (data.settings.fillColor || data.settings.color)
        ctx.fill()
        ctx.fillStyle = before
        
        return data
    }

    renderOutline(ctx, settings={}) {
        let data = this.ensureRenderData(settings)
        let outline = this.getOutlineSettings(data.settings)
        if(outline.enabled === false) {
            return false
        }

        if(this.traceOutline(ctx, data, outline) === false) {
            return false
        }

        let strokeStyle = ctx.strokeStyle
        let lineWidth = ctx.lineWidth

        if(outline.color != undefined) {
            ctx.strokeStyle = outline.color
        }

        if(outline.width != undefined) {
            ctx.lineWidth = outline.width
        }

        ctx.stroke()
        ctx.strokeStyle = strokeStyle
        ctx.lineWidth = lineWidth
        return data
    }

    tracePath(ctx, settings={}) {
        let data = this.ensureRenderData(settings)
        let start = data.edgePoints[0]
        let end = data.edgePoints.last()

        if(start == undefined || end == undefined) {
            return false
        }

        ctx.beginPath()
        ctx.moveTo(data.startAnchor.x, data.startAnchor.y)
        ctx.lineTo(start.x, start.y)

        this.drawEdgePath(ctx, data, data.curves.outer)

        if(this.samePoint(data.endAnchor, end) === false) {
            ctx.lineTo(data.endAnchor.x, data.endAnchor.y)
        }

        if(data.hasInnerEdge && data.curves.inner !== 0) {
            this.drawInnerCurve(ctx, data.originPoint, data.settings, data.endAnchor, data.startAnchor, data.curves.inner)
        }

        ctx.closePath()
        return data
    }

    traceOutline(ctx, settings={}, outline=undefined) {
        let data = this.ensureRenderData(settings)
        outline = outline || this.getOutlineSettings(data.settings)

        let start = data.edgePoints[0]
        let end = data.edgePoints.last()

        if(start == undefined || end == undefined) {
            return false
        }

        ctx.beginPath()

        if(outline.leading) {
            ctx.moveTo(data.startAnchor.x, data.startAnchor.y)
            ctx.lineTo(start.x, start.y)
        }

        if(outline.outer && data.hasOuterEdge) {
            ctx.moveTo(start.x, start.y)
            this.drawEdgePath(ctx, data, data.curves.outer)
        }

        if(outline.trailing && this.samePoint(data.endAnchor, end) === false) {
            ctx.moveTo(end.x, end.y)
            ctx.lineTo(data.endAnchor.x, data.endAnchor.y)
        }

        if(outline.inner && data.hasInnerEdge) {
            ctx.moveTo(data.endAnchor.x, data.endAnchor.y)

            if(data.curves.inner !== 0) {
                this.drawInnerCurve(ctx, data.originPoint, data.settings, data.endAnchor, data.startAnchor, data.curves.inner)
            } else {
                ctx.lineTo(data.startAnchor.x, data.startAnchor.y)
            }
        }

        return data
    }

    drawEdgePath(ctx, data, curveValue) {
        let start = data.edgePoints[0]
        let end = data.edgePoints.last()

        if(data.edgePoints.length < 2 || curveValue === 0) {
            return this.drawStraightEdge(ctx, data.edgePoints)
        }

        if(data.edgePoints.length === 2 && curveValue === 1 && this.hasEqualRadius(data.originPoint, start, end)) {
            return this.drawConeArc(ctx, data.originPoint, start, end)
        }

        if(data.edgePoints.length === 2) {
            let control = this.getConeCurveControl(data.originPoint, data.settings, start, end, curveValue)
            return ctx.quadraticCurveTo(control.x, control.y, end.x, end.y)
        }

        return this.drawQuadraticPath(ctx, data.edgePoints)
    }

    drawQuadraticPath(ctx, points) {
        let start = points[0]
        if(start == undefined) {
            return false
        }

        if(points.length === 1) {
            ctx.lineTo(start.x, start.y)
            return true
        }

        ctx.lineTo(start.x, start.y)

        for(let i = 1; i < points.length - 1; i++) {
            let current = points[i]
            let next = points[i + 1]
            let midpoint = current.midpoint(next)
            ctx.quadraticCurveTo(current.x, current.y, midpoint.x, midpoint.y)
        }

        let secondLast = points[points.length - 2]
        let end = points.last()
        ctx.quadraticCurveTo(secondLast.x, secondLast.y, end.x, end.y)
        return true
    }

    drawStraightEdge(ctx, points) {
        for(let point of points) {
            if(point == undefined) {
                continue
            }

            ctx.lineTo(point.x, point.y)
        }

        return true
    }

    appendHitOuterPoints(polygon, data) {
        let edgePoints = data.edgePoints
        let start = edgePoints[0]
        let end = edgePoints.last()
        let curveValue = data.curves.outer

        if(edgePoints.length < 2 || curveValue === 0) {
            for(let i = 1; i < edgePoints.length; i++) {
                this.pushUniquePoint(polygon, edgePoints[i])
            }
            return true
        }

        if(edgePoints.length === 2 && curveValue === 1 && this.hasEqualRadius(data.originPoint, start, end)) {
            return this.appendArcSamplePoints(polygon, data.originPoint, start, end)
        }

        if(edgePoints.length === 2) {
            let control = this.getConeCurveControl(data.originPoint, data.settings, start, end, curveValue)
            return this.appendQuadraticSamplePoints(polygon, start, control, end)
        }

        return this.appendQuadraticPathPoints(polygon, edgePoints)
    }

    appendHitInnerPoints(polygon, data) {
        let start = data.endAnchor
        let end = data.startAnchor
        let curveValue = data.curves.inner

        if(curveValue === 1 && this.hasEqualRadius(data.originPoint, start, end)) {
            return this.appendArcSamplePoints(polygon, data.originPoint, start, end, true)
        }

        let control = this.getConeCurveControl(data.originPoint, data.settings, start, end, curveValue)
        return this.appendQuadraticSamplePoints(polygon, start, control, end)
    }

    appendQuadraticPathPoints(polygon, pathPoints) {
        let start = pathPoints[0]
        if(start == undefined) {
            return false
        }

        if(pathPoints.length < 2) {
            return true
        }

        let segmentStart = start

        for(let i = 1; i < pathPoints.length - 1; i++) {
            let control = pathPoints[i]
            let next = pathPoints[i + 1]
            let midpoint = control.midpoint(next)
            this.appendQuadraticSamplePoints(polygon, segmentStart, control, midpoint)
            segmentStart = midpoint
        }

        let secondLast = pathPoints[pathPoints.length - 2]
        let end = pathPoints.last()
        return this.appendQuadraticSamplePoints(polygon, segmentStart, secondLast, end)
    }

    appendQuadraticSamplePoints(polygon, start, control, end, sampleCount=undefined) {
        let approxLength = start.distanceTo(control) + control.distanceTo(end)
        sampleCount = sampleCount || Math.max(8, Math.ceil(approxLength / 18))

        for(let i = 1; i <= sampleCount; i++) {
            let t = i / sampleCount
            let invT = 1 - t
            let x = (invT * invT * start.x)
                + (2 * invT * t * control.x)
                + (t * t * end.x)
            let y = (invT * invT * start.y)
                + (2 * invT * t * control.y)
                + (t * t * end.y)

            this.pushUniquePoint(polygon, new Point({x, y}))
        }

        return true
    }

    appendArcSamplePoints(polygon, originPoint, start, end, anticlockwise=false, sampleCount=undefined) {
        let startAngle = Math.atan2(start.y - originPoint.y, start.x - originPoint.x)
        let endAngle = Math.atan2(end.y - originPoint.y, end.x - originPoint.x)
        let sweep = endAngle - startAngle

        if(anticlockwise) {
            if(sweep >= 0) {
                sweep -= Math.PI * 2
            }
        } else if(sweep <= 0) {
            sweep += Math.PI * 2
        }

        let radius = originPoint.distanceTo(start)
        let approxLength = Math.abs(sweep) * radius
        sampleCount = sampleCount || Math.max(8, Math.ceil(approxLength / 18))

        for(let i = 1; i <= sampleCount; i++) {
            let angle = startAngle + (sweep * (i / sampleCount))
            let x = originPoint.x + (Math.cos(angle) * radius)
            let y = originPoint.y + (Math.sin(angle) * radius)
            this.pushUniquePoint(polygon, new Point({x, y}))
        }

        return true
    }

    getConeCurveControl(originPoint, settings, start, end, curveValue) {
        let radius = Math.max(originPoint.distanceTo(start), originPoint.distanceTo(end))
        let midpoint = start.midpoint(end)
        let midpointDistance = originPoint.distanceTo(midpoint)
        /* A full positive curve follows the matching circular arc passing
        through the edge points, then interpolates beyond that centerline. */
        let halfAngle = Math.abs(degToRad(settings.cone || 0))
        let circleDistance = radius / Math.max(.0001, Math.cos(halfAngle))
        /* Negative curves mirror that bulge through the edge midpoint so the
        same signed range can bend the cone inward. */
        let inverseDistance = Math.max(0, (midpointDistance * 2) - circleDistance)
        let targetDistance = curveValue > 0? circleDistance: inverseDistance
        let amount = Math.abs(curveValue)
        let distance = midpointDistance + ((targetDistance - midpointDistance) * amount)

        return this.projectDistance(originPoint, settings.rotation, distance)
    }

    drawInnerCurve(ctx, originPoint, settings, start, end, curveValue) {
        if(curveValue === 1 && this.hasEqualRadius(originPoint, start, end)) {
            return this.drawConeArc(ctx, originPoint, start, end, true)
        }

        let control = this.getConeCurveControl(originPoint, settings, start, end, curveValue)
        ctx.quadraticCurveTo(control.x, control.y, end.x, end.y)
        return true
    }

    drawConeArc(ctx, originPoint, start, end, anticlockwise=false) {
        let startAngle = Math.atan2(start.y - originPoint.y, start.x - originPoint.x)
        let endAngle = Math.atan2(end.y - originPoint.y, end.x - originPoint.x)
        let radius = originPoint.distanceTo(start)
        ctx.arc(originPoint.x, originPoint.y, radius, startAngle, endAngle, anticlockwise)
        return true
    }

    getEdgePoint(angle, settings={}) {
        let point = this.parent
        let conf = this.resolveSettings(settings)

        if(conf.inner === true) {
            let innerSettings = Object.assign({}, conf, { distance: point.radius })
            return this.getWallIntersection(angle, innerSettings)
        }

        return this.getWallIntersection(angle, conf)
    }

    getConeCorners(startAngle, endAngle, settings={}) {
        let conf = this.resolveSettings(settings)

        if(conf.distance != undefined || conf.inner === true) {
            return []
        }

        let point = this.parent
        let { width, height } = this.getContainer(conf)
        let sweep = endAngle - startAngle
        let corners = [
            new Point({x: 0, y: 0})
            , new Point({x: width, y: 0})
            , new Point({x: width, y: height})
            , new Point({x: 0, y: height})
        ]

        return corners
            .filter((corner) => {
                let angle = this.angleTo(point, corner)
                let offset = this.wrapAngle(angle - startAngle)
                return offset > 0 && offset < sweep
            })
            .sort((a, b) => {
                let aAngle = this.wrapAngle(this.angleTo(point, a) - startAngle)
                let bAngle = this.wrapAngle(this.angleTo(point, b) - startAngle)
                return aAngle - bAngle
            })
    }

    getWallIntersection(angle, settings={}) {
        let point = this.parent
        let conf = this.resolveSettings(settings)

        if(conf.distance != undefined && conf.viewportLimit !== true) {
            return this.projectDistance(point, angle, conf.distance)
        }

        let { width, height } = this.getContainer(conf)
        let radians = degToRad(angle)
        let dx = Math.cos(radians)
        let dy = Math.sin(radians)
        let hits = []

        if(dx > 0) {
            hits.push(this.hitAtX(point, width, dx, dy, conf))
        }

        if(dx < 0) {
            hits.push(this.hitAtX(point, 0, dx, dy, conf))
        }

        if(dy > 0) {
            hits.push(this.hitAtY(point, height, dx, dy, conf))
        }

        if(dy < 0) {
            hits.push(this.hitAtY(point, 0, dx, dy, conf))
        }

        let hit = hits
            .filter(Boolean)
            .sort((a, b) => a.distance - b.distance)[0]

        if(conf.distance == undefined) {
            return hit
        }

        if(hit == undefined || conf.distance < hit.distance) {
            return this.projectDistance(point, angle, conf.distance)
        }

        return hit
    }

    projectDistance(point, angle, distance) {
        let projected = point.project(distance, angle, false)
        projected.distance = distance
        return projected
    }

    hitAtX(point, wallX, dx, dy, settings={}) {
        let distance = (wallX - point.x) / dx
        let y = point.y + (dy * distance)
        let container = this.getContainer(settings)
        if(distance < 0 || y < 0 || y > container.height) {
            return undefined
        }

        return new Point({x: wallX, y, distance})
    }

    hitAtY(point, wallY, dx, dy, settings={}) {
        let distance = (wallY - point.y) / dy
        let x = point.x + (dx * distance)
        let container = this.getContainer(settings)
        if(distance < 0 || x < 0 || x > container.width) {
            return undefined
        }

        return new Point({x, y: wallY, distance})
    }

    getContainer(settings={}) {
        let conf = this.resolveSettings(settings)
        let container = conf.container
            || conf.dimensions
            || this.parent.stage?.dimensions
            || this.parent.dimensions

        if(container == undefined) {
            throw new Error('PointCone needs settings.container or settings.dimensions for wall projections.')
        }

        return container
    }

    angleTo(point, target) {
        return radiansToDegrees(point.directionTo(target))
    }

    wrapAngle(value) {
        return (value % 360 + 360) % 360
    }

    getCurveSettings(settings={}) {
        let conf = this.resolveSettings(settings)

        return {
            outer: this.normalizeCurveValue(conf.curve, conf.invertCurve === true)
            , inner: this.normalizeCurveValue(conf.innerCurve)
        }
    }

    getInnerOffset(settings={}) {
        let conf = this.resolveSettings(settings)
        let innerOffset = Number(conf.innerOffset)
        return Number.isNaN(innerOffset)? 0: innerOffset
    }

    getOutlineSettings(settings={}) {
        return getConeOutlineSettings(this.resolveSettings(settings))
    }

    normalizeCurveValue(value, invert=false) {
        return normalizeConeCurveValue(value, invert)
    }

    hasEqualRadius(originPoint, start, end) {
        let a = originPoint.distanceTo(start)
        let b = originPoint.distanceTo(end)
        return Math.abs(a - b) < .001
    }

    samePoint(a, b) {
        if(a == undefined || b == undefined) {
            return false
        }

        return a.x === b.x && a.y === b.y
    }

    pushUniquePoint(points, point) {
        if(point == undefined) {
            return false
        }

        if(this.samePoint(points[points.length - 1], point) === false) {
            points.push(point)
        }

        return true
    }

    ensureRenderData(settings={}) {
        return this.isRenderData(settings)? settings: this.renderData(settings)
    }

    isRenderData(value) {
        return value?.originPoint === this.parent
            && value?.settings != undefined
            && value?.edgePoints != undefined
    }
}


Polypoint.head.install(PointCone)


Polypoint.head.deferredProp('Point', function cone(){
    return new PointCone(this)
})
copy