Classifying cubic bezier curves according to Loop & Blinn 2005 - math

I am trying flatten a bezier path (remove all intersections and replace with end points) as part of the implementation of the rendering algorithm described here and found the algorithm described by Loop and Blinn in GPU Gems 3 Ch. 25, with correction for $a3$'s cross product to detect curve self-intersection (loops).
The algorithm calls for the evaluation of the expression discr=d₁²(3d₂²-4d₁d₃) such that the curve is self-intersecting iff the discr < 0.
where
d₁=a₁-2a₂+3a₃,
d₂=-a₂+3a₃,
d₃=3a₃
a₁=b₀·(b₃⨯b₂),
a₂=b₁·(b₀⨯b₃),
a₃=b₂·(b₁⨯b₀)
I have implemented the algorithm in the sample code below, as well as that of a few other algorithms I have been able to find. None of them agree, and all of them are wrong. Specifically, I have implemented
The algorithm described in GPU Gems 3
The same algorithm as described in the original paper.
The same algorithm as implemented in paper.js (issue)(sketch).
The curve canonicalization algorithm described in Pomax' Bezier Primer.
That all three algorithms disagree suggests a major misunderstanding. What is it, and (more importantly) how might I identify such an issue myself?
I referred to the questions here and here, as well as the paper in which Loop & Blinn describe their approach, to no effect.
#[derive(Clone, Copy, Debug, PartialEq)]
enum Kind {
Serpentine,
Cusp,
Loop,
Quadratic,
Line,
}
#[derive(Clone, Copy)]
pub struct Point {
x: f32,
y: f32,
}
#[derive(Clone, Copy)]
struct Vec3 {
x: f32,
y: f32,
z: f32,
}
impl Vec3 {
fn from_point(p: Point) -> Vec3 {
Vec3 {
x: p.x,
y: p.y,
z: 1.0,
}
}
fn dot(&self, other: Vec3) -> f32 {
self.x * other.x + self.y * other.y + self.z * other.z
}
fn cross(&self, other: Vec3) -> Vec3 {
Vec3 {
x: self.y * other.z - self.z * other.y,
y: self.z * other.x - self.x * other.z,
z: self.x * other.y - self.y * other.x,
}
}
}
fn is_zero(f: f32) -> bool {
f.abs() < 0.000001
}
fn approx_eq(a: f32, b: f32) -> bool {
is_zero((a - b).abs())
}
fn normalize(a: f32, b: f32, c: f32) -> (f32, f32, f32) {
let len = (a * a + b * b + c * c).sqrt();
if is_zero(len) {
(0.0, 0.0, 0.0)
} else {
(a / len, b / len, c / len)
}
}
pub fn classify(curve: &[Point; 4]) {
let p0 = Vec3::from_point(curve[0]);
let p1 = Vec3::from_point(curve[1]);
let p2 = Vec3::from_point(curve[2]);
let p3 = Vec3::from_point(curve[3]);
let loop_blinn = {
let det1 = -p3.dot(p2.cross(p0));
let det2 = p3.dot(p2.cross(p0));
let det3 = -p2.dot(p1.cross(p0));
let (det1, det2, det3) = normalize(det1, det2, det3);
let discr = det2 * det2 * (3.0 * det2 * det2 - 4.0 * det3 * det1);
if is_zero(det1) {
if !is_zero(det2) {
Kind::Cusp
} else if is_zero(det3) {
Kind::Line
} else {
Kind::Quadratic
}
} else if is_zero(discr) {
Kind::Cusp
} else if discr > 0.0 {
Kind::Serpentine
} else {
Kind::Loop
}
};
let gpu_gems = {
let a1 = p0.dot(p3.cross(p2));
let a2 = p1.dot(p0.cross(p3));
let a3 = p2.dot(p1.cross(p0));
let d1 = a1 - 2.0 * a2 + 3.0 * a3;
let d2 = -a2 + 3.0 * a3;
let d3 = 3.0 * a3;
let discr = d2 * d2 * (3.0 * d2 * d2 - 4.0 * d1 * d3);
if is_zero(d1) {
if is_zero(d2) && is_zero(d3) {
Kind::Line
} else {
Kind::Quadratic
}
} else if is_zero(discr) {
Kind::Cusp
} else if discr < 0.0 {
Kind::Serpentine
} else {
Kind::Loop
}
};
let paper_js = {
let x1 = p0.x;
let y1 = p0.y;
let x2 = p1.x;
let y2 = p1.y;
let x3 = p2.x;
let y3 = p2.y;
let x4 = p3.x;
let y4 = p3.y;
let a1 = x1 * (y4 - y3) + y1 * (x3 - x4) + x4 * y3 - x3 * y4;
let a2 = x2 * (y1 - y4) + y2 * (x4 - x1) + x1 * y4 - y1 * x4;
let a3 = x3 * (y2 - y1) + y3 * (x1 - x2) + x2 * y1 - y2 * x1;
let d3 = 3.0 * a3;
let d2 = d3 - a2;
let d1 = d2 - a2 + a1;
let (d1, d2, d3) = normalize(d1, d2, d3);
let d = 3.0 * d2 * d2 - 4.0 * d1 * d3;
if is_zero(d1) {
if is_zero(d2) {
if is_zero(d3) {
Kind::Line
} else {
Kind::Quadratic
}
} else {
Kind::Serpentine
}
} else if is_zero(d) {
Kind::Cusp
} else if d > 0.0 {
Kind::Serpentine
} else {
Kind::Loop
}
};
let pomax_primer = {
let y31 = p3.y / p1.y;
let y21 = p2.y / p1.y;
let x32 = (p3.x - p2.x * y31) / (p2.x - p1.x * y21);
let x = x32;
let y = y31 + x32 * (1.0 - y21);
let cusp_line = (-(x * x) + 2.0 * x + 3.0) / 4.0;
let loop_at_1 = ((3.0 * 4.0 * x - x * x).sqrt() - x) / 2.0;
let loop_at_0 = (-(x * x) + 3.0 * x) / 3.0;
if x > 1.0 || y > 1.0 {
Kind::Serpentine
} else if (0.0..1.0).contains(&x) {
if approx_eq(loop_at_1, y) {
Kind::Loop
} else if approx_eq(cusp_line, y) {
Kind::Cusp
} else if y < cusp_line || y > loop_at_1 {
Kind::Serpentine
} else {
Kind::Loop
}
} else if approx_eq(loop_at_0, y) {
Kind::Loop
} else if approx_eq(cusp_line, y) {
Kind::Cusp
} else if y < cusp_line || y > loop_at_0 {
Kind::Loop
} else {
Kind::Serpentine
}
};
println!("\tMethod 1 (Loop Blinn 2005): {:?}", loop_blinn);
println!("\tMethod 2 (GPU Gems 3.25): {:?}", gpu_gems);
println!("\tMethod 3 (Paper.js): {:?}", paper_js);
println!("\tMethod 4 (Pomax Primer): {:?}", pomax_primer);
}
pub fn main() {
let point = [
Point { x: 1.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
];
println!("Expecting: Kind::Line");
classify(&point);
let line = [
Point { x: 1.0, y: 1.0 },
Point { x: 2.0, y: 1.0 },
Point { x: 3.0, y: 1.0 },
Point { x: 4.0, y: 1.0 },
];
println!("Expecting: Kind::Line");
classify(&line);
// loop
let normal_loop = [
Point { x: 75.0, y: 98.0 },
Point { x: 195.0, y: 201.0 },
Point { x: 63.0, y: 198.0 },
Point { x: 135.0, y: 103.0 },
];
println!("Expecting: Kind::Loop");
classify(&normal_loop);
let end_loop = [
Point { x: 120.0, y: 331.0 },
Point { x: 246.0, y: 261.0 },
Point { x: 187.0, y: 242.0 },
Point { x: 148.0, y: 314.0 },
];
println!("Expecting: Kind::Loop");
classify(&end_loop);
// cusp
let cusp = [
Point { x: 272.0, y: 305.0 },
Point { x: 93.0, y: 223.0 },
Point { x: 221.0, y: 223.0 },
Point { x: 148.0, y: 314.0 },
];
println!("Expecting: Kind::Cusp");
classify(&cusp);
// serpentine
let serpentine = [
Point { x: 148.0, y: 314.0 },
Point { x: 187.0, y: 242.0 },
Point { x: 246.0, y: 261.0 },
Point { x: 272.0, y: 305.0 },
];
println!("Expecting: Kind::Serpentine");
classify(&serpentine);
let serpentine2 = [
Point { x: 110.0, y: 150.0 },
Point { x: 25.0, y: 190.0 },
Point { x: 210.0, y: 250.0 },
Point { x: 210.0, y: 30.0 },
];
println!("Expecting: Kind::Serpentine");
classify(&serpentine2);
}

Related

Konvajs scroll to the center of layer

I'm a little stuck on this problem. I would like to scale based on the center of the layer rather than a mouse pointer.
Heres the Konva demo I got this from https://konvajs.org/docs/sandbox/Zooming_Relative_To_Pointer.html
form
state.stage.on('wheel', (e) => {
e.evt.preventDefault();
var oldScale = state.layer.scaleX();
var pointer = state.layer.getPointerPosition();
var mousePointTo = {
x: 0,
y: 0
};
var newScale =
e.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy;
stage.scale({ x: newScale, y: newScale });
var newPos = {
x: newScale,
y: newScale,
};
state.layer.position(newPos);
state.layer.batchDraw();
});
Also I want to have a way to have it go back to its original position.
You just need to update that example by thinking that "mouse pointer" is at the center of the canvas. It can be something like this:
var scaleBy = 1.01;
stage.on('wheel', (e) => {
e.evt.preventDefault();
var oldScale = stage.scaleX();
var pointer = {
x: stage.width() / 2,
y: stage.height() / 2
};
var origin = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
var newScale =
e.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy;
stage.scale({ x: newScale, y: newScale });
var newPos = {
x: pointer.x - origin.x * newScale,
y: pointer.y - origin.y * newScale,
};
stage.position(newPos);
stage.batchDraw();
});
https://jsbin.com/jikuzutuse/2/edit?html,js,output

Ionic basic function inside class is not executing fully

calculate_distance(lat1: number, lat2: number, long1: number, long2: number) {
console.log("Inside getting calculate_distance");
console.log(lat1);
console.log(lat2);
console.log(long1);
console.log(long2);
let p = 0.017453292519943295; // Math.PI / 180
let c = Math.cos;
let a = 0.5 - c((lat1 - lat2) * p) / 2 + c(lat2 * p) * c((lat1) * p) * (1 - c(((long1 - long2) * p))) / 2;
let dis = (12742 * Math.asin(Math.sqrt(a))); // 2 * R; R = 6371 km
return dis.toFixed(2);
}
calling this function inside other function and i am not getting the distance value.
i just added a promise return type to my function. So that it will return me the value.
calculateDistance(lat1: number, lat2: number, long1: number, long2: number):Promise<any> {
console.log("Inside getting calculate_distance");
console.log(lat1);
console.log(lat2);
console.log(long1);
console.log(long2);
let p = 0.017453292519943295; // Math.PI / 180
let c = Math.cos;
let a = 0.5 - c((lat1 - lat2) * p) / 2 + c(lat2 * p) * c((lat1) * p) * (1 - c(((long1 - long2) * p))) / 2;
let dis = (12742 * Math.asin(Math.sqrt(a))); // 2 * R; R = 6371 km
return dis.toFixed(2);
}
and called calculateDistance like this
this.calculateDistance(this.my_lat, this.proj_lat, this.my_long, this.proj_long).then(result=>{
this.distance = String(result)
})

How do I draw the outline of an object in Qt3D?

How can I draw the outline of an object on top of any other object in Qt3D? For instance to highlight a selected object in a 3D editor?
If you want to draw the outline of an entity at all times, even if the entity is behind other entities, one solution is to do it in two steps:
Draw everything as normal.
Draw only the outline of the selected object.
When drawing the outline, you need to use an outline effect, which can be implemented in two render passes:
Render the geometry to a texture using a simple color shader.
Render to screen using a shader that takes each pixel in the texture and compares the surrounding pixels. If they are equal, we are inside the object and the fragment can be discarded. If they differ, we are on the edge of the object and we should draw the color.
Here is a simple implementation of the above-mentioned shader:
#version 150
uniform sampler2D color;
uniform vec2 winSize;
out vec4 fragColor;
void main()
{
int lineWidth = 5;
vec2 texCoord = gl_FragCoord.xy / winSize;
vec2 texCoordUp = (gl_FragCoord.xy + vec2(0, lineWidth)) / winSize;
vec2 texCoordDown = (gl_FragCoord.xy + vec2(0, -lineWidth)) / winSize;
vec2 texCoordRight = (gl_FragCoord.xy + vec2(lineWidth, 0)) / winSize;
vec2 texCoordLeft = (gl_FragCoord.xy + vec2(-lineWidth, 0)) / winSize;
vec4 col = texture(color, texCoord);
vec4 colUp = texture(color, texCoordUp);
vec4 colDown = texture(color, texCoordDown);
vec4 colRight = texture(color, texCoordRight);
vec4 colLeft = texture(color, texCoordLeft);
if ((colUp == colDown && colRight == colLeft) || col.a == 0.0)
discard;
fragColor = col;
}
Note: It might be a better idea to take the difference between the values instead of using an equality.
With this method, you don't have to worry about depth testing and the order in which the objects are drawn: The second time you draw, you will always draw on top of everything else.
You could do this by adding a single effect with two techniques with different filter keys. Alternatively, if you want to use materials from Qt3D.Extras, you can add another entity with the same transform and mesh and a material that uses the outline technique.
Here is an example that draws the outline on top of everything else using two render passes:
import QtQuick 2.2 as QQ2
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.0
Entity {
Camera {
id: camera
projectionType: CameraLens.PerspectiveProjection
fieldOfView: 45
aspectRatio: 16/9
nearPlane : 0.1
farPlane : 1000.0
position: Qt.vector3d( 0.0, 0.0, -40.0 )
upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
}
OrbitCameraController {
camera: camera
}
components: [
RenderSettings {
activeFrameGraph: RenderSurfaceSelector {
id: surfaceSelector
Viewport {
CameraSelector {
camera: camera
FrustumCulling {
TechniqueFilter {
matchAll: [
FilterKey { name: "renderingStyle"; value: "forward" }
]
ClearBuffers {
clearColor: Qt.rgba(0.1, 0.2, 0.3)
buffers: ClearBuffers.ColorDepthStencilBuffer
}
}
TechniqueFilter {
matchAll: [
FilterKey { name: "renderingStyle"; value: "outline" }
]
RenderPassFilter {
matchAny: [
FilterKey {
name: "pass"; value: "geometry"
}
]
ClearBuffers {
buffers: ClearBuffers.ColorDepthStencilBuffer
RenderTargetSelector {
target: RenderTarget {
attachments : [
RenderTargetOutput {
objectName : "color"
attachmentPoint : RenderTargetOutput.Color0
texture : Texture2D {
id : colorAttachment
width : surfaceSelector.surface.width
height : surfaceSelector.surface.height
format : Texture.RGBA32F
}
}
]
}
}
}
}
RenderPassFilter {
parameters: [
Parameter { name: "color"; value: colorAttachment },
Parameter { name: "winSize"; value : Qt.size(surfaceSelector.surface.width, surfaceSelector.surface.height) }
]
matchAny: [
FilterKey {
name: "pass"; value: "outline"
}
]
}
}
}
}
}
}
},
InputSettings { }
]
PhongMaterial {
id: material
}
Material {
id: outlineMaterial
effect: Effect {
techniques: [
Technique {
graphicsApiFilter {
api: GraphicsApiFilter.OpenGL
majorVersion: 3
minorVersion: 1
profile: GraphicsApiFilter.CoreProfile
}
filterKeys: [
FilterKey { name: "renderingStyle"; value: "outline" }
]
renderPasses: [
RenderPass {
filterKeys: [
FilterKey { name: "pass"; value: "geometry" }
]
shaderProgram: ShaderProgram {
vertexShaderCode: "
#version 150 core
in vec3 vertexPosition;
uniform mat4 modelViewProjection;
void main()
{
gl_Position = modelViewProjection * vec4( vertexPosition, 1.0 );
}
"
fragmentShaderCode: "
#version 150 core
out vec4 fragColor;
void main()
{
fragColor = vec4( 1.0, 0.0, 0.0, 1.0 );
}
"
}
}
]
}
]
}
}
SphereMesh {
id: sphereMesh
radius: 3
}
Transform {
id: sphereTransform
}
Transform {
id: sphereTransform2
// TODO workaround because the transform cannot be shared
matrix: sphereTransform.matrix
}
Entity {
id: sphereEntity
components: [ sphereMesh, material, sphereTransform ]
}
Entity {
id: sphereOutlineEntity
components: [ sphereMesh, outlineMaterial, sphereTransform2 ]
}
Entity {
id: outlineQuad
components: [
PlaneMesh {
width: 2.0
height: 2.0
meshResolution: Qt.size(2, 2)
},
Transform {
rotation: fromAxisAndAngle(Qt.vector3d(1, 0, 0), 90)
},
Material {
effect: Effect {
techniques: [
Technique {
filterKeys: [
FilterKey { name: "renderingStyle"; value: "outline" }
]
graphicsApiFilter {
api: GraphicsApiFilter.OpenGL
profile: GraphicsApiFilter.CoreProfile
majorVersion: 3
minorVersion: 1
}
renderPasses : RenderPass {
filterKeys : FilterKey { name : "pass"; value : "outline" }
shaderProgram : ShaderProgram {
vertexShaderCode: "
#version 150
in vec4 vertexPosition;
uniform mat4 modelMatrix;
void main()
{
gl_Position = modelMatrix * vertexPosition;
}
"
fragmentShaderCode: "
#version 150
uniform sampler2D color;
uniform vec2 winSize;
out vec4 fragColor;
void main()
{
int lineWidth = 5;
vec2 texCoord = gl_FragCoord.xy / winSize;
vec2 texCoordUp = (gl_FragCoord.xy + vec2(0, lineWidth)) / winSize;
vec2 texCoordDown = (gl_FragCoord.xy + vec2(0, -lineWidth)) / winSize;
vec2 texCoordRight = (gl_FragCoord.xy + vec2(lineWidth, 0)) / winSize;
vec2 texCoordLeft = (gl_FragCoord.xy + vec2(-lineWidth, 0)) / winSize;
vec4 col = texture(color, texCoord);
vec4 colUp = texture(color, texCoordUp);
vec4 colDown = texture(color, texCoordDown);
vec4 colRight = texture(color, texCoordRight);
vec4 colLeft = texture(color, texCoordLeft);
if ((colUp == colDown && colRight == colLeft) || col.a == 0.0)
discard;
fragColor = col;
}
"
}
}
}]
}
}
]
}
}
The result:

Deconstructing Google maps smarty pins animation

Updates
Updated fiddle to simplify what is going on:
added four buttons to move the stick, each button increments the value by 30 in the direction
plotted x and y axis
red line is the stick, with bottom end coordinates at (ax,ay) and top end coordinates at (bx,by)
green line is (presumably) previous position of the stick, with bottom end coordinates at (ax, ay) and top end coordinates at (bx0, by0)
So, after having my ninja moments. I'm still nowhere near understanding the sorcery behind unknownFunctionA and unknownFunctionB
For the sake of everyone (all two of you) here is what I've sort of learnt so far
function unknownFunctionB(e) {
var t = e.b.x - e.a.x
, n = e.b.y - e.a.y
, a = t * t + n * n;
if (a > 0) {
if (a == e.lengthSq)
return;
var o = Math.sqrt(a)
, i = (o - e.length) / o
, s = .5;
e.b.x -= t * i * .5 * s,
e.b.y -= n * i * .5 * s
}
}
In the unknownFunctionB above, variable o is length of the red sitck.
Still don't understand
What is variable i and how is (bx,by) calculated? essentially:
bx = bx - (bx - ax) * 0.5 * 0.5
by = by - (by - ay) * 0.5 * 0.5
In unknownFunctionA what are those magic numbers 1.825 and 0.825?
Below is irrelevant
I'm trying to deconstruct marker drag animation used on smartypins
I've managed to get the relevant code for marker move animation but I'm struggling to learn how it all works, especially 2 functions (that I've named unknownFunctionA and unknownFunctionB)
Heres the StickModel class used on smartypins website, unminified to best of my knowledge
function unknownFunctionA(e) {
var t = 1.825
, n = .825
, a = t * e.x - n * e.x0
, o = t * e.y - n * e.y0 - 5;
e.x0 = e.x,
e.y0 = e.y,
e.x = a,
e.y = o;
}
function unknownFunctionB(e) {
var t = e.b.x - e.a.x
, n = e.b.y - e.a.y
, a = t * t + n * n;
if (a > 0) {
if (a == e.lengthSq)
return;
var o = Math.sqrt(a)
, i = (o - e.length) / o
, s = .5;
e.b.x -= t * i * .5 * s,
e.b.y -= n * i * .5 * s
}
}
function StickModel() {
this._props = function(e) {
return {
length: e,
lengthSq: e * e,
a: {
x: 0,
y: 0
},
b: {
x: 0,
y: 0 - e,
x0: 0,
y0: 0 - e
},
angle: 0
}
}
(60)
}
var radianToDegrees = 180 / Math.PI;
StickModel.prototype = {
pos: {
x: 0,
y: 0
},
angle: function() {
return this._props.angle
},
reset: function(e, t) {
var n = e - this._props.a.x
, a = t - this._props.a.y;
this._props.a.x += n,
this._props.a.y += a,
this._props.b.x += n,
this._props.b.y += a,
this._props.b.x0 += n,
this._props.b.y0 += a
},
move: function(e, t) {
this._props.a.x = e,
this._props.a.y = t
},
update: function() {
unknownFunctionA(this._props.b),
unknownFunctionB(this._props),
this.pos.x = this._props.a.x,
this.pos.y = this._props.a.y;
var e = this._props.b.x - this._props.a.x
, t = this._props.b.y - this._props.a.y
, o = Math.atan2(t, e);
this._props.angle = o * radianToDegrees;
}
}
StickModel.prototype.constructor = StickModel;
Fiddle link with sample implementation on canvas: http://jsfiddle.net/vff1w82w/3/
Again, Everything works as expected, I'm just really curious to learn the following:
What could be the ideal names for unknownFunctionA and unknownFunctionB and an explanation of their functionality
What are those magic numbers in unknownFunctionA (1.825 and .825) and .5 in unknownFunctionB.
Variable o in unknownFunctionB appears to be hypotenuse. If that's the case, then what exactly is i = (o - e.length) / o in other words, i = (hypotenuse - stickLength) / hypotenuse?
First thing I'd recommend is renaming all those variables and methods until they start making sense. I also removed unused code.
oscillator
adds wobble to the Stick model by creating new position values for the Stick that follows the mouse
Exaggerates its movement by multiplying its new position by 1.825 and also subtracting the position of an "echo" of its previous position multiplied by 0.825. Sort of looking for a middle point between them. Helium makes the stick sit upright.
overshooter minus undershooter must equal 1 or you will have orientation problems with your stick. overshooter values above 2.1 tend to make it never settle.
seekerUpdate
updates the seeker according to mouse positions.
The distance_to_cover variable measures the length of the total movement. You were right: hypothenuse (variable o).
The ratio variable calculates the ratio of the distance that can be covered subtracting the size of the stick. The ratio is then used to limit the adjustment of the update on the seeker in both directions (x and y). That's how much of the update should be applied to prevent overshooting the target.
easing slows down the correct updates.
There are lots of interesting info related to vectors on the book The nature of code.
function oscillator(seeker) {
var overshooter = 1.825;
var undershooter = .825;
var helium = -5;
var new_seeker_x = overshooter * seeker.x - undershooter * seeker.echo_x;
var new_seeker_y = overshooter * seeker.y - undershooter * seeker.echo_y + helium;
seeker.echo_x = seeker.x;
seeker.echo_y = seeker.y;
seeker.x = new_seeker_x;
seeker.y = new_seeker_y;
}
function seekerUpdate(stick) {
var dX = stick.seeker.x - stick.mouse_pos.x;
var dY = stick.seeker.y - stick.mouse_pos.y;
var distance_to_cover = Math.sqrt(dX * dX + dY * dY);
var ratio = (distance_to_cover - stick.length) / distance_to_cover;
var easing = .25;
stick.seeker.x -= dX * ratio * easing;
stick.seeker.y -= dY * ratio * easing;
}
function StickModel() {
this._props = function(length) {
return {
length: length,
lengthSq: length * length,
mouse_pos: {
x: 0,
y: 0
},
seeker: {
x: 0,
y: 0 - length,
echo_x: 0,
echo_y: 0 - length
}
}
}(60)
}
StickModel.prototype = {
move: function(x, y) {
this._props.mouse_pos.x = x;
this._props.mouse_pos.y = y;
},
update: function() {
oscillator(this._props.seeker);
seekerUpdate(this._props);
}
};
StickModel.prototype.constructor = StickModel;
// Canvas to draw stick model coordinates
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
canvas.width = window.outerWidth;
canvas.height = window.outerHeight;
var canvasCenterX = Math.floor(canvas.width / 2);
var canvasCenterY = Math.floor(canvas.height / 2);
context.translate(canvasCenterX, canvasCenterY);
var stickModel = new StickModel();
draw();
setInterval(function() {
stickModel.update();
draw();
}, 16);
$(window).mousemove(function(e) {
var mouseX = (e.pageX - canvasCenterX);
var mouseY = (e.pageY - canvasCenterY);
stickModel.move(mouseX, mouseY);
stickModel.update();
draw();
});
function draw() {
context.clearRect(-canvas.width, -canvas.height, canvas.width * 2, canvas.height * 2);
// red line from (ax, ay) to (bx, by)
context.beginPath();
context.strokeStyle = "#ff0000";
context.moveTo(stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.lineTo(stickModel._props.seeker.x, stickModel._props.seeker.y);
context.fillText('mouse_pos x:' + stickModel._props.mouse_pos.x + ' y: ' + stickModel._props.mouse_pos.y, stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.fillText('seeker x:' + stickModel._props.seeker.x + ' y: ' + stickModel._props.seeker.y, stickModel._props.seeker.x - 30, stickModel._props.seeker.y);
context.lineWidth = 1;
context.stroke();
context.closePath();
// green line from (ax, ay) to (bx0, by0)
context.beginPath();
context.strokeStyle = "#00ff00";
context.moveTo(stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.lineTo(stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y);
context.fillText('echo x:' + stickModel._props.seeker.echo_x + ' y: ' + stickModel._props.seeker.echo_y, stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y - 20);
context.lineWidth = 1;
context.stroke();
context.closePath();
// blue line from (bx0, by0) to (bx, by)
context.beginPath();
context.strokeStyle = "#0000ff";
context.moveTo(stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y);
context.lineTo(stickModel._props.seeker.x, stickModel._props.seeker.y);
context.stroke();
context.closePath();
}
body {
margin: 0px;
padding: 0px;
}
canvas {
display: block;
}
p {
position: absolute;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<p>Move your mouse to see the stick (colored red) follow</p>
<canvas id="myCanvas"></canvas>

Angle between 3 points in 3d space

I have 3 points containing X, Y, Z coordinates:
var A = {x: 100, y: 100, z: 80},
B = {x: 100, y: 175, z: 80},
C = {x: 100, y: 100, z: 120};
The coordinates are pixels from a 3d CSS transform.
How can I get the angle between vectors BA and BC?
A math formula will do, JavaScript code will be better.
Thank you.
In pseudo-code, the vector BA (call it v1) is:
v1 = {A.x - B.x, A.y - B.y, A.z - B.z}
Similarly the vector BC (call it v2) is:
v2 = {C.x - B.x, C.y - B.y, C.z - B.z}
The dot product of v1 and v2 is a function of the cosine of the angle between them (it's scaled by the product of their magnitudes). So first normalize v1 and v2:
v1mag = sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z)
v1norm = {v1.x / v1mag, v1.y / v1mag, v1.z / v1mag}
v2mag = sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z)
v2norm = {v2.x / v2mag, v2.y / v2mag, v2.z / v2mag}
Then calculate the dot product:
res = v1norm.x * v2norm.x + v1norm.y * v2norm.y + v1norm.z * v2norm.z
And finally, recover the angle:
angle = acos(res)
double GetAngleABC( double* a, double* b, double* c )
{
double ab[3] = { b[0] - a[0], b[1] - a[1], b[2] - a[2] };
double bc[3] = { c[0] - b[0], c[1] - b[1], c[2] - b[2] };
double abVec = sqrt(ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2]);
double bcVec = sqrt(bc[0] * bc[0] + bc[1] * bc[1] + bc[2] * bc[2]);
double abNorm[3] = {ab[0] / abVec, ab[1] / abVec, ab[2] / abVec};
double bcNorm[3] = {bc[0] / bcVec, bc[1] / bcVec, bc[2] / bcVec};
double res = abNorm[0] * bcNorm[0] + abNorm[1] * bcNorm[1] + abNorm[2] * bcNorm[2];
return acos(res)*180.0/ 3.141592653589793;
}
double a[] = {1, 0, 0};
double b[] = {0, 0, 0};
double c[] = {0, 1, 0};
std::cout<< "The angle of ABC is " << GetAngleABC(a,b,c)<< "º " << std::endl;
#Roger algorithm in swift
func SCNVector3Angle(start: SCNVector3, mid: SCNVector3, end: SCNVector3) -> Double {
let v1 = (start - mid)
let v2 = (end - mid)
let v1norm = v1.normalized()
let v2norm = v2.normalized()
let res = v1norm.x * v2norm.x + v1norm.y * v2norm.y + v1norm.z * v2norm.z
let angle: Double = Double(GLKMathRadiansToDegrees(acos(res)))
return angle
}
/**
* Subtracts two SCNVector3 vectors and returns the result as a new SCNVector3.
*/
func - (left: SCNVector3, right: SCNVector3) -> SCNVector3 {
return SCNVector3Make(left.x - right.x, left.y - right.y, left.z - right.z)
}
extension SCNVector3
{
/**
* Returns the length (magnitude) of the vector described by the SCNVector3
*/
func length() -> Float {
return sqrtf(x*x + y*y + z*z)
}
/**
* Normalizes the vector described by the SCNVector3 to length 1.0 and returns
* the result as a new SCNVector3.
*/
func normalized() -> SCNVector3 {
return self / length()
}
}
The same in python (with output in degrees):
import numpy as np
import math
import time
def angle_2p_3d(a, b, c):
v1 = np.array([ a[0] - b[0], a[1] - b[1], a[2] - b[2] ])
v2 = np.array([ c[0] - b[0], c[1] - b[1], c[2] - b[2] ])
v1mag = np.sqrt([ v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2] ])
v1norm = np.array([ v1[0] / v1mag, v1[1] / v1mag, v1[2] / v1mag ])
v2mag = np.sqrt(v2[0] * v2[0] + v2[1] * v2[1] + v2[2] * v2[2])
v2norm = np.array([ v2[0] / v2mag, v2[1] / v2mag, v2[2] / v2mag ])
res = v1norm[0] * v2norm[0] + v1norm[1] * v2norm[1] + v1norm[2] * v2norm[2]
angle_rad = np.arccos(res)
return math.degrees(angle_rad)
p1 = np.array([1,0,0])
p2 = np.array([0,0,0])
p3 = np.array([0,0,1])
start = time.time()
angle= angle_2p_3d(p1, p2, p3)
end = time.time()
print("angle: ", angle)
print("elapsed in: ", end - start)
Output:
angle: 90.0
elapsed in: 8.392333984375e-05

Resources