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,
Related
I'm working on another tunnel effect demo. This time I'm trying to make the tunnel move within the image.
However, the function that handles rendering the tunnel always throws an error, and I'm not entirely sure why:
function draw(time) {
let animation = time / 1000.0;
let shiftX = ~~(texWidth * animation);
let shiftY = ~~(texHeight * 0.25 * animation);
let shiftLookX = (screenWidth / 2) + ~~(screenWidth / 2 * Math.sin(animation))
let shiftLookY = (screenHeight / 2) + ~~(screenHeight / 2 * Math.sin(animation))
for (y = 0; y < buffer.height; y++) {
for (x = 0; x < buffer.width; x++) {
let id = (y * buffer.width + x) * 4;
let d = ~~(distanceTable[y + shiftLookY][x + shiftLookX] + shiftX) % texWidth;
let a = ~~(angleTable[y + shiftLookY][x + shiftLookX] + shiftY) % texHeight;
let tex = (a * texture.width + d) * 4;
buffer.data[id] = texture.data[tex];
buffer.data[id+1] = texture.data[tex+1];
buffer.data[id+2] = texture.data[tex+2];
buffer.data[id+3] = texture.data[tex+3];
}
}
ctx.putImageData(buffer, 0, 0);
window.requestAnimationFrame(draw);
}
The rest of the code is viewable here, just in case the problem happens to be somewhere else.
I have identified a possible cause -- if the first index used to read from distanceTable or
angleTable is anything other than y, the error appears, even if it's simply a value being added to y. Unfortunately, I haven't figured out what causes it, or why the second index isn't affected by this.
I've also searched for similar questions, but it seems like the people asking them all got this error for different reasons, so I'm kind of stuck.
It appears that setting the for loops to use the canvas' height and width as the upper limit instead of the pixel buffer's width and height was enough to fix it.
I have absolutely no idea why, though. Was it because the buffer was twice the size of the canvas?
var texWidth = 256;
var texHeight = 256;
var screenWidth = 640;
var screenHeight = 480;
var canvas = document.createElement('canvas');
canvas.width = screenWidth;
canvas.height = screenHeight;
var ctx = canvas.getContext("2d");
var texture = new ImageData(texWidth, texHeight);
var distanceTable = [];
var angleTable = [];
var buffer = new ImageData(canvas.width * 2, canvas.height * 2);
for (let y = 0; y < texture.height; y++) {
for (let x = 0; x < texture.width; x++) {
let id = (y * texture.width + x) * 4;
let c = x ^ y;
texture.data[id] = c;
texture.data[id+1] = c;
texture.data[id+2] = c;
texture.data[id+3] = 255;
}
}
for (let y = 0; y < buffer.height; y++) {
distanceTable[y] = [];
angleTable[y] = [];
let sqy = Math.pow(y - canvas.height, 2);
for (let x = 0; x < buffer.width; x++) {
let sqx = Math.pow(x - canvas.width, 2);
let ratio = 32.0;
let distance = ~~(ratio * texHeight / Math.sqrt(sqx + sqy)) % texHeight;
let angle = Math.abs(~~(0.5 * texWidth * Math.atan2(y - canvas.height, x - canvas.width)) / Math.PI);
distanceTable[y][x] = distance;
angleTable[y][x] = angle;
}
}
function draw(time) {
let animation = time / 1000.0;
let shiftX = ~~(texWidth * animation);
let shiftY = ~~(texHeight * 0.25 * animation);
let shiftLookX = (screenWidth / 2) + ~~(screenWidth / 2 * Math.sin(animation))
let shiftLookY = (screenHeight / 2) + ~~(screenHeight / 2 * Math.sin(animation * 2.0))
for (y = 0; y < canvas.height; y++) {
for (x = 0; x < canvas.width; x++) {
let id = (y * buffer.width + x) * 4;
let d = ~~(distanceTable[y + shiftLookY][x + shiftLookX] + shiftX) % texWidth;
let a = ~~(angleTable[y + shiftLookY][x + shiftLookX] + shiftY) % texHeight;
let tex = (a * texture.width + d) * 4;
buffer.data[id] = texture.data[tex];
buffer.data[id+1] = texture.data[tex+1];
buffer.data[id+2] = texture.data[tex+2];
buffer.data[id+3] = texture.data[tex+3];
}
}
ctx.putImageData(buffer, 0, 0);
window.requestAnimationFrame(draw);
}
document.body.appendChild(canvas);
window.requestAnimationFrame(draw);
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.
I am working on this script where I have x-number bouncing balls (in this case 20 balls) in a canvas.
My question is, how do I make them bounce off each other when they hit, as well as bounce off the yellow rectangle when they hit it?
var mycanvas =document.getElementById("mycanvas");
var ctx=mycanvas.getContext("2d");
var w=500,h=500;
mycanvas.height=h;
mycanvas.width=w;
var ball=[];
function Ball(x,y,r,c,vx,vy){
this.x=x; //starting x coordinate
this.y=y; //starting x coordinate
this.r=r; //radius
this.c=c; //color
this.vx=vx; // x direction speed
this.vy=vy; // y direction speed
this.update=function(){
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI*2, false);
ctx.fillStyle = this.c;
ctx.fill();
ctx.closePath();
this.x += this.vx;
this.y += this.vy;
//changing direction on hitting wall
if(this.y>=(w-10)||this.y<=10){
this.vy=-this.vy;
}
if(this.x>=(h-10)||this.x<=10){
this.vx=-this.vx;
}
}
}
function clearCanvas(){
ctx.clearRect(0, 0, w, h);
}
var count;
for (count = 0; count < 20; count++) {
var rndColor=Math.floor((Math.random() * 9) + 1); //random color
ball[count]= new Ball(Math.floor((Math.random() * 490) + 1),Math.floor((Math.random() * 490)+1),5,'red',5,5);
}
function update(){
var i;
clearCanvas();
//draw rectangle
ctx.rect(250, 200, 10, 100);
ctx.fillStyle = 'yellow';
ctx.fill();
for(i=0;i<count;i++){
ball[i].update();
}
}
setInterval(update, 1000/60);
There are several methods you can use. The following methods are about the simplest.
Update
I have added an example that uses the second method. See snippet at the bottom.
Defining the balls
Each example is as an object called Ball.
// x,y position of center,
// vx,vy is velocity,
// r is radius defaults 45,
// m is mass defaults to the volume of the sphere of radius r
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;
}
Ball.prototype = {
// add collision functions here
};
The code assumes the balls are touching.
Elastic collisions
The logic used can be found at wikis elastic collision page
The calculation splits the forces into two parts for each ball. (4 in total for 2 balls)
The transfer of energy along the line between the balls,
The adjustment of energy per ball along the tangent of the collision point
Equal mass
Each ball has the same mass which means that the transfer of energy is balanced and can be ignored
After the function is called each ball has a new velocity vector.
Note that if you call collision and the velocities mean that the balls are moving away from each other (collision paradox) then the result will have the balls moving into each other (resolution paradox)
To keep the math simple the vector magnitudes u1, u2, u3, and u4 are converted into a unit that is the length of the line between the ball centers (square root of d)
collide(b) { // b is the ball that the collision is with
const a = this;
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; // From this to b
const u2 = (x * a.vy - y * a.vx) / d; // Adjust self along tangent
const u3 = (b.vx * x + b.vy * y) / d; // From b to this
const u4 = (x * b.vy - y * b.vx) / d; // Adjust b along tangent
// set new velocities
b.vx = x * u1 - y * u4;
b.vy = y * u1 + x * u4;
a.vx = x * u3 - y * u2;
a.vy = y * u3 + x * u2;
},
Different masses
Each ball has its own mass and thus the transfer needs to calculate the amount of energy related to the mass that is transferred.
Only the energy transferred along the line between the balls is effect by the mass differences
collideMass(b) {
const a = this;
const m1 = a.m;
const m2 = b.m;
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;
},
Example
Simple ball collision example. Balls bound by lines (Note lines have an outside and inside, if looking from the start to the end the inside is on the right)
Collisions are fully resolved in chronological order between frames. The time used is a frame where 0 is the previous frame and 1 is the current frame.
canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 20; // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 13;
const MAX_BALL_SIZE = 20;
const VEL_MIN = 1;
const VEL_MAX = 5;
const MAX_RESOLUTION_CYCLES = 100;
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;
}
}
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;
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += GRAVITY;
},
draw() {
ctx.moveTo(this.x + this.r, this.y);
ctx.arc(this.x, this.y, this.r, 0, Math.TAU);
},
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) {
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) {
const a = this;
const m1 = a.m;
const m2 = b.m;
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;
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;
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, 10, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 10)).reverse());
lines.push((new Line(10, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 10, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
debugger
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 + 10, canvas.width - MAX_BALL_SIZE - 10),
Math.rand(MAX_BALL_SIZE + 10, canvas.height - MAX_BALL_SIZE - 10),
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() }
ctx.strokeStyle = "#000";
ctx.beginPath();
for(const b of balls) { b.draw() }
for(const l of lines) { l.draw() }
ctx.stroke();
requestAnimationFrame(mainLoop);
}
<canvas id="canvas"></canvas>
To bounce balls off of one another, he's what you need to know
Have the balls collided? The way to determine is to measure the distance between the centers of the two circles. If this is less than the combined radiuses, the balls have collided
What direction should they have after colliding? Use use atan2 to calculate the angle between the centers of the two balls. Then set them in opposite directions on that angle, in a way that they don't end up deeper within each other. Of course, this simple solution ignores existing momentum that the balls may have. But doing the momentum calculation (which involves mass, speed, and current angle) is more complicated.
Basically, I am trying to make an interactive background where wherever the mouse hovers or goes to, the lines in this background flows or follows the mouse in that certain direction. Is there anyway I could do this by using HTML/CSS/JavaScript? Here is the code. CSS is on the top, JS on the bottom.
//CSS
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
//JS
// Init Context
let c = document.createElement('canvas').getContext('2d')
let postctx = document.body.appendChild(document.createElement('canvas')).getContext('2d')
let canvas = c.canvas
let vertices = []
// Effect Properties
let vertexCount = 7000
let vertexSize = 2
let oceanWidth = 204
let oceanHeight = -80
let gridSize = 32;
let waveSize = 16;
let perspective = 500;
// Common variables
let depth = (vertexCount / oceanWidth * gridSize)
let frame = 0
let { sin, cos, tan, PI } = Math
// Render loop
let loop = () => {
let rad = sin(frame / 100) * PI / 20
let rad2 = sin(frame / 50) * PI / 10
frame++
if (postctx.canvas.width !== postctx.canvas.offsetWidth || postctx.canvas.height !== postctx.canvas.offsetHeight) {
postctx.canvas.width = canvas.width = postctx.canvas.offsetWidth
postctx.canvas.height = canvas.height = postctx.canvas.offsetHeight
}
c.fillStyle = `hsl(200deg, 100%, 2%)`
c.fillRect(0, 0, canvas.width, canvas.height)
c.save()
c.translate(canvas.width / 2, canvas.height / 2)
c.beginPath()
vertices.forEach((vertex, i) => {
let ni = i + oceanWidth
let x = vertex[0] - frame % (gridSize * 2)
let z = vertex[2] - frame * 2 % gridSize + (i % 2 === 0 ? gridSize / 2 : 0)
let wave = (cos(frame / 45 + x / 50) - sin(frame / 20 + z / 50) + sin(frame / 30 + z*x / 10000))
let y = vertex[1] + wave * waveSize
let a = Math.max(0, 1 - (Math.sqrt(x ** 2 + z ** 2)) / depth)
let tx, ty, tz
y -= oceanHeight
// Transformation variables
tx = x
ty = y
tz = z
// Rotation Y
tx = x * cos(rad) + z * sin(rad)
tz = -x * sin(rad) + z * cos(rad)
x = tx
y = ty
z = tz
// Rotation Z
tx = x * cos(rad) - y * sin(rad)
ty = x * sin(rad) + y * cos(rad)
x = tx;
y = ty;
z = tz;
// Rotation X
ty = y * cos(rad2) - z * sin(rad2)
tz = y * sin(rad2) + z * cos(rad2)
x = tx;
y = ty;
z = tz;
x /= z / perspective
y /= z / perspective
if (a < 0.01) return
if (z < 0) return
c.globalAlpha = a
c.fillStyle = `hsl(${180 + wave * 20}deg, 100%, 50%)`
c.fillRect(x - a * vertexSize / 2, y - a * vertexSize / 2, a * vertexSize, a * vertexSize)
c.globalAlpha = 1
})
c.restore()
// Post-processing
postctx.drawImage(canvas, 0, 0)
postctx.globalCompositeOperation = "screen"
postctx.filter = 'blur(16px)'
postctx.drawImage(canvas, 0, 0)
postctx.filter = 'blur(0)'
postctx.globalCompositeOperation = "source-over"
requestAnimationFrame(loop)
}
// Generating dots
for (let i = 0; i < vertexCount; i++) {
let x = i % oceanWidth
let y = 0
let z = i / oceanWidth >> 0
let offset = oceanWidth / 2
vertices.push([(-offset + x) * gridSize, y * gridSize, z * gridSize])
}
loop()
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>