/*
title: Elbow Constraints
category: constraints
files:
head
point
stroke
../point_src/point-content.js
pointlist
mouse
dragging
../point_src/distances.js
../point_src/functions/clamp.js
../point_src/mirror.js
../point_src/stage.js
../point_src/text/label.js
../point_src/intersections.js
../point_src/cone.js
../point_src/easing.js
../point_src/arc.js
../point_src/constrain-distance.js
---
An elbow contraint ensures a target point is _connected_ to another point, at
a distance of the two radii.
point.constraint.elbow(other)
It's called an elbow, as there will always be an intersection at the max distance.
Similar to rings bound at the edge.
Synonymous to:
let pA = this.legL
let pB = this.primaryPoint
pA.leash(pointB,
(pB.radius + pA.radius) - .01)
pA.avoid(pointB,
Math.abs(pB.radius - pA.radius) + .01)
*/
const getContinuationArc = function(fromPoint, toPoint, centerPoint) {
/* Return a plotted point on the continuation arc. The endpoint
rotations define the tangent directions, while `centerPoint` acts as
the landing-point hint and selects the correct sweep side. */
const dx = toPoint.x - fromPoint.x
const dy = toPoint.y - fromPoint.y
const distance = Math.hypot(dx, dy)
if(distance === 0) {
return {
x: fromPoint.x
, y: fromPoint.y
}
}
const midpointX = (fromPoint.x + toPoint.x) * .5
const midpointY = (fromPoint.y + toPoint.y) * .5
const startTangent = fromPoint.radians == undefined
? degToRad(fromPoint.rotation + 180)
: fromPoint.radians + Math.PI
const endTangent = toPoint.radians == undefined
? degToRad(toPoint.rotation)
: toPoint.radians
const sweep = Math.atan2(
Math.sin(endTangent - startTangent)
, Math.cos(endTangent - startTangent)
)
const minimumSweep = degToRad(.5)
const absSweep = Math.max(Math.abs(sweep), minimumSweep)
if(centerPoint != undefined && Math.abs(sweep) < degToRad(1)) {
const projectionAmount = (
((centerPoint.x - fromPoint.x) * dx)
+ ((centerPoint.y - fromPoint.y) * dy)
) / (distance * distance)
return {
x: fromPoint.x + (dx * projectionAmount)
, y: fromPoint.y + (dy * projectionAmount)
}
}
const offsetDistance = (distance * .5) / Math.tan(absSweep * .5)
const unitPerpX = -dy / distance
const unitPerpY = dx / distance
const sideHint = centerPoint == undefined
? 1
: ((dx * (centerPoint.y - midpointY)) - (dy * (centerPoint.x - midpointX))) < 0 ? -1 : 1
const radiansDiff = function(a, b) {
return Math.atan2(Math.sin(a - b), Math.cos(a - b))
}
const getCandidate = function(direction) {
const centerX = midpointX + (unitPerpX * offsetDistance * direction)
const centerY = midpointY + (unitPerpY * offsetDistance * direction)
const startRadiusAngle = Math.atan2(fromPoint.y - centerY, fromPoint.x - centerX)
const endRadiusAngle = Math.atan2(toPoint.y - centerY, toPoint.x - centerX)
const candidateSweep = radiansDiff(endRadiusAngle, startRadiusAngle)
const tangentDirection = candidateSweep < 0 ? -1 : 1
const tangentAtStart = startRadiusAngle + (tangentDirection * Math.PI * .5)
const tangentAtEnd = endRadiusAngle + (tangentDirection * Math.PI * .5)
const tangentError = Math.abs(radiansDiff(tangentAtStart, startTangent))
+ Math.abs(radiansDiff(tangentAtEnd, endTangent))
const sweepError = Math.abs(Math.abs(candidateSweep) - Math.min(absSweep, Math.PI))
const radius = Math.hypot(centerX - fromPoint.x, centerY - fromPoint.y)
let pointX = midpointX
let pointY = midpointY
let hintError = direction == sideHint ? 0 : .001
if(centerPoint != undefined) {
const hintDx = centerPoint.x - centerX
const hintDy = centerPoint.y - centerY
const hintDistance = Math.hypot(hintDx, hintDy)
if(hintDistance > 0.000001) {
pointX = centerX + (hintDx / hintDistance) * radius
pointY = centerY + (hintDy / hintDistance) * radius
hintError += Math.abs(hintDistance - radius)
}
}
return {
x: pointX
, y: pointY
, score: ((tangentError + sweepError) * distance) + (hintError / distance)
}
}
const candidateA = getCandidate(-1)
const candidateB = getCandidate(1)
return candidateA.score < candidateB.score ? candidateA : candidateB
}
class MainStage extends Stage {
canvas='playspace'
mounted(){
console.log('main')
this.a = new PointList(
{x:180,y:360, radius:20}
, {x:200,y:320, radius:20}
, {x:240,y:290, radius:20}
, {x:270,y:320, radius:20}
, {x:300,y:350, radius:20}
).cast()
this.dragging.addPoints(...this.a)
}
draw(ctx){
this.clear(ctx)
/* hip leashes knee */
// this.a[2].constraint.leash(this.a[1], 100)
/* Knee leashes foot */
// this.a[1].constraint.leash(this.a[0], 100)
/* Knee looks away from hip*/
let orig = this.a;
let lineCurve = this.genLine(orig)
// a1.pen.indicator(ctx)
// this.a.pen.indicator(ctx, {color: 'cyan'})
orig.pen.indicator(ctx, {color: '#444'})
let last;
lineCurve.forEach(trip => {
let sub = new PointList(...trip)
if(last) {
sub[0].pen.line(ctx, last, {color: 'cyan', width: 2})
}
sub.pen.indicator(ctx, {color: 'orange'})
// sub.pen.quadCurve(ctx, {color: 'pink', width: 2, loop:false})
this.drawF(ctx, ...trip)
last = sub.last()
})
// lineCurve.pen.indicator(ctx, {color: 'orange'})
// lineCurve.pen.quadCurve(ctx, {color: 'red', width: 2, loop:true})
// lineCurve.pen.quadCurve(ctx, {color: 'pink', width: 2, loop:false})
// lineCurve.pen.line(ctx, {color: 'pink', width: 2, loop:false})
}
drawF(ctx, fromPoint, toPoint, centerPoint){
// let cap = fromPoint.arc.to(toPoint, centerPoint)
let cap = fromPoint.arc.to(centerPoint, toPoint)
if(!cap) { return }
ctx.beginPath();
ctx.arc(cap.cx, cap.cy, cap.radius, cap.startRadians, cap.toRadians, 0);
ctx.stroke();
}
genLine(orig){
let getPair = function(fromIndex=0, centerIndex=1, toIndex=2) {
let r = orig[centerIndex].rotation
orig[centerIndex].lookAt(orig[fromIndex])
let a1 = orig[centerIndex].project()
orig[centerIndex].lookAt(orig[toIndex])
let a2 = orig[centerIndex].project()
orig[centerIndex].rotation = r
return [a1, a2]
}
let triple = function(fromIndex=0, centerIndex=1, toIndex=2, easingName) {
let ab = getPair(fromIndex, centerIndex, toIndex)
let arcPoint = orig[centerIndex].copy().update({radius:5})
arcPoint.update(getContinuationArc(ab[0], ab[1], orig[centerIndex]))
// return [ab[0], orig[centerIndex], ab[1]]
return [ab[0], arcPoint, ab[1]]
}
let tipA = function(beforeIndex, index, toTip=true, easingName) {
let origR = orig[index].rotation
// orig[index].lookAt(orig.last())
let a1 = orig[index].project()
orig[index].lookAt(orig[beforeIndex])
let a2 = orig[index]
if(toTip) { a2 = a2.project() }
orig[index].rotation = origR
if(!toTip) { return [a2, orig[index]] }
let c = orig[index].copy()
c.lookAt(a1.midpoint(a2))
let real = c.radius - (a1.distanceTo(a2)) * .5
c.radius = undefined
c.update(getContinuationArc(a1, a2, orig[index]))
return [a1, c, a2]
}
let tipB = function(beforeIndex, index, toTip=true, easingName) {
let origR = orig[index].rotation
orig[index].lookAt(orig[beforeIndex])
let a1 = orig[index]
if(toTip) { a1 = a1.project() }
orig[index].rotation = origR
let a2 = orig[index].project()
let c = orig[index].copy()
// if(easingName) {
// c.lookAt(a1.midpoint(a2))
// let real = c.radius - (a1.distanceTo(a2)) * .5
// real = c.radius * easingFunctions[easingName][easingType](real / c.radius)
// c = c.project(real)
// }
c.lookAt(a1.midpoint(a2))
let real = c.radius - (a1.distanceTo(a2)) * .5
c.radius = undefined
c.update(getContinuationArc(a1, a2, orig[index]))
if(!toTip) { return [a1, orig[index]] }
return [a1, c, a2]
}
let items = orig//.slice(1, stage.a.length-1)
let toTip = true;
let easingName = 'sine'
let easingType = 'out'
// orig[1].constraint.cone(orig[2], 90)
let lineCurve = new PointList(
tipA(1, 0, toTip, easingName)
)//.cast()
items.forEach((e,i, a) => {
if(i == 0 || i == items.length-1) { return }
lineCurve.push(triple(i-1, i, i+1, easingName))
})
lineCurve.push(tipB(items.length-2, items.length-1, toTip, easingName));
return lineCurve
}
}
;stage = MainStage.go();