Ball-Triangle Collision - javascript

Goal: I have a ball in a triangle. The ball has an initial position and velocity. I'm trying to figure out which side of the triangle the ball will hit.
What I've Tried: I derived a formula that outputs which side the ball will hit, by parametrizing the ball's path and the triangle's sides, and finding the minimum time that satisfies the parametric equations. But when I implement this formula into my program, it produces the wrong results! I've tried many things, to no avail. Any help is greatly appreciated. The MWE is here: CodePen
let angle = 0;
let sides = [];
let vertices = [];
const len = 100;
function setup() {
createCanvas(windowWidth, windowHeight);
angleMode(DEGREES);
angleOne = createSlider(0, 89, 60);
angleOne.position(10, 10);
angleOne.style("width", "80px");
angleTwo = createSlider(0, 89, 60);
angleTwo.position(10, 30);
angleTwo.style("width", "80px");
// Initial vertice & side setup (these don't change)
let v1 = createVector(width / 2 - len / 2, height * 0.7);
let v2 = createVector(width / 2 + len / 2, height * 0.7);
sides[0] = new Side(v1.x, v1.y, v2.x, v2.y, "green");
vertices[0] = new Vertex(v1.x, v1.y);
vertices[1] = new Vertex(v2.x, v2.y);
}
function draw() {
background(255);
let angOne = angleOne.value();
let angTwo = angleTwo.value();
fill(0);
strokeWeight(0);
textSize(15);
text(angOne, 100, 25);
text(angTwo, 100, 45);
let v2Offset = createVector(len * cos(-angOne), len * sin(-angOne));
let v3Offset = createVector(-len * cos(angTwo), -len * sin(angTwo));
vertices[2] = new Vertex(
vertices[0].a.x + v2Offset.x,
vertices[0].a.y + v2Offset.y
);
vertices[3] = new Vertex(
vertices[1].a.x + v3Offset.x,
vertices[1].a.y + v3Offset.y
);
// Update the sides
sides[1] = new Side(
vertices[0].a.x,
vertices[0].a.y,
vertices[2].a.x,
vertices[2].a.y
);
sides[3] = new Side(
vertices[1].a.x,
vertices[1].a.y,
vertices[3].a.x,
vertices[3].a.y
);
const m1 =
(vertices[2].a.y - vertices[0].a.y) / (vertices[2].a.x - vertices[0].a.x);
const m2 =
(vertices[3].a.y - vertices[1].a.y) / (vertices[3].a.x - vertices[1].a.x);
// Calculate the y-offset relative to vertices[0]
const b2 = (vertices[1].a.x - vertices[0].a.x) * -m2;
const xInt = b2 / (m1 - m2);
const yInt = xInt * m1;
// Note xInt and yInt are relative to vertices[0]
// draw all the things
// sides.forEach((s) => s.show());
// stroke(0, 255, 0);
// strokeWeight(20);
point(vertices[0].a.x + xInt, vertices[0].a.y + yInt);
vertices[4] = new Vertex(vertices[0].a.x + xInt, vertices[0].a.y + yInt);
sides[4] = new Side(
vertices[1].a.x,
vertices[1].a.y,
vertices[0].a.x + xInt,
vertices[0].a.y + yInt,
"blue"
);
sides[5] = new Side(
vertices[0].a.x,
vertices[0].a.y,
vertices[0].a.x + xInt,
vertices[0].a.y + yInt,
"purple"
);
scale(2); // so I can make the triangle actually *visible*
translate(-width / 3, -height / 4);
sides[0].show();
sides[4].show();
sides[5].show();
vertices[0].show();
vertices[1].show();
vertices[4].show();
strokeWeight(1);
stroke(255, 0, 0);
noFill();
arc(vertices[0].a.x, vertices[0].a.y, 40, 40, -1 * angleOne.value(), 0, PIE);
arc(
vertices[1].a.x,
vertices[1].a.y,
40,
40, -180, -(180 - angleTwo.value()),
PIE
);
let P1x = vertices[0].a.x;
let P1y = vertices[0].a.y;
let P2x = vertices[1].a.x;
let P2y = vertices[1].a.y;
let P3x = vertices[4].a.x;
let P3y = vertices[4].a.y;
stroke(255, 255, 0);
stroke("purple"); // Change the color
strokeWeight(5); // Make the points 10 pixels in size
let P0 = createVector(P1x + 60, P1y - 40);
let V0 = createVector(0, -15);
point(P0.x, P0.y);
stroke(255, 0, 0);
point(P0.x + V0.x, P0.y + V0.y);
strokeWeight(2);
stroke("purple");
line(P0.x, P0.y, P0.x + V0.x, P0.y + V0.y);
// console.log(P1x,P1y,P2x,P2y,P3x,P3y);
let A1 = P3y - P1y;
let B1 = -(P3x - P1x);
let C1 = A1 * P1x + B1 * P1y;
let A2 = -(P3y - P2y);
let B2 = P3x - P2x;
let C2 = A2 * P2x + B2 * P2y;
let A3 = -(P2y - P1y);
let B3 = P2x - P1x;
let C3 = A3 * P2x + B3 * P2y;
let t1 = (C1 - A1 * P0.x - B1 * P0.y) / (A1 * V0.x + B1 * P0.y);
let t2 = (C2 - A2 * P0.x - B2 * P0.y) / (A2 * V0.x + B2 * P0.y);
let t3 = (C3 - A3 * P0.x - B3 * P0.y) / (A3 * V0.x + B3 * P0.y);
let times = [t1, t2, t3];
let posTimes = [];
for (let i = 0; i < times.length; i++) {
times[i] = round(times[i], 2);
}
// console.log("After rounding:", times);
for (let i = 0; i < times.length; i++) {
if (times[i] > 0) {
posTimes.push(times[i]);
}
}
// console.log("posTimes:", posTimes);
trueTime = min(posTimes);
if (trueTime == round(t1, 2)) {
fill("Blue");
text("Hit Blue", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
} else if (trueTime == round(t2, 2)) {
fill("Green");
text("Hit Green", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
} else {
fill("Purple");
text("Hit Purple", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
}
}
class Side {
constructor(x1, y1, x2, y2, col = "black") {
this.a = createVector(x1, y1);
this.b = createVector(x2, y2);
this.color = col;
}
show() {
stroke(this.color);
strokeWeight(4);
line(this.a.x, this.a.y, this.b.x, this.b.y);
}
}
class Vertex {
constructor(x1, y1) {
this.a = createVector(x1, y1);
}
show() {
stroke(255, 0, 0);
strokeWeight(10);
point(this.a.x, this.a.y);
}
}
html, body { margin: 0; padding: 0; overflow: hidden }
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.min.js"></script>

I couldn't figure out your math. I think you should try annotating this kind of code with explanatory comments. Often that will help you spot your own mistake:
let A1 = P3y - P1y;
let B1 = -(P3x - P1x);
let C1 = A1 * P1x + B1 * P1y;
let A2 = -(P3y - P2y);
let B2 = P3x - P2x;
let C2 = A2 * P2x + B2 * P2y;
let A3 = -(P2y - P1y);
let B3 = P2x - P1x;
let C3 = A3 * P2x + B3 * P2y;
let t1 = (C1 - A1 * P0.x - B1 * P0.y) / (A1 * V0.x + B1 * P0.y);
let t2 = (C2 - A2 * P0.x - B2 * P0.y) / (A2 * V0.x + B2 * P0.y);
let t3 = (C3 - A3 * P0.x - B3 * P0.y) / (A3 * V0.x + B3 * P0.y);
Here's some working math. It may not be the most dense/elegant, but I tried to explain it step by step from basic algebra:
// Find which of
//
// P1 to P2 = green
// P1 to P3 = purple
// P2 to P3 = blue
//
// PO to PO + V0 intersects with
// Find the intersection point between lines A and B
function intersection(Ax1, Ay1, Ax2, Ay2, Bx1, By1, Bx2, By2) {
// Calculate the slope of line A
let Am = (Ay2 - Ay1) / (Ax2 - Ax1);
// Calculate the y-intercept of line A
let Ab = Ay1 - Ax1 * Am;
// slope of line B
let Bm = (By2 - By1) / (Bx2 - Bx1);
// y-intercept of line B
let Bb = By1 - Bx1 * Bm;
if (Am === Bm) {
// Parallel lines
return;
}
if (!Number.isFinite(Am)) {
// Line A is vertical
if (!Number.isFinite(Bm)) {
// Line B is also vertical (Am may not equal Bm though because Infinity != NegativeInfinity)
return;
} else {
// Since line A is vertical, intersection point will lie along the same x position as Ax1 and Ax2
const xInt = Ax1;
// Simply use the equation for line segment B to find the corresponding Y value
const yInt = Bm * xInt + Bb;
return createVector(xInt, yInt);
}
} else if (!Number.isFinite(Bm)) {
// Line B is vertical
const xInt = Bx1;
const yInt = Am * xInt + Ab;
return createVector(xInt, yInt);
} else {
// Derived from Am * x + Ab = Bm * x + Bb
const xInt = (Bb - Ab) / (Am - Bm);
const yInt = Am * xInt + Ab;
return createVector(xInt, yInt);
}
}
let P1toP2int =
intersection(P1.x, P1.y, P2.x, P2.y, P0.x, P0.y, P0.x + V0.x, P0.y + V0.y);
let P1toP3int =
intersection(P1.x, P1.y, P3.x, P3.y, P0.x, P0.y, P0.x + V0.x, P0.y + V0.y);
let P2toP3int =
intersection(P2.x, P2.y, P3.x, P3.y, P0.x, P0.y, P0.x + V0.x, P0.y + V0.y);
// These intersection points assume that all lines point infinitely in both
// directions, so we still have some more work to do.
// Check if each of these points is within the target segment
function isWithin(lineX1, lineY1, lineX2, lineY2, xInt, yInt) {
if (abs((lineY2 - lineY1) / (lineX2 - lineX1)) > 1) {
// If the line segment is more vertical, check the Y position
return yInt >= min(lineY1, lineY2) && yInt <= max(lineY1, lineY2);
} else {
return xInt >= min(lineX1, lineX2) && xInt <= max(lineX1, lineX2);
}
}
if (P1toP2int && !isWithin(P1.x, P1.y, P2.x, P2.y, P1toP2int.x, P1toP2int.y)) {
P1toP2int = undefined;
}
if (P1toP3int && !isWithin(P1.x, P1.y, P3.x, P3.y, P1toP3int.x, P1toP3int.y)) {
P1toP3int = undefined;
}
if (P2toP3int && !isWithin(P2.x, P2.y, P3.x, P3.y, P2toP3int.x, P2toP3int.y)) {
P2toP3int = undefined;
}
// Check if each intersection point is in the direction our ray is pointing
function isOnRay(rayX0, rayY0, rayX1, rayY1, xInt, yInt) {
// If the ray is more vertical, check the y coordinates
if (abs((rayY1 - rayY0) / (rayX1 - rayX0)) > 1) {
// If the ray is pointing in the positive Y direction
// (rayY1 > rayY0) then the yInt must be on the positive
// side of rayY0; and vice versa
return (rayY1 > rayY0) === (yInt > rayY0);
} else {
return (rayX1 > rayX0) === (xInt > rayX0);
}
}
if (P1toP2int && !isOnRay(P0.x, P0.y, P0.x + V0.x, P0.y + V0.y, P1toP2int.x, P1toP2int.y)) {
P1toP2int = undefined;
}
if (P1toP3int && !isOnRay(P0.x, P0.y, P0.x + V0.x, P0.y + V0.y, P1toP3int.x, P1toP3int.y)) {
P1toP3int = undefined;
}
if (P2toP3int && !isOnRay(P0.x, P0.y, P0.x + V0.x, P0.y + V0.y, P2toP3int.x, P2toP3int.y)) {
P2toP3int = undefined;
}
let angle = 0;
let sides = [];
let vertices = [];
const len = 100;
let angleOne;
let angleTwo;
let angleThree;
function setup() {
createCanvas(windowWidth, windowHeight);
angleMode(DEGREES);
angleOne = createSlider(0, 89, 60);
angleOne.position(10, 10);
angleOne.style("width", "80px");
angleTwo = createSlider(0, 89, 60);
angleTwo.position(10, 30);
angleTwo.style("width", "80px");
angleThree = createSlider(0, 360, 0);
angleThree.position(10, 50);
angleThree.style("width", "80px");
// Initial vertice & side setup (these don't change)
let v1 = createVector(width / 2 - len / 2, height * 0.7);
let v2 = createVector(width / 2 + len / 2, height * 0.7);
sides[0] = new Side(v1.x, v1.y, v2.x, v2.y, "green");
vertices[0] = new Vertex(v1.x, v1.y);
vertices[1] = new Vertex(v2.x, v2.y);
}
function draw() {
background(255);
let angOne = angleOne.value();
let angTwo = angleTwo.value();
let rayAngle = angleThree.value();
fill(0);
strokeWeight(0);
textSize(15);
text(angOne, 100, 25);
text(angTwo, 100, 45);
text(rayAngle, 100, 65);
let v2Offset = createVector(len * cos(-angOne), len * sin(-angOne));
let v3Offset = createVector(-len * cos(angTwo), -len * sin(angTwo));
vertices[2] = new Vertex(
vertices[0].a.x + v2Offset.x,
vertices[0].a.y + v2Offset.y
);
vertices[3] = new Vertex(
vertices[1].a.x + v3Offset.x,
vertices[1].a.y + v3Offset.y
);
// Update the sides
sides[1] = new Side(
vertices[0].a.x,
vertices[0].a.y,
vertices[2].a.x,
vertices[2].a.y
);
sides[3] = new Side(
vertices[1].a.x,
vertices[1].a.y,
vertices[3].a.x,
vertices[3].a.y
);
const m1 =
(vertices[2].a.y - vertices[0].a.y) / (vertices[2].a.x - vertices[0].a.x);
const m2 =
(vertices[3].a.y - vertices[1].a.y) / (vertices[3].a.x - vertices[1].a.x);
// Calculate the y-offset relative to vertices[0]
const b2 = (vertices[1].a.x - vertices[0].a.x) * -m2;
const xInt = b2 / (m1 - m2);
const yInt = xInt * m1;
// Note xInt and yInt are relative to vertices[0]
// draw all the things
// sides.forEach((s) => s.show());
// stroke(0, 255, 0);
// strokeWeight(20);
point(vertices[0].a.x + xInt, vertices[0].a.y + yInt);
vertices[4] = new Vertex(vertices[0].a.x + xInt, vertices[0].a.y + yInt);
sides[4] = new Side(
vertices[1].a.x,
vertices[1].a.y,
vertices[4].a.x,
vertices[4].a.y,
"blue"
);
sides[5] = new Side(
vertices[0].a.x,
vertices[0].a.y,
vertices[4].a.x,
vertices[4].a.y,
"purple"
);
scale(2); // so I can make the triangle actually *visible*
translate(-width / 3, -height / 4);
sides[0].show();
sides[4].show();
sides[5].show();
vertices[0].show();
vertices[1].show();
vertices[4].show();
strokeWeight(1);
stroke(255, 0, 0);
noFill();
arc(vertices[0].a.x, vertices[0].a.y, 40, 40, -1 * angleOne.value(), 0, PIE);
arc(
vertices[1].a.x,
vertices[1].a.y,
40,
40, -180, -(180 - angleTwo.value()),
PIE
);
let P1 = vertices[0].a;
let P2 = vertices[1].a;
let P3 = vertices[4].a;
stroke(255, 255, 0);
stroke("purple"); // Change the color
strokeWeight(5); // Make the points 10 pixels in size
let P0 = createVector(P1.x + 60, P1.y - 40);
let V0 = createVector(15 * cos(rayAngle), 15 * sin(rayAngle));
point(P0.x, P0.y);
stroke(255, 0, 0);
point(P0.x + V0.x, P0.y + V0.y);
strokeWeight(2);
stroke("purple");
line(P0.x, P0.y, P0.x + V0.x, P0.y + V0.y);
// console.log(P1x,P1y,P2x,P2y,P3x,P3y);
// Find which of
//
// P1 to P2 = green
// P1 to P3 = purple
// P2 to P3 = blue
//
// PO to PO + V0 intersects with
// Find the intersection point between lines A and B
function intersection(Ax1, Ay1, Ax2, Ay2, Bx1, By1, Bx2, By2) {
// Calculate the slope of line A
let Am = (Ay2 - Ay1) / (Ax2 - Ax1);
// Calculate the y-intercept of line A
let Ab = Ay1 - Ax1 * Am;
// slope of line B
let Bm = (By2 - By1) / (Bx2 - Bx1);
// y-intercept of line B
let Bb = By1 - Bx1 * Bm;
if (Am === Bm) {
// Parallel lines
return;
}
if (!Number.isFinite(Am)) {
// Line A is vertical
if (!Number.isFinite(Bm)) {
// Line B is also vertical (Am may not equal Bm though because Infinity != NegativeInfinity)
return;
} else {
// Since line A is vertical, intersection point will lie along the same x position as Ax1 and Ax2
const xInt = Ax1;
// Simply use the equation for line segment B to find the corresponding Y value
const yInt = Bm * xInt + Bb;
return createVector(xInt, yInt);
}
} else if (!Number.isFinite(Bm)) {
// Line B is vertical
const xInt = Bx1;
const yInt = Am * xInt + Ab;
return createVector(xInt, yInt);
} else {
// Derived from Am * x + Ab = Bm * x + Bb
const xInt = (Bb - Ab) / (Am - Bm);
const yInt = Am * xInt + Ab;
return createVector(xInt, yInt);
}
}
let P1toP2int =
intersection(P1.x, P1.y, P2.x, P2.y, P0.x, P0.y, P0.x + V0.x, P0.y + V0.y);
let P1toP3int =
intersection(P1.x, P1.y, P3.x, P3.y, P0.x, P0.y, P0.x + V0.x, P0.y + V0.y);
let P2toP3int =
intersection(P2.x, P2.y, P3.x, P3.y, P0.x, P0.y, P0.x + V0.x, P0.y + V0.y);
// These intersection points assume that all lines point infinitely in both
// directions, so we still have some more work to do.
// Check if each of these points is within the target segment
function isWithin(lineX1, lineY1, lineX2, lineY2, xInt, yInt) {
if (abs((lineY2 - lineY1) / (lineX2 - lineX1)) > 1) {
// If the line segment is more vertical, check the Y position
return yInt >= min(lineY1, lineY2) && yInt <= max(lineY1, lineY2);
} else {
return xInt >= min(lineX1, lineX2) && xInt <= max(lineX1, lineX2);
}
}
if (P1toP2int && !isWithin(P1.x, P1.y, P2.x, P2.y, P1toP2int.x, P1toP2int.y)) {
P1toP2int = undefined;
}
if (P1toP3int && !isWithin(P1.x, P1.y, P3.x, P3.y, P1toP3int.x, P1toP3int.y)) {
P1toP3int = undefined;
}
if (P2toP3int && !isWithin(P2.x, P2.y, P3.x, P3.y, P2toP3int.x, P2toP3int.y)) {
P2toP3int = undefined;
}
// Check if each intersection point is in the direction our ray is pointing
function isOnRay(rayX0, rayY0, rayX1, rayY1, xInt, yInt) {
// If the ray is more vertical, check the y coordinates
if (abs((rayY1 - rayY0) / (rayX1 - rayX0)) > 1) {
// If the ray is pointing in the positive Y direction
// (rayY1 > rayY0) then the yInt must be on the positive
// side of rayY0; and vice versa
return (rayY1 > rayY0) === (yInt > rayY0);
} else {
return (rayX1 > rayX0) === (xInt > rayX0);
}
}
if (P1toP2int && !isOnRay(P0.x, P0.y, P0.x + V0.x, P0.y + V0.y, P1toP2int.x, P1toP2int.y)) {
P1toP2int = undefined;
}
if (P1toP3int && !isOnRay(P0.x, P0.y, P0.x + V0.x, P0.y + V0.y, P1toP3int.x, P1toP3int.y)) {
P1toP3int = undefined;
}
if (P2toP3int && !isOnRay(P0.x, P0.y, P0.x + V0.x, P0.y + V0.y, P2toP3int.x, P2toP3int.y)) {
P2toP3int = undefined;
}
// Only one of these should be true, except perhaps if the ray passes precisely through a corner
if (P1toP2int) {
stroke("Red");
strokeWeight(8);
point(P1toP2int.x, P1toP2int.y);
fill("Green");
noStroke();
text("Hit Green", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
}
if (P1toP3int) {
stroke("Red");
strokeWeight(8);
point(P1toP3int.x, P1toP3int.y);
fill("Purple");
noStroke();
text("Hit Purple", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
}
if (P2toP3int) {
stroke("Red");
strokeWeight(8);
point(P2toP3int.x, P2toP3int.y);
fill("Blue");
noStroke();
text("Hit Blue", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
}
/* I don't understand this math at all
let A1 = P3y - P1y;
let B1 = -(P3x - P1x);
let C1 = A1 * P1x + B1 * P1y;
let A2 = -(P3y - P2y);
let B2 = P3x - P2x;
let C2 = A2 * P2x + B2 * P2y;
let A3 = -(P2y - P1y);
let B3 = P2x - P1x;
let C3 = A3 * P2x + B3 * P2y;
let t1 = (C1 - A1 * P0.x - B1 * P0.y) / (A1 * V0.x + B1 * P0.y);
let t2 = (C2 - A2 * P0.x - B2 * P0.y) / (A2 * V0.x + B2 * P0.y);
let t3 = (C3 - A3 * P0.x - B3 * P0.y) / (A3 * V0.x + B3 * P0.y);
let times = [t1, t2, t3];
let posTimes = [];
for (let i = 0; i < times.length; i++) {
times[i] = round(times[i], 2);
}
// console.log("After rounding:", times);
for (let i = 0; i < times.length; i++) {
if (times[i] > 0) {
posTimes.push(times[i]);
}
}
// console.log("posTimes:", posTimes);
trueTime = min(posTimes);
if (trueTime == round(t1, 2)) {
fill("Blue");
text("Hit Blue", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
} else if (trueTime == round(t2, 2)) {
fill("Green");
text("Hit Green", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
} else {
fill("Purple");
text("Hit Purple", vertices[1].a.x + 50, max(50, vertices[1].a.y - 50));
}
*/
}
class Side {
constructor(x1, y1, x2, y2, col = "black") {
this.a = createVector(x1, y1);
this.b = createVector(x2, y2);
this.color = col;
}
show() {
stroke(this.color);
strokeWeight(4);
line(this.a.x, this.a.y, this.b.x, this.b.y);
}
}
class Vertex {
constructor(x1, y1) {
this.a = createVector(x1, y1);
}
show() {
stroke(255, 0, 0);
strokeWeight(10);
point(this.a.x, this.a.y);
}
}
html, body { margin: 0; padding: 0; overflow: hidden }
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.min.js"></script>

The intersection of a ray (R1, R2) and a line (L1, L2) can be calculated using the Dot Product:
N = (L1.y - L2.y, L2.x - L1.x)
t = dot(R1-L1, N) / dot(L2-L1, N)
X = R1 + (R2-R1) * t (if 0 <= t <= 1 and dot(R2-R1, X-R1) > 0)
In p5.js this can be implemented using p5.Vector:
function intersect_ray_line_segment(r1, r2, l1, l2) {
let R = p5.Vector.sub(r2, r1);
let L = p5.Vector.sub(l2, l1);
let N = p.createVector(-R.y, R.x);
if (L.dot(N) == 0) {
return undefined; // parallel
}
let t = p5.Vector.sub(r1, l1).dot(N) / L.dot(N);
if (t < 0 || t > 1) {
return undefined; // intersection is not on line segment
}
let X = L.mult(t).add(l1);
if (R.dot(p5.Vector.sub(X, r1)) < 0) {
return undefined; // wrong direction
}
return X;
}
Example:
var sketch = function( p ) {
p.setup = function() {
let sketchCanvas = p.createCanvas(p.windowWidth, p.windowHeight);
}
p.windowResized = function() {
p.resizeCanvas(p.windowWidth, p.windowHeight);
}
function intersect_ray_line_segment(r1, r2, l1, l2) {
let R = p5.Vector.sub(r2, r1);
let L = p5.Vector.sub(l2, l1);
let N = p.createVector(-R.y, R.x);
if (L.dot(N) == 0) {
return undefined; // parallel
}
let t = p5.Vector.sub(r1, l1).dot(N) / L.dot(N);
if (t < 0 || t > 1) {
return undefined; // intersection is not on line segment
}
let X = L.mult(t).add(l1);
if (R.dot(p5.Vector.sub(X, r1)) < 0) {
return undefined; // wrong direction
}
return X;
}
function closest_hit_point(r1, r2, points, lines) {
let hit_p = undefined;
let dist = undefined;
let N = undefined;
for (let i = 0; i < lines.length; ++i) {
let l1 = points[lines[i][0]];
let l2 = points[lines[i][1]];
let new_hit_p = intersect_ray_line_segment(r1, r2, points[lines[i][0]], points[lines[i][1]]);
if (new_hit_p) {
let new_d = p5.Vector.dist(r1, new_hit_p);
if (new_d > 0.1 && (!dist || new_d < dist)) {
dist = new_d;
hit_p = new_hit_p;
N = p.createVector(l1.y - l2.y, l2.x - l1.x);
}
}
}
return hit_p ? [hit_p, N] : undefined;
}
p.draw = function() {
let sx = p.width / 2;
let sy = p.height / 2;
let points = [
p.createVector(-sx*0.7, -sy*0.2), p.createVector(-sx*0.7, +sy*0.2),
p.createVector(-sx*0.3, -sy*0.5), p.createVector(sx*0.4, -sy*0.5),
p.createVector(sx*0.6, -sy*0.3), p.createVector(sx*0.6, sy*0.5), p.createVector(-sx*0.3, sy*0.5)];
let lines = [[0, 1], [2, 3], [3, 4], [4, 5], [5, 6]]
let center = new p5.Vector(0, 0);
let mouse_p = p.createVector(p.mouseX - sx, p.mouseY - sy);
let direction = p.createVector(mouse_p.x - center.x, mouse_p.y - center.y);
let hit_points = [center]
if (center.x != mouse_p.x || center.y != mouse_p.y) {
let start = center;
result = closest_hit_point(center, mouse_p, points, lines);
let count = 0
while (result && count < 100) {
hit_points.push(result[0]);
direction = direction.reflect(result[1]);
result = closest_hit_point(result[0], p5.Vector.add(result[0], direction), points, lines);
count ++;
}
}
direction.normalize();
hit_points.push(direction.mult( new p5.Vector(p.width, p.height).mag()).add(hit_points[hit_points.length-1]));
p.translate(p.width/2, p.height/2);
p.background(192);
p.strokeWeight(3);
p.stroke(64, 64, 255);
p.fill(128, 128, 255);
for (let i = 0; i < lines.length; ++i) {
let p0 = points[lines[i][0]];
let p1 = points[lines[i][1]];
p.line(p0.x, p0.y, p1.x, p1.y);
}
for (let i = 0; i < points.length; ++i) {
p.ellipse(points[i].x, points[i].y, 10, 10);
}
p.stroke(0, 0, 0);
p.fill(128, 128, 128);
p.ellipse(center.x, center.y, 10, 10);
for (let i = 1; i < hit_points.length; ++i) {
p.line(hit_points[i-1].x, hit_points[i-1].y, hit_points[i].x, hit_points[i].y);
}
for (let i = 0; i < hit_points.length; ++i) {
p.ellipse(hit_points[i].x, hit_points[i].y, 10, 10);
}
}
};
var circle_3_pts = new p5(sketch);
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.min.js"></script>
Demo

Related

Draw a line profile for an image using canvas html5

I am trying to draw intensity profile for an image with x axis as the length of the line on the image and the y-axis with intensity values along the length of the line. How can i do this on html 5 canvas? I tried the below code but I am not getting the right intensity values. Not sure where i am going wrong.
private getLineIntensityVals = function (lineObj, img) {
const slope = this.calculateSlopeOfLine(lineObj.upPos, lineObj.downPos);
const intercept = this.calculateIntercept(lineObj.downPos, slope);
const ctx = img.getContext('2d');
const coordinates = [];
const intensities = [];
for (let x = lineObj.downPos.x; x <= lineObj.upPos.x; x++) {
const y = slope * x + intercept;
const pixelData = ctx.getImageData(x, y, 1, 1).data;
pixelData[0] = 255 - pixelData[0];
pixelData[1] = 255 - pixelData[1];
pixelData[2] = 255 - pixelData[2];
const intensity = ((0.299 * pixelData[0]) + (0.587 * pixelData[1]) + (0.114 * pixelData[2]));
intensities.push(intensity);
}
return intensities;
};
private calculateSlopeOfLine = function (upPos, downPos) {
if (upPos.x === downPos.x || upPos.y === downPos.y) {
return null;
}
return (downPos.y - upPos.y) / (downPos.x - upPos.x);
};
private calculateIntercept = function (startPoint, slope) {
if (slope === null) {
return startPoint.x;
}
return startPoint.y - slope * startPoint.x;
};
private calculateLineLength(line) {
const dim = {width: Math.abs(line.downPos.x -line.upPos.x),height:Math.abs(line.downPos.y- line.upPos.y)};
length = Math.sqrt(Math.pow(dim.width, 2) + Math.pow(dim.height, 2));
return length;
};
Image data
Don't get the image data one pixel at a time. Gaining access to pixel data is expensive (CPU cycles), and memory is cheap. Get all the pixels once and reuse that data.
Sampling the data
Most lines will not fit into pixels evenly. To solve divide the line into the number of samples you want (You can use the line length)
Then step to each sample in turn getting the 4 neighboring pixels values and interpolating the color at the sample point.
As we are interpolating we need to ensure that we do not use the wrong color model. In this case we use sRGB.
We thus get the function
// imgData is the pixel date
// x1,y1 and x2,y2 are the line end points
// sampleRate is number of samples per pixel
// Return array 3 values for each sample.
function getProfile(imgData, x1, y1, x2, y2, sampleRate) {
// convert line to vector
const dx = x2 - x1;
const dy = y2 - y1;
// get length and calculate number of samples for sample rate
const samples = (dx * dx + dy * dy) ** 0.5 * Math.abs(sampleRate) + 1 | 0;
// Divide line vector by samples to get x, and y step per sample
const nx = dx / samples;
const ny = dy / samples;
const w = imgData.width;
const h = imgData.height;
const pixels = imgData.data;
const values = [];
// Offset line to center of pixel
var x = x1 + 0.5;
var y = y1 + 0.5;
var i = samples;
while (i--) { // for each sample
// make sure we are in the image
if (x >= 0 && x < w - 1 && y >= 0 && y < h - 1) {
// get 4 closest pixel indexes
const idxA = ((x | 0) + (y | 0) * w) * 4;
const idxB = ((x + 1 | 0) + (y | 0) * w) * 4;
const idxC = ((x + 1 | 0) + (y + 1 | 0) * w) * 4;
const idxD = ((x | 0) + (y + 1 | 0) * w) * 4;
// Get channel data using sRGB approximation
const r1 = pixels[idxA] ** 2.2;
const r2 = pixels[idxB] ** 2.2;
const r3 = pixels[idxC] ** 2.2;
const r4 = pixels[idxD] ** 2.2;
const g1 = pixels[idxA + 1] ** 2.2;
const g2 = pixels[idxB + 1] ** 2.2;
const g3 = pixels[idxC + 1] ** 2.2;
const g4 = pixels[idxD + 1] ** 2.2;
const b1 = pixels[idxA + 2] ** 2.2;
const b2 = pixels[idxB + 2] ** 2.2;
const b3 = pixels[idxC + 2] ** 2.2;
const b4 = pixels[idxD + 2] ** 2.2;
// find value at location via linear interpolation
const xf = x % 1;
const yf = y % 1;
const rr = (r2 - r1) * xf + r1;
const gg = (g2 - g1) * xf + g1;
const bb = (b2 - b1) * xf + b1;
/// store channels as uncompressed sRGB
values.push((((r3 - r4) * xf + r4) - rr) * yf + rr);
values.push((((g3 - g4) * xf + g4) - gg) * yf + gg);
values.push((((b3 - b4) * xf + b4) - bb) * yf + bb);
} else {
// outside image
values.push(0,0,0);
}
// step to next sample
x += nx;
y += ny;
}
return values;
}
Conversion to values
The array hold raw sample data. There are a variety of ways to convert to a value. That is why we separate the sampling from the conversion to values.
The next function takes the raw sample array and converts it to values. It returns an array of values. While it is doing the conversion it also get the max value so that the data can be plotted to fit a graph.
function convertToMean(values) {
var i = 0, v;
const results = [];
results._max = 0;
while (i < values.length) {
results.push(v = (values[i++] * 0.299 + values[i++] * 0.587 + values[i++] * 0.114) ** (1/2.2));
results._max = Math.max(v, results._max);
}
return results;
}
Now you can plot the data how you like.
Example
Click drag line on image (when loaded)
Results are plotted real time.
Move mouse over plot to see values.
Use full page to see all.
const ctx = canvas.getContext("2d");
const ctx1 = canvas1.getContext("2d");
const SCALE_IMAGE = 0.5;
const PLOT_WIDTH = 500;
const PLOT_HEIGHT = 150;
canvas1.width = PLOT_WIDTH;
canvas1.height = PLOT_HEIGHT;
const line = {x1: 0, y1: 0, x2: 0, y2:0, canUse: false, haveData: false, data: undefined};
var bounds, bounds1, imgData;
// ix iy image coords, px, py plot coords
const mouse = {ix: 0, iy: 0, overImage: false, px: 0, py:0, overPlot: false, button : false, dragging: 0};
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const img = new Image;
img.crossOrigin = "Anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Black_and_yellow_garden_spider%2C_Washington_DC.jpg/800px-Black_and_yellow_garden_spider%2C_Washington_DC.jpg";
img.addEventListener("load",() => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img,0,0);
imgData = ctx.getImageData(0,0,ctx.canvas.width, ctx.canvas.height);
canvas.width = img.width * SCALE_IMAGE;
canvas.height = img.height * SCALE_IMAGE;
bounds = canvas.getBoundingClientRect();
bounds1 = canvas1.getBoundingClientRect();
requestAnimationFrame(update);
},{once: true});
function getProfile(imgData, x1, y1, x2, y2, sampleRate) {
x1 *= 1 / SCALE_IMAGE;
y1 *= 1 / SCALE_IMAGE;
x2 *= 1 / SCALE_IMAGE;
y2 *= 1 / SCALE_IMAGE;
const dx = x2 - x1;
const dy = y2 - y1;
const samples = (dx * dx + dy * dy) ** 0.5 * Math.abs(sampleRate) + 1 | 0;
const nx = dx / samples;
const ny = dy / samples;
const w = imgData.width;
const h = imgData.height;
const pixels = imgData.data;
const values = [];
var x = x1 + 0.5;
var y = y1 + 0.5;
var i = samples;
while (i--) {
if (x >= 0 && x < w - 1 && y >= 0 && y < h - 1) {
// get 4 closest pixel indexs
const idxA = ((x | 0) + (y | 0) * w) * 4;
const idxB = ((x + 1 | 0) + (y | 0) * w) * 4;
const idxC = ((x + 1 | 0) + (y + 1 | 0) * w) * 4;
const idxD = ((x | 0) + (y + 1 | 0) * w) * 4;
// Get channel data using sRGB approximation
const r1 = pixels[idxA] ** 2.2;
const r2 = pixels[idxB] ** 2.2;
const r3 = pixels[idxC] ** 2.2;
const r4 = pixels[idxD] ** 2.2;
const g1 = pixels[idxA + 1] ** 2.2;
const g2 = pixels[idxB + 1] ** 2.2;
const g3 = pixels[idxC + 1] ** 2.2;
const g4 = pixels[idxD + 1] ** 2.2;
const b1 = pixels[idxA + 2] ** 2.2;
const b2 = pixels[idxB + 2] ** 2.2;
const b3 = pixels[idxC + 2] ** 2.2;
const b4 = pixels[idxD + 2] ** 2.2;
// find value at location via linear interpolation
const xf = x % 1;
const yf = y % 1;
const rr = (r2 - r1) * xf + r1;
const gg = (g2 - g1) * xf + g1;
const bb = (b2 - b1) * xf + b1;
/// store channels as uncompressed sRGB
values.push((((r3 - r4) * xf + r4) - rr) * yf + rr);
values.push((((g3 - g4) * xf + g4) - gg) * yf + gg);
values.push((((b3 - b4) * xf + b4) - bb) * yf + bb);
} else {
// outside image
values.push(0,0,0);
}
x += nx;
y += ny;
}
values._nx = nx;
values._ny = ny;
values._x = x1;
values._y = y1;
return values;
}
function convertToMean(values) {
var i = 0, max = 0, v;
const results = [];
while (i < values.length) {
results.push(v = (values[i++] * 0.299 + values[i++] * 0.587 + values[i++] * 0.114) ** (1/2.2));
max = Math.max(v, max);
}
results._max = max;
results._nx = values._nx;
results._ny = values._ny;
results._x = values._x;
results._y = values._y;
return results;
}
function plotValues(ctx, values) {
const count = values.length;
const scaleX = ctx.canvas.width / count;
// not using max in example
// const scaleY = (ctx.canvas.height-3) / values._max;
const scaleY = (ctx.canvas.height-3) / 255;
ctx1.clearRect(0,0, ctx.canvas.width, ctx.canvas.height);
var i = 0;
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
while (i < count) {
const y = ctx.canvas.height - values[i] * scaleY + 1;
ctx.lineTo(i++ * scaleX, y);
}
ctx.stroke();
if (!mouse.button && mouse.overPlot) {
ctx.fillStyle = "#f008";
ctx.fillRect(mouse.px, 0, 1, ctx.canvas.height);
const val = values[mouse.px / scaleX | 0];
info.textContent = "Value: " + (val !== undefined ? val.toFixed(2) : "");
}
}
function update() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, 0, 0, img.width * SCALE_IMAGE, img.height * SCALE_IMAGE);
var atSample = 0;
if (!mouse.button) {
if (line.canUse) {
if (line.haveData && mouse.overPlot) {
const count = line.data.length;
const scaleX = ctx1.canvas.width / count
atSample = mouse.px / scaleX;
}
}
}
if (mouse.button) {
if (mouse.dragging === 1) { // dragging line
line.x2 = mouse.ix;
line.y2 = mouse.iy;
line.canUse = true;
line.haveData = false;
} else if(mouse.overImage) {
mouse.dragging = 1;
line.x1 = mouse.ix;
line.y1 = mouse.iy;
line.canUse = false;
line.haveData = false;
canvas.style.cursor = "none";
}
} else {
mouse.dragging = 0;
canvas.style.cursor = "crosshair";
}
if (line.canUse) {
ctx.strokeStyle = "#F00";
ctx.strokeWidth = 2;
ctx.beginPath();
ctx.lineTo(line.x1, line.y1);
ctx.lineTo(line.x2, line.y2);
ctx.stroke();
if (atSample) {
ctx.fillStyle = "#FF0";
ctx.beginPath();
ctx.arc(
(line.data._x + line.data._nx * atSample) * SCALE_IMAGE,
(line.data._y + line.data._ny * atSample) * SCALE_IMAGE,
line.data[atSample | 0] / 32,
0, Math.PI * 2
);
ctx.fill();
}
if (!line.haveData) {
const vals = getProfile(imgData, line.x1, line.y1, line.x2, line.y2, 1);
line.data = convertToMean(vals);
line.haveData = true;
plotValues(ctx1, line.data);
} else {
plotValues(ctx1, line.data);
}
}
requestAnimationFrame(update);
}
function mouseEvents(e){
if (bounds) {
mouse.ix = e.pageX - bounds.left;
mouse.iy = e.pageY - bounds.top;
mouse.overImage = mouse.ix >= 0 && mouse.ix < bounds.width && mouse.iy >= 0 && mouse.iy < bounds.height;
mouse.px = e.pageX - bounds1.left;
mouse.py = e.pageY - bounds1.top;
mouse.overPlot = mouse.px >= 0 && mouse.px < bounds1.width && mouse.py >= 0 && mouse.py < bounds1.height;
}
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
canvas {
border: 2px solid black;
}
<canvas id="canvas"></canvas>
<div id="info">Click drag line over image</div>
<canvas id="canvas1"></canvas>
Image source: https://commons.wikimedia.org/w/index.php?curid=93680693 By BethGuay - Own work, CC BY-SA 4.0,

How to change color of circle when collision detection occurs?

As far as I understood it seems that even after changing the color when the collision is detected it reverts back to blue due to the else statement when it is compared between other circle and they are not colliding. So how would you solve this so that that the instance when the collision between any circle occurs it changes to red
collision detection
this.update = function() {
for (let i = 0; i < circles.length; i++) {
if (this !== circles[i] && getDistance(this.x, this.y, circles[i].x, circles[i].y) <= 200 * 200) {
this.c = 'red';
circles[i].c = 'red';
resolveCollision(this, circles[i]);
} else {
this.c = 'blue';
circles[i].c = 'blue';
}
}
//wall deflection
if (this.x - this.r <= 0 || this.x + this.r >= innerWidth)
this.v.x *= -1
if (this.y - this.r <= 0 || this.y + this.r >= innerHeight)
this.v.y *= -1
this.x += this.v.x;
this.y += this.v.y;
this.draw();
};
//deflection amongst other circles
function resolveCollision(circle, othercircle) {
const xVelocityDiff = circle.v.x - othercircle.v.x;
const yVelocityDiff = circle.v.y - othercircle.v.y;
const xDist = othercircle.x - circle.x;
const yDist = othercircle.y - circle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = -Math.atan2(othercircle.y - circle.y, othercircle.x - circle.x);
const m1 = circle.m;
const m2 = othercircle.m;
const u1 = rotate(circle.v, angle);
const u2 = rotate(othercircle.v, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
}
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
}
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
circle.v.x = vFinal1.x;
circle.v.y = vFinal1.y;
othercircle.v.x = vFinal2.x;
othercircle.v.y = vFinal2.y;
}
}
Semaphores
Use a semaphore that holds the collision state of the circle.
Thus in your Circle.prototype would have something like these functions and properties
Circle.prototype = {
collided: false, // when true change color
draw() {
ctx.strokeStyle = this.collided ? "red" : "blue";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
},
...
...
// in update
update() {
// when collision is detected set semaphore
if (collision) {
this.collided = true;
}
}
}
Counters
Or you may want to only have the color change last for some time. You can modify the semaphore and use it as a counter. On collision set it to the number of frames to change color for.
Circle.prototype = {
collided: 0,
draw() {
ctx.strokeStyle = this.collided ? (this.collided--, "red") : "blue";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
},
...
...
// in update
update() {
// when collision is detected set semaphore
if (collision) {
this.collided = 60; // 1 second at 60FPS
}
}
}
Example
This example is taken from another answer I did earlier this year.
As there is a lot of code I have highlighted the relevant code with
/*= ANSWER CODE ==============================================================
...
=============================================================================*/
The example uses counters and changes color for 30 frames after a collision with another ball or wall.
I did not use a semaphore as all the balls would be red within a second.
canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
mathExt(); // creates some additional math functions
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 10; // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 6;
const MAX_BALL_SIZE = 30;
const VEL_MIN = 1;
const VEL_MAX = 5;
const MAX_RESOLUTION_CYCLES = 100; // Put too many balls (or too large) in the scene and the
// number of collisions per frame can grow so large that
// it could block the page.
// If the number of resolution steps is above this value
// simulation will break and balls can pass through lines,
// get trapped, or worse. LOL
const SHOW_COLLISION_TIME = 30;
const balls = [];
const lines = [];
function Line(x1,y1,x2,y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
Line.prototype = {
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
},
reverse() {
const x = this.x1;
const y = this.y1;
this.x1 = this.x2;
this.y1 = this.y2;
this.x2 = x;
this.y2 = y;
return this;
}
}
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
this.r = r;
this.m = m
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
/*= ANSWER CODE ==============================================================*/
this.collided = 0;
/*============================================================================*/
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += GRAVITY;
},
draw() {
/*= ANSWER CODE ==============================================================*/
ctx.strokeStyle = this.collided ? (this.collided--, "#F00") : "#00F";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
/* ============================================================================*/
},
interceptLineTime(l, time) {
const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2, this.r);
if (u >= time && u <= 1) {
return u;
}
},
checkBallBallTime(t, minTime) {
return t > minTime && t <= 1;
},
interceptBallTime(b, time) {
const x = this.x - b.x;
const y = this.y - b.y;
const d = (x * x + y * y) ** 0.5;
if (d > this.r + b.r) {
const times = Math.circlesInterceptUnitTime(
this.x, this.y,
this.x + this.vx, this.y + this.vy,
b.x, b.y,
b.x + b.vx, b.y + b.vy,
this.r, b.r
);
if (times.length) {
if (times.length === 1) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
return;
}
if (times[0] <= times[1]) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
if(this.checkBallBallTime(times[1], time)) { return times[1] }
return
}
if(this.checkBallBallTime(times[1], time)) { return times[1] }
if(this.checkBallBallTime(times[0], time)) { return times[0] }
}
}
},
collideLine(l, time) {
/*= ANSWER CODE ==============================================================*/
this.collided = SHOW_COLLISION_TIME;
/*============================================================================*/
const x1 = l.x2 - l.x1;
const y1 = l.y2 - l.y1;
const d = (x1 * x1 + y1 * y1) ** 0.5;
const nx = x1 / d;
const ny = y1 / d;
const u = (this.vx * nx + this.vy * ny) * 2;
this.x += this.vx * time;
this.y += this.vy * time;
this.vx = (nx * u - this.vx) * WALL_LOSS;
this.vy = (ny * u - this.vy) * WALL_LOSS;
this.x -= this.vx * time;
this.y -= this.vy * time;
},
collide(b, time) { // b is second ball
/*= ANSWER CODE ==============================================================*/
this.collided = SHOW_COLLISION_TIME;
b.collided = SHOW_COLLISION_TIME;
/*============================================================================*/
const a = this;
const m1 = a.m;
const m2 = b.m;
a.x = a.x + a.vx * time;
a.y = a.y + a.vy * time;
b.x = b.x + b.vx * time;
b.y = b.y + b.vy * time;
const x = a.x - b.x
const y = a.y - b.y
const d = (x * x + y * y);
const u1 = (a.vx * x + a.vy * y) / d
const u2 = (x * a.vy - y * a.vx ) / d
const u3 = (b.vx * x + b.vy * y) / d
const u4 = (x * b.vy - y * b.vx ) / d
const mm = m1 + m2;
const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
b.vx = x * vu1 - y * u4;
b.vy = y * vu1 + x * u4;
a.vx = x * vu3 - y * u2;
a.vy = y * vu3 + x * u2;
a.x = a.x - a.vx * time;
a.y = a.y - a.vy * time;
b.x = b.x - b.vx * time;
b.y = b.y - b.vy * time;
},
doesOverlap(ball) {
const x = this.x - ball.x;
const y = this.y - ball.y;
return (this.r + ball.r) > ((x * x + y * y) ** 0.5);
}
}
function canAdd(ball) {
for(const b of balls) {
if (ball.doesOverlap(b)) { return false }
}
return true;
}
function create(bCount) {
lines.push(new Line(-10, 20, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 30)).reverse());
lines.push((new Line(30, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 30, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
while (tries--) {
const dir = Math.rand(0, Math.TAU);
const vel = Math.rand(VEL_MIN, VEL_MAX);
const ball = new Ball(
Math.rand(MAX_BALL_SIZE + 30, canvas.width - MAX_BALL_SIZE - 30),
Math.rand(MAX_BALL_SIZE + 30, canvas.height - MAX_BALL_SIZE - 30),
Math.cos(dir) * vel,
Math.sin(dir) * vel,
Math.rand(MIN_BALL_SIZE, MAX_BALL_SIZE),
);
if (canAdd(ball)) {
balls.push(ball);
break;
}
}
}
}
function resolveCollisions() {
var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
while (resolving && e++ < MAX_RESOLUTION_CYCLES) { // too main ball may create very lone resolution cycle. e limits this
resolving = false;
minObj = undefined;
minBall = undefined;
minTime = 1;
idx = 0;
for (const b of balls) {
idx1 = idx + 1;
while (idx1 < balls.length) {
const b1 = balls[idx1++];
const time = b.interceptBallTime(b1, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = b1;
minBall = b;
resolving = true;
}
}
}
for (const l of lines) {
const time = b.interceptLineTime(l, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = l;
minBall = b;
resolving = true;
}
}
}
idx ++;
}
if (resolving) {
if (minObj instanceof Ball) {
minBall.collide(minObj, minTime);
} else {
minBall.collideLine(minObj, minTime);
}
after = minTime;
}
}
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
resolveCollisions();
for (const b of balls) { b.update() }
for (const b of balls) { b.draw() }
ctx.lineWidth = 1;
ctx.strokeStyle = "#000";
ctx.beginPath();
for(const l of lines) { l.draw() }
ctx.stroke();
requestAnimationFrame(mainLoop);
}
function mathExt() {
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1
// contact points of two circles radius r1, r2 moving along two lines (a,e)-(b,f) and (c,g)-(d,h) [where (,) is coord (x,y)]
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
const A = a * a, B = b * b, C = c * c, D = d * d;
const E = e * e, F = f * f, G = g * g, H = h * h;
var R = (r1 + r2) ** 2;
const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
return Math.quadRoots(AA, BB, CC);
}
Math.quadRoots = (a, b, c) => { // find roots for quadratic
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2, r) => {
const xx = x2 - x1;
const yy = y2 - y1;
const d = vx * yy - vy * xx;
if (d > 0) { // only if moving towards the line
const dd = r / (xx * xx + yy * yy) ** 0.5;
const nx = xx * dd;
const ny = yy * dd;
return (xx * (y - (y1 + nx)) - yy * (x -(x1 - ny))) / d;
}
}
}
<canvas id="canvas"></canvas>
const collided = {
color: 'red',
get current() {
return this.color
},
set current(clr) {
if (this.color === 'red') {
this.color = 'blue'
} else {
this.color = 'red'
}
}
}
this.update= function(){
for(let i=0;i<circles.length;i++){
if(this!==circles[i] && getDistance(this.x,this.y,circles[i].x,circles[i].y)<=200*200){
this.c=collided.current
collided.current = this.c
circles[i].c=collided.current
resolveCollision(this,circles[i]);
}
// ...
}
}
The trick is to use use getters and setters to ensure that the most recently used color value is never reapplied
First, split the if:
// psudo-code
if this is not circles[i], then
if overlapping, then
do something
else
do something else
else
do nothing
Then, fix if this is not circles[i]:
{x:100,y:100}!={x:100,y:100}, so
either add id to circles and compare ids (my recommendation), or,
compare .xs and .ys (less desired - what if they are equal but not same circle?), or,
use JSON.stringify(a)==JSON.stringify(b).
I would add .overlapping and before the loop, I'd add another loop setting all .overlapping to false, then, withing the modified original loop, I'd check if .overlapping is false, and then if there is a collision, I'd set .overlapping to true for both.
Another way would be to create an array to hole overlapping circles' .ids and check if that array includes the current loop item's .id.

Mapping mouse coordinates with context

I have a webgl being rendered on a canvas element. After it is rendered I want to allow user to draw on it with a mouse (rect for example). Since the getContext does not work for the second time, I added another transparent canvas on top of my webgl canvas and I am want to draw a rect with a mouse on the transparent canvas. The problem is that the coordinates in a mousedown event are very different to the context corrdinates
My canvas are as below
<div id="container">
<canvas id="webglCanvas" tabindex='1'></canvas>
<canvas id="transCanvas" tabindex='1'></canvas>
</div>
to get context
var $canvas1 = document.getElementById('transCanvas');
var ctx = $canvas1.getContext("2d");
mouse down event of transCanvas. Please note that I have hard coded the rect at the moment on mouse down event. Later I will do it on mouse move etc. This works fine on my canvas and I can see the rect on my screen. But the mouse coordinates eg e.clientX and e.clientY are in hundereds and go off the screen?
function handleCanvasMouseMove(e) {
ctx.beginPath();
ctx.fillStyle = '#F30';
ctx.fillRect(75, 75, 75, 75);
}
Remember, you're converting NormalizedDeviceCoords with a range of [-1..1] in each axis to a position on screen. All the transformations you applied takes the model-space vert and essentially put it in a cube of size 2, centred on the origin.
So... I imagine you'd also like to get back mouse-coordinates in this same space. If so, it's just a matter of constructing a matrix and then multiplying the screen-space position by this matrix to get x,y in the range of [-1..1]
When I've done similar things in the past, I've used a series of transformations as follows:
function makeClipToPixMat(width,height)
{
// flip the Y
let s1 = mat4.scaling(1,-1,1);
// translate so space is now [0..2]
let t1 = mat4.translation(1,1,0);
// scale so space is now [0..width], [0..height]
let s2 = mat4.scaling(width/2,height/2,1);
// s1, then t1, then s2 are applied to the input coords
let result = mat4.matrixByMatrix(s1,t1);
result = result.multiply(s2);
return result;
}
But as you'll notice from the name, it's a mapping in the wrong direction. We want to map screen-coords to NDC, but this code does the opposite. What now then? Simple - either invert the matrix or determine the series of transforms needed and construct a matrix that will do them all in one go. It's a simple enough transform, that a matrix-inversion seems like a fantastically expensive way to do something so simple.
In fact, here's the function I use. Inversion works fine too and can decrease the code size at the expensive of run-time.
function pixelsToClipspace(width,height)
{
let scaleMat = mat4.scaling( 1/(width/2), -1/(height/2), 1);
let translateMat = mat4.translation(-width/2, -height/2, 0); //.transpose();
let mat = mat4.matrixByMatrix(translateMat, scaleMat);
return mat;
}
Since I've some time at the moment, I hacked together a quick demo for you. Shame, there's 380 lines of code for the vec4 and the matrix, yet only about 35 for the demo. :laughs: That said, it perfectly illustrates how expensive and complicated the matrix .inverse() function is.
LASTLY: please note, I do not make any claims as to the accuracy of any of the code included yet not utilized. Exercises like this one benefit each of us. You get some understanding, I get some more debug test-cases. :) The matrices are column-major (like all good GL ones should be)
"use strict";
window.addEventListener('load', onLoaded, false);
let s2Clip = null;
function onLoaded(evt)
{
let can = document.querySelector('canvas');
s2Clip = pixelsToClipspace(can.clientWidth, can.clientHeight); // use clienWidth/clientHeight to avoid CSS scaling problems
can.addEventListener('mousemove', onMouse, false);
}
function onMouse(evt)
{
var rawPos = new vec4(evt.offsetX, evt.offsetY, 0, 1);
var trPos = s2Clip.timesVector(rawPos);
document.getElementById('rawMouse').innerText = `${rawPos.x}, ${rawPos.y}`
document.getElementById('transMouse').innerText = `${trPos.x.toFixed(2)}, ${trPos.y.toFixed(2)}`
}
function pixelsToClipspace(width,height)
{
let scaleMat = mat4.scaling( 1/(width/2), -1/(height/2), 1);
let translateMat = mat4.translation(-width/2, -height/2, 0); //.transpose();
let mat = mat4.matrixByMatrix(translateMat, scaleMat);
return mat;
}
// </script>
// <script origSrc='vector.js'>
class vec4
{
// w=0 for dir (cant translate), w=1 for pos (can)
constructor(x=0,y=0,z=0,w=0){this.values = [x,y,z,w];}
clone(){ return new vec4(this.x,this.y,this.z,this.w); }
get x(){return this.values[0];}
get y(){return this.values[1];}
get z(){return this.values[2];}
get w(){return this.values[3];}
set x(x){this.values[0]=x;}
set y(y){this.values[1]=y;}
set z(z){this.values[2]=z;}
set w(w){this.values[3]=w;}
get length(){return Math.hypot( ...this.values ); }
normalize(){ var l = this.length; if (l>1e-6) {this.x/=l;this.y/=l;this.z/=l;this.w/=l;} return this;}
scaleBy(scalar){this.x*=scalar;this.y*=scalar;this.z*=scalar;this.w*=scalar;return this;}
divBy(scalar){this.x/=scalar;this.y/=scalar;this.z/=scalar;this.w/=scalar;return this;}
add(other){return new vec4(this.x+other.x, this.y+other.y, this.z+other.z, this.w+other.w);}
sub(other){return new vec4(this.x-other.x, this.y-other.y, this.z-other.z, this.w-other.w);}
get xyz(){return new vec3(this.x,this.y,this.z);}
toStringN(n){return `[${pad(this.x,n)}, ${pad(this.y,n)}, ${pad(this.z,n)}, ${pad(this.w,n)}]`;}
timesMatrix(matrix)
{
let m0 = matrix.getCol(0), m1 = matrix.getCol(1), m2 = matrix.getCol(2), m3 = matrix.getCol(3);
return new vec4(
(m0.x*this.x) + (m1.x*this.y) + m2.x*this.z + m3.x*this.w,
(m0.y*this.x) + (m1.y*this.y) + m2.y*this.z + m3.y*this.w,
(m0.z*this.x) + (m1.z*this.y) + m2.z*this.z + m3.z*this.w,
(m0.w*this.x) + (m1.w*this.y) + m2.w*this.z + m3.w*this.w
);
}
vecByMatrix(m) /// operator * (matrix, vector)
{
let mc0 = m.getCol(0), mc1=m.getCol(1), mc2=m.getCol(2), mc3=m.getCol(3);
return new vec4(
(mc0.x * this.x) + (mc1.x * this.y) + (mc2.x * this.z) + (mc3.x * this.w),
(mc0.y * this.x) + (mc1.y * this.y) + (mc2.y * this.z) + (mc3.y * this.w),
(mc0.z * this.x) + (mc1.z * this.y) + (mc2.z * this.z) + (mc3.z * this.w),
(mc0.w * this.x) + (mc1.w * this.y) + (mc2.w * this.z) + (mc3.w * this.w),
);
}
matrixByVec(m) /// operator * (vector, matrix)
{
let mCol0 = m.getCol(0), mCol1=m.getCol(1), mCol2=m.getCol(2), mCol3=m.getCol(3);
return new vec4(
this.x*mCol0.x + this.y*mCol0.y + this.z*mCol0.z + this.w*mCol0.w,
this.x*mCol1.x + this.y*mCol1.y + this.z*mCol1.z + this.w*mCol1.w,
this.x*mCol2.x + this.y*mCol2.y + this.z*mCol2.z + this.w*mCol2.w,
this.x*mCol3.x + this.y*mCol3.y + this.z*mCol3.z + this.w*mCol3.w
);
}
}
class mat4
{
constructor(xVec4=new vec4(1,0,0,0), yVec4=new vec4(0,1,0,0), zVec4=new vec4(0,0,1,0), wVec4=new vec4(0,0,0,1) )
{
this.columns = [
xVec4.clone(),
yVec4.clone(),
zVec4.clone(),
wVec4.clone()
];
}
getCol(colIndex) {return this.columns[colIndex];}
setCol(colIndex, newVec) {this.columns[colIndex] = newVec.clone();}
setIdentity()
{
let x=new vec4(1,0,0,0);
let y=new vec4(0,1,0,0);
let z=new vec4(0,0,1,0);
let w=new vec4(0,0,0,1);
this.setCol(0,x);
this.setCol(0,y);
this.setCol(0,z);
this.setCol(0,w);
return this;
}
static clone(other)
{
var result = new mat4( other.columns[0], other.columns[1], other.columns[2], other.columns[3] );
return result;
}
clone()
{
return mat4.clone(this);
}
static scaling(sx=1,sy=1,sz=1)
{
let x = new vec4(sx,0,0,);
let y = new vec4(0,sy,0,);
let z = new vec4(0,0,sz,);
let w = new vec4(0,0,0,1);
return new mat4(x,y,z,w);
}
static translation(tx=0,ty=0,tz=0)
{
let X = new vec4(1,0,0,tx);
let Y = new vec4(0,1,0,ty);
let Z = new vec4(0,0,1,tz);
let W = new vec4(0,0,0,1);
return new mat4(X,Y,Z,W);
}
static matrixByMatrix(m1, m2)
{
let mCol0 = m2.getCol(0), mCol1=m2.getCol(1), mCol2=m2.getCol(2), mCol3=m2.getCol(3);
let X = mCol0.vecByMatrix(m1);
let Y = mCol1.vecByMatrix(m1);
let Z = mCol2.vecByMatrix(m1);
let W = mCol3.vecByMatrix(m1);
return new mat4(X,Y,Z,W);
}
static matTimeMat(m1,m2)
{
let mc0=m2.getCol(0),mc1=m2.getCol(1),mc2=m2.getCol(2),mc3=m2.getCol(3);
let x = m1.timesVector(mc0);
let y = m1.timesVector(mc1);
let z = m1.timesVector(mc2);
let w = m1.timesVector(mc3);
return new mat4(x,y,z,w);
}
multiply(other,shouldPrepend=false)
{
var a=this,b=other,c;
if (shouldPrepend===true){a=other;b=this;}
c = mat4.matrixByMatrix(a,b);
this.columns = c.columns.slice();
return this;
}
translate(tx=0,ty=0,tz=0)
{
return this.multiply( mat4.translation(tx,ty,tz) );
}
setScale(sx=1,sy=1,sz=1)
{
let x = new vec4(sx,0,0,0);
let y = new vec4(0,sy,0,0);
let z = new vec4(0,0,sz,0);
let w = new vec4(0,0,0,1);
let tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
setTrans(tx=0,ty=0,tz=0)
{
let x = new vec4( 1, 0, 0, 0);
let y = new vec4( 0, 1, 0, 0);
let z = new vec4( 0, 0, 1, 0);
let w = new vec4( tx, ty, tz, 1);
var tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
setRotX(degrees)
{
let cosa = Math.cos(degrees * 3.141/180);
let sina = Math.sin(degrees * 3.141/180);
let x = new vec4(1,0,0,0);
let y = new vec4(0,cosa,sina,0)
let z = new vec4(0,-sina,cosa,0);
let w = new vec4(0,0,0,1);
let tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
setRotY(degrees)
{
let cosa = Math.cos(degrees * 3.141/180);
let sina = Math.sin(degrees * 3.141/180);
let x = new vec4( cosa, 0,-sina,0);
let y = new vec4( 0, 1, 0, 0)
let z = new vec4( sina, 0,cosa, 0);
let w = new vec4( 0, 0, 0, 1);
let tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
setRotZ(degrees)
{
let cosa = Math.cos(degrees * 3.141/180);
let sina = Math.sin(degrees * 3.141/180);
let x = new vec4(cosa,sina,0,0);
let y = new vec4(-sina,cosa,0,0)
let z = new vec4(0,0,1,0);
let w = new vec4(0,0,0,1);
let tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
scaleEach(sX=1,sY=1,sZ=1,shouldPrepend=false)
{
let tmp = new mat4();
let X = tmp.getCol(0);
X.x = sX;
tmp.setCol(0,X);
let Y = tmp.getCol(1);
Y.y = sY;
tmp.setCol(1,Y);
let Z = tmp.getCol(2);
Z.z = sZ;
tmp.setCol(2,Z);
return this.multiply(tmp, shouldPrepend);
//return this;
}
scaleAll(sXYZ, shouldPrepend=false)
{
return this.scaleEach(sXYZ,sXYZ,sXYZ,shouldPrepend);
//return this;
}
/*
translate(tX=0, tY=0, tZ=0, shouldPrepend=false)
{
let tmp = new mat4();
let W = tmp.getCol(3);
W.x = tX;
W.y = tY;
W.z = tZ;
tmp.setCol(3,W);
return this.multiply(tmp, shouldPrepend);
}
*/
timesVector(vector)
{
let m0=this.getCol(0), m1=this.getCol(1), m2=this.getCol(2), m3=this.getCol(3);
return new vec4(
(vector.x*m0.x) + (vector.y*m0.y) + (vector.z*m0.z) + (vector.w*m0.w),
(vector.x*m1.x) + (vector.y*m1.y) + (vector.z*m1.z) + (vector.w*m1.w),
(vector.x*m2.x) + (vector.y*m2.y) + (vector.z*m2.z) + (vector.w*m2.w),
(vector.x*m3.x) + (vector.y*m3.y) + (vector.z*m3.z) + (vector.w*m3.w)
);
}
toString()
{
let result = '', row=0,col=0;
result = `[ ${this.getCol(0).x}, ${this.getCol(1).x}, ${this.getCol(2).x}, ${this.getCol(3).x} ]\n`;
result += `[ ${this.getCol(0).y}, ${this.getCol(1).y}, ${this.getCol(2).y}, ${this.getCol(3).y} ]\n`;
result += `[ ${this.getCol(0).z}, ${this.getCol(1).z}, ${this.getCol(2).z}, ${this.getCol(3).z} ]\n`;
result += `[ ${this.getCol(0).w}, ${this.getCol(1).w}, ${this.getCol(2).w}, ${this.getCol(3).w} ]\n`;
return result;
}
toStrN(n)
{
return this.toStringN(n);
}
toStringN(nDigs)
{
let result = '';
let xVec=this.getCol(0).clone(),
yVec=this.getCol(1).clone(),
zVec=this.getCol(2).clone(),
wVec=this.getCol(3).clone();
let vs=[xVec,yVec,zVec,wVec];
for (var i=0,n=vs.length; i<n; i++)
{
vs[i].x = pad(vs[i].x, nDigs);
vs[i].y = pad(vs[i].y, nDigs);
vs[i].z = pad(vs[i].z, nDigs);
vs[i].w = pad(vs[i].w, nDigs);
}
result = `[ ${xVec.x}, ${yVec.x}, ${zVec.x}, ${wVec.x} ]\n`;
result += `[ ${xVec.y}, ${yVec.y}, ${zVec.y}, ${wVec.y} ]\n`;
result += `[ ${xVec.z}, ${yVec.z}, ${zVec.z}, ${wVec.z} ]\n`;
result += `[ ${xVec.w}, ${yVec.w}, ${zVec.w}, ${wVec.w} ]\n`;
return result;
}
asRows(nDigs=2)
{
let result = '',xVec=this.getCol(0),yVec=this.getCol(1),zVec=this.getCol(2),wVec=this.getCol(3);
result = `[${xVec.x.toFixed(nDigs)}, ${xVec.y.toFixed(nDigs)}, ${xVec.z.toFixed(nDigs)}, ${xVec.w.toFixed(nDigs)}]\n`;
result += `[${yVec.x.toFixed(nDigs)}, ${yVec.y.toFixed(nDigs)}, ${yVec.z.toFixed(nDigs)}, ${yVec.w.toFixed(nDigs)}]\n`;
result += `[${zVec.x.toFixed(nDigs)}, ${zVec.y.toFixed(nDigs)}, ${zVec.z.toFixed(nDigs)}, ${zVec.w.toFixed(nDigs)}]\n`;
result += `[${wVec.x.toFixed(nDigs)}, ${wVec.y.toFixed(nDigs)}, ${wVec.z.toFixed(nDigs)}, ${wVec.w.toFixed(nDigs)}]\n`;
return result;
}
transpose()
{
let X=this.getCol(0), Y=this.getCol(1), Z=this.getCol(2), W=this.getCol(3);
let tmp = new mat4(
new vec4(X.x,Y.x,Z.x,W.x),
new vec4(X.y,Y.y,Z.y,W.y),
new vec4(X.z,Y.z,Z.z,W.z),
new vec4(X.w,Y.w,Z.w,W.w),
);
this.setCol(0,X);
this.setCol(1,Y);
this.setCol(2,Z);
this.setCol(3,W);
return tmp; //this.copy(tmp);
}
inverse()
{
let X = this.getCol(0), Y = this.getCol(1), Z = this.getCol(2), W = this.getCol(3);
let m00=X.x, m01=X.y, m02=X.z, m03=X.w,
m10=Y.x, m11=Y.y, m12=Y.z, m13=Y.w,
m20=Z.x, m21=Z.y, m22=Z.z, m23=Z.w,
m30=W.x, m31=W.y, m32=W.z, m33=W.w;
let tmp_0=m22*m33, tmp_1=m32*m23, tmp_2=m12*m33,
tmp_3=m32*m13, tmp_4=m12*m23, tmp_5=m22*m13,
tmp_6=m02*m33, tmp_7=m32*m03, tmp_8=m02*m23,
tmp_9=m22*m03, tmp_10=m02*m13,tmp_11=m12*m03,
tmp_12=m20*m31,tmp_13=m30*m21,tmp_14=m10*m31,
tmp_15=m30*m11,tmp_16=m10*m21,tmp_17=m20*m11,
tmp_18=m00*m31,tmp_19=m30*m01,tmp_20=m00*m21,
tmp_21=m20*m01,tmp_22=m00*m11,tmp_23=m10*m01;
var t0 = (tmp_0 * m11 + tmp_3 * m21 + tmp_4 * m31) - (tmp_1 * m11 + tmp_2 * m21 + tmp_5 * m31);
var t1 = (tmp_1 * m01 + tmp_6 * m21 + tmp_9 * m31) - (tmp_0 * m01 + tmp_7 * m21 + tmp_8 * m31);
var t2 = (tmp_2 * m01 + tmp_7 * m11 + tmp_10 * m31) - (tmp_3 * m01 + tmp_6 * m11 + tmp_11 * m31);
var t3 = (tmp_5 * m01 + tmp_8 * m11 + tmp_11 * m21) - (tmp_4 * m01 + tmp_9 * m11 + tmp_10 * m21);
var d = 1.0 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3);
let Xo = new vec4(d*t0, d*t1, d*t2, d*t3);
// d * t0,
// d * t1,
// d * t2,
// d * t3,
let Yo = new vec4(
d * ((tmp_1 * m10 + tmp_2 * m20 + tmp_5 * m30) - (tmp_0 * m10 + tmp_3 * m20 + tmp_4 * m30)),
d * ((tmp_0 * m00 + tmp_7 * m20 + tmp_8 * m30) - (tmp_1 * m00 + tmp_6 * m20 + tmp_9 * m30)),
d * ((tmp_3 * m00 + tmp_6 * m10 + tmp_11 * m30) - (tmp_2 * m00 + tmp_7 * m10 + tmp_10 * m30)),
d * ((tmp_4 * m00 + tmp_9 * m10 + tmp_10 * m20) - (tmp_5 * m00 + tmp_8 * m10 + tmp_11 * m20))
);
let Zo = new vec4(
d * ((tmp_12 * m13 + tmp_15 * m23 + tmp_16 * m33) - (tmp_13 * m13 + tmp_14 * m23 + tmp_17 * m33)),
d * ((tmp_13 * m03 + tmp_18 * m23 + tmp_21 * m33) - (tmp_12 * m03 + tmp_19 * m23 + tmp_20 * m33)),
d * ((tmp_14 * m03 + tmp_19 * m13 + tmp_22 * m33) - (tmp_15 * m03 + tmp_18 * m13 + tmp_23 * m33)),
d * ((tmp_17 * m03 + tmp_20 * m13 + tmp_23 * m23) - (tmp_16 * m03 + tmp_21 * m13 + tmp_22 * m23))
);
let Wo = new vec4(
d * ((tmp_14 * m22 + tmp_17 * m32 + tmp_13 * m12) - (tmp_16 * m32 + tmp_12 * m12 + tmp_15 * m22)),
d * ((tmp_20 * m32 + tmp_12 * m02 + tmp_19 * m22) - (tmp_18 * m22 + tmp_21 * m32 + tmp_13 * m02)),
d * ((tmp_18 * m12 + tmp_23 * m32 + tmp_15 * m02) - (tmp_22 * m32 + tmp_14 * m02 + tmp_19 * m12)),
d * ((tmp_22 * m22 + tmp_16 * m02 + tmp_21 * m12) - (tmp_20 * m12 + tmp_23 * m22 + tmp_17 * m02))
);
this.columns = [Xo,Yo,Zo,Wo];
return this;
}
}
function pad(num, n)
{
let str = num.toFixed(n);
if (num >= 0)
str = " " + str;
return str;
}
canvas
{
background-color: #333;
cursor: crosshair;
}
<body>
<canvas width='300' height='300'></canvas><br>
<div>Screen Coords of mouse: <span id='rawMouse'></span></div>
<div>(2d) NDC of mouse: <span id='transMouse'></span></div>
</body>
I've put explanations as comments in the code
// Canvas viewport (4:3)
const DRAW_WIDTH = 800;
const DRAW_HEIGHT = 600;
const RECT_SIZE = 10;
const RECT_FILL = 'black';
let canvas, ctx;
function init() {
canvas = document.querySelector("canvas");
// setup canvas drawing space
// this will give an aspect-ratio to the canvas
canvas.setAttribute('width', DRAW_WIDTH);
canvas.setAttribute('height', DRAW_HEIGHT);
ctx = canvas.getContext('2d');
// attach listener
canvas.addEventListener("click", onMouseDown);
}
function onMouseDown(e) {
// get canvas position and size infos:
const bbox = canvas.getBoundingClientRect();
const {
x: canvasX,
y: canvasY,
width: canvasW,
height: canvasH
} = bbox;
// mouse click position
const {
clientX: mouseX,
clientY: mouseY
} = e;
// compute ratio between drawing size (viewport) and actual size
const widthRatio = DRAW_WIDTH / canvasW;
// compute x relative to your canvas
const relativeX = (mouseX - canvasX);
// I advise you to use int values when drawing on canvas
// thus Math.round
const finalX = Math.round(widthRatio * relativeX);
// same for Y-axis
const heightRatio = DRAW_HEIGHT / canvasH;
const relativeY = (mouseY - canvasY);
const finalY = Math.round(heightRatio * relativeY);
// draw something with that:
ctx.fillStyle = RECT_FILL;
ctx.rect(finalX - RECT_SIZE / 2, finalY - RECT_SIZE / 2, RECT_SIZE, RECT_SIZE);
ctx.fill();
ctx.closePath();
}
init();
/* set canvas width in the document */
canvas {
width: 80vw;
margin-left: 5vw;
background: coral;
display: block;
}
<canvas></canvas>

Add shiny gradient to tickets with javascript

I'm trying to give each of the falling objects a gradient to make them look like shiny gold tickets.
I have a Codepen
I forked the pen from another repo and all i've changed in the background colour and the ticket colour.
This is the part of the code that control the colour. How do I add the gradient?
var colorThemes = [
function() {
//return color(200 * random()|0, 200 * random()|0, 200 * random()|0);
return color(218,165,32);
}, function() {
var black = 200 * random()|0; return color(200, black, black);
}, function() {
var black = 200 * random()|0; return color(black, 200, black);
}, function() {
var black = 200 * random()|0; return color(black, black, 200);
}, function() {
return color(200, 100, 200 * random()|0);
}, function() {
return color(200 * random()|0, 200, 200);
}, function() {
var black = 256 * random()|0; return color(black, black, black);
}, function() {
return colorThemes[random() < .5 ? 1 : 2]();
}, function() {
return colorThemes[random() < .5 ? 3 : 5]();
}, function() {
return colorThemes[random() < .5 ? 2 : 4]();
}
];
function color(r, g, b) {
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
'use strict';
// If set to true, the user must press
// UP UP DOWN ODWN LEFT RIGHT LEFT RIGHT A B
// to trigger the confetti with a random color theme.
// Otherwise the confetti constantly falls.
var onlyOnKonami = false;
$(function() {
// Globals
var $window = $(window)
, random = Math.random
, cos = Math.cos
, sin = Math.sin
, PI = Math.PI
, PI2 = PI * 2
, timer = undefined
, frame = undefined
, confetti = [];
// Settings
var konami = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]
, pointer = 0;
var particles = 150
, spread = 10
, sizeMin = 3
, sizeMax = 40 - sizeMin
, eccentricity = 10
, deviation = 100
, dxThetaMin = -.1
, dxThetaMax = -dxThetaMin - dxThetaMin
, dyMin = .13
, dyMax = .18
, dThetaMin = .4
, dThetaMax = .7 - dThetaMin;
var colorThemes = [
function() {
//return color(200 * random()|0, 200 * random()|0, 200 * random()|0);
return color(218,165,32);
}, function() {
var black = 200 * random()|0; return color(200, black, black);
}, function() {
var black = 200 * random()|0; return color(black, 200, black);
}, function() {
var black = 200 * random()|0; return color(black, black, 200);
}, function() {
return color(200, 100, 200 * random()|0);
}, function() {
return color(200 * random()|0, 200, 200);
}, function() {
var black = 256 * random()|0; return color(black, black, black);
}, function() {
return colorThemes[random() < .5 ? 1 : 2]();
}, function() {
return colorThemes[random() < .5 ? 3 : 5]();
}, function() {
return colorThemes[random() < .5 ? 2 : 4]();
}
];
function color(r, g, b) {
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
// Cosine interpolation
function interpolation(a, b, t) {
return (1-cos(PI*t))/2 * (b-a) + a;
}
// Create a 1D Maximal Poisson Disc over [0, 1]
var radius = 1/eccentricity, radius2 = radius+radius;
function createPoisson() {
// domain is the set of points which are still available to pick from
// D = union{ [d_i, d_i+1] | i is even }
var domain = [radius, 1-radius], measure = 1-radius2, spline = [0, 1];
while (measure) {
var dart = measure * random(), i, l, interval, a, b, c, d;
// Find where dart lies
for (i = 0, l = domain.length, measure = 0; i < l; i += 2) {
a = domain[i], b = domain[i+1], interval = b-a;
if (dart < measure+interval) {
spline.push(dart += a-measure);
break;
}
measure += interval;
}
c = dart-radius, d = dart+radius;
// Update the domain
for (i = domain.length-1; i > 0; i -= 2) {
l = i-1, a = domain[l], b = domain[i];
// c---d c---d Do nothing
// c-----d c-----d Move interior
// c--------------d Delete interval
// c--d Split interval
// a------b
if (a >= c && a < d)
if (b > d) domain[l] = d; // Move interior (Left case)
else domain.splice(l, 2); // Delete interval
else if (a < c && b > c)
if (b <= d) domain[i] = c; // Move interior (Right case)
else domain.splice(i, 0, c, d); // Split interval
}
// Re-measure the domain
for (i = 0, l = domain.length, measure = 0; i < l; i += 2)
measure += domain[i+1]-domain[i];
}
return spline.sort();
}
// Create the overarching container
var container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100%';
container.style.height = '0';
container.style.overflow = 'visible';
container.style.zIndex = '9999';
// Confetto constructor
function Confetto(theme) {
this.frame = 0;
this.outer = document.createElement('div');
this.inner = document.createElement('div');
this.outer.appendChild(this.inner);
var outerStyle = this.outer.style, innerStyle = this.inner.style;
outerStyle.position = 'absolute';
outerStyle.width = (sizeMin + sizeMax * random()) + 'px';
outerStyle.height = (sizeMin + sizeMax * random()) + 'px';
innerStyle.width = '100%';
innerStyle.height = '100%';
innerStyle.backgroundColor = theme();
outerStyle.perspective = '100px';
outerStyle.transform = 'rotate(' + (360 * random()) + 'deg)';
this.axis = 'rotate3D(' +
cos(360 * random()) + ',' +
cos(360 * random()) + ',0,';
this.theta = 360 * random();
this.dTheta = dThetaMin + dThetaMax * random();
innerStyle.transform = this.axis + this.theta + 'deg)';
this.x = $window.width() * random();
this.y = -deviation;
this.dx = sin(dxThetaMin + dxThetaMax * random());
this.dy = dyMin + dyMax * random();
outerStyle.left = this.x + 'px';
outerStyle.top = this.y + 'px';
// Create the periodic spline
this.splineX = createPoisson();
this.splineY = [];
for (var i = 1, l = this.splineX.length-1; i < l; ++i)
this.splineY[i] = deviation * random();
this.splineY[0] = this.splineY[l] = deviation * random();
this.update = function(height, delta) {
this.frame += delta;
this.x += this.dx * delta;
this.y += this.dy * delta;
this.theta += this.dTheta * delta;
// Compute spline and convert to polar
var phi = this.frame % 7777 / 7777, i = 0, j = 1;
while (phi >= this.splineX[j]) i = j++;
var rho = interpolation(
this.splineY[i],
this.splineY[j],
(phi-this.splineX[i]) / (this.splineX[j]-this.splineX[i])
);
phi *= PI2;
outerStyle.left = this.x + rho * cos(phi) + 'px';
outerStyle.top = this.y + rho * sin(phi) + 'px';
innerStyle.transform = this.axis + this.theta + 'deg)';
return this.y > height+deviation;
};
}
function poof() {
if (!frame) {
// Append the container
document.body.appendChild(container);
// Add confetti
var theme = colorThemes[onlyOnKonami ? colorThemes.length * random()|0 : 0]
, count = 0;
(function addConfetto() {
if (onlyOnKonami && ++count > particles)
return timer = undefined;
var confetto = new Confetto(theme);
confetti.push(confetto);
container.appendChild(confetto.outer);
timer = setTimeout(addConfetto, spread * random());
})(0);
// Start the loop
var prev = undefined;
requestAnimationFrame(function loop(timestamp) {
var delta = prev ? timestamp - prev : 0;
prev = timestamp;
var height = $window.height();
for (var i = confetti.length-1; i >= 0; --i) {
if (confetti[i].update(height, delta)) {
container.removeChild(confetti[i].outer);
confetti.splice(i, 1);
}
}
if (timer || confetti.length)
return frame = requestAnimationFrame(loop);
// Cleanup
document.body.removeChild(container);
frame = undefined;
});
}
}
$window.keydown(function(event) {
pointer = konami[pointer] === event.which
? pointer+1
: +(event.which === konami[0]);
if (pointer === konami.length) {
pointer = 0;
poof();
}
});
if (!onlyOnKonami) poof();
});
html {
height: 100%;
}
body {
background: #d09d42;
background: linear-gradient(to bottom, #efc466, #d09d42);
height: 100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
You need to modify method Confetto() to change the color of the divI've added the following javascript after innerStyle.backgroundColor = theme():
function Confetto(){
//Code
innerStyle.backgroundColor = theme()
innerStyle.background = "linear-gradient(to right, " + theme() + " , yellow)";
innerStyle.border = "thick solid #FFFB00";
innerStyle.borderWidth = "thin";
//Rest of the code
}
Check out this :
// JavaScript source code
'use strict';
// If set to true, the user must press
// UP UP DOWN ODWN LEFT RIGHT LEFT RIGHT A B
// to trigger the confetti with a random color theme.
// Otherwise the confetti constantly falls.
var onlyOnKonami = false;
$(function () {
// Globals
var $window = $(window),
random = Math.random,
cos = Math.cos,
sin = Math.sin,
PI = Math.PI,
PI2 = PI * 2,
timer = undefined,
frame = undefined,
confetti = [];
// Settings
var konami = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65],
//var konami = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
pointer = 0;
var particles = 10,
spread = 10,
sizeMin = 3,
sizeMax = 40 - sizeMin,
eccentricity = 10,
deviation = 100,
dxThetaMin = -.1,
dxThetaMax = -dxThetaMin - dxThetaMin,
dyMin = .13,
dyMax = .18,
dThetaMin = .4,
dThetaMax = .7 - dThetaMin;
var colorThemes = [
function () {
//return color(200 * random()|0, 200 * random()|0, 200 * random()|0);
return color(218, 165, 32);
},
function () {
var black = 200 * random() | 0;
return color(200, black, black);
},
function () {
var black = 200 * random() | 0;
return color(black, 200, black);
},
function () {
var black = 200 * random() | 0;
return color(black, black, 200);
},
function () {
return color(200, 100, 200 * random() | 0);
},
function () {
return color(200 * random() | 0, 200, 200);
},
function () {
var black = 256 * random() | 0;
return color(black, black, black);
},
function () {
return colorThemes[random() < .5 ? 1 : 2]();
},
function () {
return colorThemes[random() < .5 ? 3 : 5]();
},
function () {
return colorThemes[random() < .5 ? 2 : 4]();
}
];
function color(r, g, b) {
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
// Cosine interpolation
function interpolation(a, b, t) {
return (1 - cos(PI * t)) / 2 * (b - a) + a;
}
// Create a 1D Maximal Poisson Disc over [0, 1]
var radius = 1 / eccentricity,
radius2 = radius + radius;
function createPoisson() {
// domain is the set of points which are still available to pick from
// D = union{ [d_i, d_i+1] | i is even }
var domain = [radius, 1 - radius],
measure = 1 - radius2,
spline = [0, 1];
while (measure) {
var dart = measure * random(),
i, l, interval, a, b, c, d;
// Find where dart lies
for (i = 0, l = domain.length, measure = 0; i < l; i += 2) {
a = domain[i], b = domain[i + 1], interval = b - a;
if (dart < measure + interval) {
spline.push(dart += a - measure);
break;
}
measure += interval;
}
c = dart - radius, d = dart + radius;
// Update the domain
for (i = domain.length - 1; i > 0; i -= 2) {
l = i - 1, a = domain[l], b = domain[i];
// c---d c---d Do nothing
// c-----d c-----d Move interior
// c--------------d Delete interval
// c--d Split interval
// a------b
if (a >= c && a < d)
if (b > d) domain[l] = d; // Move interior (Left case)
else domain.splice(l, 2); // Delete interval
else if (a < c && b > c)
if (b <= d) domain[i] = c; // Move interior (Right case)
else domain.splice(i, 0, c, d); // Split interval
}
// Re-measure the domain
for (i = 0, l = domain.length, measure = 0; i < l; i += 2)
measure += domain[i + 1] - domain[i];
}
return spline.sort();
}
// Create the overarching container
var container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100%';
container.style.height = '0';
container.style.overflow = 'visible';
container.style.zIndex = '9999';
// Confetto constructor
function Confetto(theme) {
this.frame = 0;
this.outer = document.createElement('div');
this.inner = document.createElement('div');
this.outer.appendChild(this.inner);
var outerStyle = this.outer.style,
innerStyle = this.inner.style;
outerStyle.position = 'absolute';
outerStyle.width = (sizeMin + sizeMax * random()) + 'px';
outerStyle.height = (sizeMin + sizeMax * random()) + 'px';
innerStyle.width = '100%';
innerStyle.height = '100%';
var f = theme();
innerStyle.backgroundColor = theme();
innerStyle.background = "linear-gradient(to right, " + theme() + " , yellow)";
innerStyle.border = "thick solid #FFFB00";
innerStyle.borderWidth = "thin";
outerStyle.perspective = '100px';
outerStyle.transform = 'rotate(' + (360 * random()) + 'deg)';
this.axis = 'rotate3D(' +
cos(360 * random()) + ',' +
cos(360 * random()) + ',0,';
this.theta = 360 * random();
this.dTheta = dThetaMin + dThetaMax * random();
innerStyle.transform = this.axis + this.theta + 'deg)';
this.x = $window.width() * random();
this.y = -deviation;
this.dx = sin(dxThetaMin + dxThetaMax * random());
this.dy = dyMin + dyMax * random();
outerStyle.left = this.x + 'px';
outerStyle.top = this.y + 'px';
// Create the periodic spline
this.splineX = createPoisson();
this.splineY = [];
for (var i = 1, l = this.splineX.length - 1; i < l; ++i)
this.splineY[i] = deviation * random();
this.splineY[0] = this.splineY[l] = deviation * random();
this.update = function (height, delta) {
this.frame += delta;
this.x += this.dx * delta;
this.y += this.dy * delta;
this.theta += this.dTheta * delta;
// Compute spline and convert to polar
var phi = this.frame % 7777 / 7777,
i = 0,
j = 1;
while (phi >= this.splineX[j]) i = j++;
var rho = interpolation(
this.splineY[i],
this.splineY[j],
(phi - this.splineX[i]) / (this.splineX[j] - this.splineX[i])
);
phi *= PI2;
outerStyle.left = this.x + rho * cos(phi) + 'px';
outerStyle.top = this.y + rho * sin(phi) + 'px';
innerStyle.transform = this.axis + this.theta + 'deg)';
return this.y > height + deviation;
};
}
function poof() {
if (!frame) {
// Append the container
document.body.appendChild(container);
// Add confetti
var theme = colorThemes[onlyOnKonami ? colorThemes.length * random() | 0 : 0],
count = 0;
(function addConfetto() {
if (onlyOnKonami && ++count > particles)
return timer = undefined;
var confetto = new Confetto(theme);
confetti.push(confetto);
container.appendChild(confetto.outer);
timer = setTimeout(addConfetto, spread * random());
})(0);
// Start the loop
var prev = undefined;
requestAnimationFrame(function loop(timestamp) {
var delta = prev ? timestamp - prev : 0;
prev = timestamp;
var height = $window.height();
for (var i = confetti.length - 1; i >= 0; --i) {
if (confetti[i].update(height, delta)) {
container.removeChild(confetti[i].outer);
confetti.splice(i, 1);
}
}
if (timer || confetti.length)
return frame = requestAnimationFrame(loop);
// Cleanup
document.body.removeChild(container);
frame = undefined;
});
}
}
$window.keydown(function (event) {
pointer = konami[pointer] === event.which ?
pointer + 1 :
+(event.which === konami[0]);
if (pointer === konami.length) {
pointer = 0;
poof();
}
});
if (!onlyOnKonami) poof();
});
html {
height: 100%;
}
body {
background: #d09d42;
background: linear-gradient(to bottom, #efc466, #d09d42);
height: 100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

How do I draw an arrow between two points in d3v4?

I have created a custom path renderer that draws an arrow between the nodes in my d3 graph as shown in the snippet. I have one last issue I am getting stuck on,
How would I rotate the arrow portion so that it is pointing from the direction of the curve instead of the direction of the source?
var w2 = 6,
ar2 = w2 * 2,
ah = w2 * 3,
baseHeight = 30;
// Arrow function
function CurvedArrow(context, index) {
this._context = context;
this._index = index;
}
CurvedArrow.prototype = {
areaStart: function() {
this._line = 0;
},
areaEnd: function() {
this._line = NaN;
},
lineStart: function() {
this._point = 0;
},
lineEnd: function() {
if (this._line || (this._line !== 0 && this._point === 1)) {
this._context.closePath();
}
this._line = 1 - this._line;
},
point: function(x, y) {
x = +x, y = +y; // jshint ignore:line
switch (this._point) {
case 0:
this._point = 1;
this._p1x = x;
this._p1y = y;
break;
case 1:
this._point = 2; // jshint ignore:line
default:
var p1x = this._p1x,
p1y = this._p1y,
p2x = x,
p2y = y,
dx = p2x - p1x,
dy = p2y - p1y,
px = dy,
py = -dx,
pr = Math.sqrt(px * px + py * py),
nx = px / pr,
ny = py / pr,
dr = Math.sqrt(dx * dx + dy * dy),
wx = dx / dr,
wy = dy / dr,
ahx = wx * ah,
ahy = wy * ah,
awx = nx * ar2,
awy = ny * ar2,
phx = nx * w2,
phy = ny * w2,
//Curve figures
alpha = Math.floor((this._index - 1) / 2),
direction = p1y < p2y ? -1 : 1,
height = (baseHeight + alpha * 3 * ar2) * direction,
// r5
//r7 r6|\
// ------------ \
// ____________ /r4
//r1 r2|/
// r3
r1x = p1x - phx,
r1y = p1y - phy,
r2x = p2x - phx - ahx,
r2y = p2y - phy - ahy,
r3x = p2x - awx - ahx,
r3y = p2y - awy - ahy,
r4x = p2x,
r4y = p2y,
r5x = p2x + awx - ahx,
r5y = p2y + awy - ahy,
r6x = p2x + phx - ahx,
r6y = p2y + phy - ahy,
r7x = p1x + phx,
r7y = p1y + phy,
//Curve 1
c1mx = (r2x + r1x) / 2,
c1my = (r2y + r1y) / 2,
m1b = (c1mx - r1x) / (r1y - c1my),
den1 = Math.sqrt(1 + Math.pow(m1b, 2)),
mp1x = c1mx + height * (1 / den1),
mp1y = c1my + height * (m1b / den1),
//Curve 2
c2mx = (r7x + r6x) / 2,
c2my = (r7y + r6y) / 2,
m2b = (c2mx - r6x) / (r6y - c2my),
den2 = Math.sqrt(1 + Math.pow(m2b, 2)),
mp2x = c2mx + height * (1 / den2),
mp2y = c2my + height * (m2b / den2);
this._context.moveTo(r1x, r1y);
this._context.quadraticCurveTo(mp1x, mp1y, r2x, r2y);
this._context.lineTo(r3x, r3y);
this._context.lineTo(r4x, r4y);
this._context.lineTo(r5x, r5y);
this._context.lineTo(r6x, r6y);
this._context.quadraticCurveTo(mp2x, mp2y, r7x, r7y);
break;
}
}
};
var w = 600,
h = 220;
var t0 = Date.now();
var points = [{
R: 100,
r: 3,
speed: 2,
phi0: 190
}];
var path = d3.line()
.curve(function(ctx) {
return new CurvedArrow(ctx, 1);
});
var svg = d3.select("svg");
var container = svg.append("g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
container.selectAll("g.planet").data(points).enter().append("g")
.attr("class", "planet").each(function(d, i) {
d3.select(this).append("circle").attr("r", d.r).attr("cx", d.R)
.attr("cy", 0).attr("class", "planet");
});
container.append("path");
var planet = d3.select('.planet circle');
d3.timer(function() {
var delta = (Date.now() - t0);
planet.attr("transform", function(d) {
return "rotate(" + d.phi0 + delta * d.speed / 50 + ")";
});
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttributeNS(null, "transform", planet.attr('transform'));
var matrix = g.transform.baseVal.consolidate().matrix;
svg.selectAll("path").attr('d', function(d) {
return path([
[0, 0],
[matrix.a * 100, matrix.b * 100]
])
});
});
path {
stroke: #11a;
fill: #eee;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="220"></svg>
I ended up doing what #Mark suggested in the comments, I calculate the point that is the height of the curve away along the normal midway between the two points, then calculate the unit vectors from the start point to the mid point and again from the midpoint to the end. I can then use those to get all the required points.
var arrowRadius = 6,
arrowPointRadius = arrowRadius * 2,
arrowPointHeight = arrowRadius * 3,
baseHeight = 30;
// Arrow function
function CurvedArrow(context, index) {
this._context = context;
this._index = index;
}
CurvedArrow.prototype = {
areaStart: function() {
this._line = 0;
},
areaEnd: function() {
this._line = NaN;
},
lineStart: function() {
this._point = 0;
},
lineEnd: function() {
if (this._line || (this._line !== 0 && this._point === 1)) {
this._context.closePath();
}
this._line = 1 - this._line;
},
point: function(x, y) {
x = +x, y = +y; // jshint ignore:line
switch (this._point) {
case 0:
this._point = 1;
this._p1x = x;
this._p1y = y;
break;
case 1:
this._point = 2; // jshint ignore:line
default:
var p1x = this._p1x,
p1y = this._p1y,
p2x = x,
p2y = y,
//Curve figures
// mp1
// |
// | height
// |
// p1 ----------------------- p2
//
alpha = Math.floor((this._index - 1) / 2),
direction = p1y < p2y ? -1 : 1,
height = (baseHeight + alpha * 3 * arrowPointRadius) * direction,
c1mx = (p2x + p1x) / 2,
c1my = (p2y + p1y) / 2,
m1b = (c1mx - p1x) / (p1y - c1my),
den1 = Math.sqrt(1 + Math.pow(m1b, 2)),
// Perpendicular point from the midpoint.
mp1x = c1mx + height * (1 / den1),
mp1y = c1my + height * (m1b / den1),
// Arrow figures
dx = p2x - mp1x,
dy = p2y - mp1y,
dr = Math.sqrt(dx * dx + dy * dy),
// Normal unit vectors
nx = dy / dr,
wy = nx,
wx = dx / dr,
ny = -wx,
ahx = wx * arrowPointHeight,
ahy = wy * arrowPointHeight,
awx = nx * arrowPointRadius,
awy = ny * arrowPointRadius,
phx = nx * arrowRadius,
phy = ny * arrowRadius,
// Start arrow offset.
sdx = mp1x - p1x,
sdy = mp1y - p1y,
spr = Math.sqrt(sdy * sdy + sdx * sdx),
snx = sdy / spr,
sny = -sdx / spr,
sphx = snx * arrowRadius,
sphy = sny * arrowRadius,
// r5
//r7 r6|\
// ------------ \
// ____________ /r4
//r1 r2|/
// r3
r1x = p1x - sphx,
r1y = p1y - sphy,
r2x = p2x - phx - ahx,
r2y = p2y - phy - ahy,
r3x = p2x - awx - ahx,
r3y = p2y - awy - ahy,
r4x = p2x,
r4y = p2y,
r5x = p2x + awx - ahx,
r5y = p2y + awy - ahy,
r6x = p2x + phx - ahx,
r6y = p2y + phy - ahy,
r7x = p1x + sphx,
r7y = p1y + sphy,
mpc1x = mp1x - phx,
mpc1y = mp1y - phy,
mpc2x = mp1x + phx,
mpc2y = mp1y + phy;
this._context.moveTo(r1x, r1y);
this._context.quadraticCurveTo(mpc1x, mpc1y, r2x, r2y);
this._context.lineTo(r3x, r3y);
this._context.lineTo(r4x, r4y);
this._context.lineTo(r5x, r5y);
this._context.lineTo(r6x, r6y);
this._context.quadraticCurveTo(mpc2x, mpc2y, r7x, r7y);
this._context.closePath();
break;
}
}
};
var w = 600,
h = 220;
var t0 = Date.now();
var points = [{
R: 100,
r: 3,
speed: 2,
phi0: 190
}];
var path = d3.line()
.curve(function(ctx) {
return new CurvedArrow(ctx, 1);
});
var svg = d3.select("svg");
var container = svg.append("g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
container.selectAll("g.planet").data(points).enter().append("g")
.attr("class", "planet").each(function(d, i) {
d3.select(this).append("circle").attr("r", d.r).attr("cx", d.R)
.attr("cy", 0).attr("class", "planet");
});
container.append("path");
var planet = d3.select('.planet circle');
d3.timer(function() {
var delta = (Date.now() - t0);
planet.attr("transform", function(d) {
return "rotate(" + d.phi0 + delta * d.speed / 50 + ")";
});
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttributeNS(null, "transform", planet.attr('transform'));
var matrix = g.transform.baseVal.consolidate().matrix;
svg.selectAll("path").attr('d', function(d) {
return path([
[0, 0],
[matrix.a * 100, matrix.b * 100]
])
});
});
path {
stroke: #11a;
fill: #eee;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="220"></svg>

Categories