Inverse kinematics with an arbitrary number of points - javascript

I've modified this example of inverse kinematics in JavaScript with HTML5 Canvas and made it dynamic by seperating it into a function, and it works, but the example only uses 3 points -- start, middle, and end, and I'd like to change the number of points at will. Here's my current fiddle...
function _kinematics(joints, fx, mouse) {
joints.forEach(function (joint) {
joint.target = joint.target || {
x: fx.canvas.width / 2,
y: fx.canvas.height / 2
};
joint.start = joint.start || {
x: 0,
y: 0
};
joint.middle = joint.middle || {
x: 0,
y: 0
};
joint.end = joint.end || {
x: 0,
y: 0
};
joint.length = joint.length || 50;
});
var theta,
$theta,
_theta,
dx,
dy,
distance;
joints.forEach(function (joint) {
if (mouse) {
joint.target.x = mouse.x;
joint.target.y = mouse.y;
}
dx = joint.target.x - joint.start.x;
dy = joint.target.y - joint.start.y;
distance = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
_theta = Math.atan2(dy, dx);
if (distance < joint.length) {
theta = Math.acos(distance / (joint.length + joint.length)) + _theta;
dx = dx - joint.length * Math.cos(theta);
dy = dy - joint.length * Math.sin(theta);
$theta = Math.atan2(dy, dx);
} else {
theta = $theta = _theta;
}
joint.middle.x = joint.start.x + Math.cos(theta) * joint.length;
joint.middle.y = joint.start.y + Math.sin(theta) * joint.length;
joint.end.x = joint.middle.x + Math.cos($theta) * joint.length;
joint.end.y = joint.middle.y + Math.sin($theta) * joint.length;
fx.beginPath();
fx.moveTo(joint.start.x, joint.start.y);
/* for (var i = 0; i < joint.points.length / 2; i++) {
fx.lineTo(joint.points[i].x, joint.points[i].y);
} */
fx.lineTo(joint.middle.x, joint.middle.y);
/* for (var j = joint.points.length / 2; j < joint.points.length; j++) {
fx.lineTo(joint.points[j].x, joint.points[j].y);
} */
fx.lineTo(joint.end.x, joint.end.y);
fx.strokeStyle = "rgba(0,0,0,0.5)";
fx.stroke();
fx.beginPath();
fx.arc(joint.start.x, joint.start.y, 10, 0, Math.PI * 2);
fx.fillStyle = "rgba(255,0,0,0.5)";
fx.fill();
fx.beginPath();
fx.arc(joint.middle.x, joint.middle.y, 10, 0, Math.PI * 2);
fx.fillStyle = "rgba(0,255,0,0.5)";
fx.fill();
fx.beginPath();
fx.arc(joint.end.x, joint.end.y, 10, 0, Math.PI * 2);
fx.fillStyle = "rgba(0,0,255,0.5)";
fx.fill();
});
}
That's just the function, I've omitted the rest for brevity. As you can see, the commented-out lines were my attempt to draw the other points.
Also, here's where I populate the joints array with the points and such. See commented lines.
populate(_joints, $joints, function() {
var coords = randCoords(map);
var o = {
start: {
x: coords.x,
y: coords.y
},
// points: [],
target: {
x: mouse.x,
y: mouse.y
}
};
/* for (var p = 0; p < 10; p++) {
o.points.push({
x: p === 0 ? o.start.x + (o.length || 50) : o.points[p - 1].x + (o.length || 50),
y: p === 0 ? o.start.y + (o.length || 50) : o.points[p - 1].y + (o.length || 50)
});
}; */
return o;
});
How would I make this function work with n points?

Related

Fix Fish-eye effect 2.5D Raycaster game

I'm working on a simple raycaster game for fun. I have two canvases, (id's canvas1 and canvas2), one for the 2D view of the world, and the other for the 2.5D raycasted view. The raycasted view looks sort of distorted, like a fish eye effect. Another problem (less important) is that the view doesn't really look like your average 3D world, even with the effect of fish eye distortion. Looking for some good FOV + height values/settings.
Here's my code:
const canvas1 = document.querySelector("#canvas1");
const canvas2 = document.querySelector("#canvas2");
const ctx1 = canvas1.getContext("2d");
const ctx2 = canvas2.getContext("2d");
let width1 = 256;
let height1 = 256;
let width2 = 720;
let height2 = 360;
const columns = 100;
let columnWidth = width2 / columns;
canvas1.width = width1;
canvas1.height = height1;
canvas2.width = width2;
canvas2.height = height2;
let deltaTime = 0;
let lastFrame = Date.now();
const moveSpeed = 70;
const rotationSpeed = 175;
const FOV = 65;
const rayCastStep = 0.5;
let rayCastWidth = FOV / columns;
let map = [
["##########"],
["#...#....#"],
["#...#....#"],
["### #....#"],
["#...#....#"],
["# ###....#"],
["#...#### #"],
["#........#"],
["#........#"],
["##########"]
];
const player = {
x: 45,
y: 165,
heading: 270
}
const keyMap = {
w: false,
a: false,
s: false,
d: false
}
const willCollide = ({ xC, yC }, { x, y, w, h }) => {
boundingBox = {
left: x - w / 2,
right: x + w / 2,
top: y - h / 2,
bottom: y + h / 2
}
return xC > boundingBox.left && xC < boundingBox.right && yC > boundingBox.top && yC < boundingBox.bottom;
}
const handlePlayerMovement = () => {
if (keyMap.w) {
let dx = Math.cos(player.heading * Math.PI / 180);
let dy = Math.sin(player.heading * Math.PI / 180);
xCheck = player.x + dx * moveSpeed * deltaTime;
yCheck = player.y + dy * moveSpeed * deltaTime;
let canMove = true;
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y][0].length; x++) {
if (map[y][0][x] == "#" && willCollide({ xC: xCheck, yC: yCheck }, { x: x * 20 + 10, y: y * 20 + 10, w: 20, h: 20})) {
canMove = false;
}
}
}
if (canMove) {
player.x = xCheck;
player.y = yCheck;
}
}
if (keyMap.s) {
let dx = Math.cos((player.heading * Math.PI / 180) + Math.PI);
let dy = Math.sin((player.heading * Math.PI / 180) + Math.PI);
xCheck = player.x + dx * moveSpeed * deltaTime;
yCheck = player.y + dy * moveSpeed * deltaTime;
let canMove = true;
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y][0].length; x++) {
if (map[y][0][x] == "#" && willCollide({ xC: xCheck, yC: yCheck }, { x: x * 20 + 10, y: y * 20 + 10, w: 20, h: 20})) {
canMove = false;
}
}
}
if (canMove) {
player.x = xCheck;
player.y = yCheck;
}
}
if (keyMap.a) {
player.heading -= rotationSpeed * deltaTime;
}
if (keyMap.d) {
player.heading += rotationSpeed * deltaTime;
}
}
const generateColumns = () => {
let distances = Array(columns).fill(0);
for (let i = 0; i < columns; i++) {
let collided = false;
let rayX = player.x;
let rayY = player.y;
let angle = (player.heading - FOV / 2) + (i / columns) * FOV;
let dx = rayCastStep * Math.cos(angle * Math.PI / 180 + Math.PI);
let dy = rayCastStep * Math.sin(angle * Math.PI / 180 + Math.PI);
while (!collided) {
rayX -= dx;
rayY -= dy;
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y][0].length; x++) {
if (map[y][0][x] == "#" && willCollide({ xC: rayX, yC: rayY }, { x: x * 20 + 10, y: y * 20 + 10, w: 20, h: 20 })) {
collided = true;
let deltaX = player.x - rayX;
let deltaY = player.y - rayY;
let dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
let h = Math.max(height2 - height2 * (dist / 200), 0);
distances[i] = {
fill: h / height2,
h: h
}
}
}
}
}
ctx1.strokeStyle = "#fff";
ctx1.beginPath();
ctx1.moveTo(player.x * (256 / map[0][0].length) / 20, player.y * (256 / map.length) / 20);
ctx1.lineTo(rayX * (256 / map[0][0].length) / 20, rayY * (256 / map.length) / 20)
ctx1.stroke();
}
return distances;
}
const update = () => {
ctx1.fillStyle = "#000";
ctx1.fillRect(0, 0, width1, height1);
ctx2.fillStyle = "#000";
ctx2.fillRect(0, 0, width2, height2 / 2);
ctx2.fillStyle = "#fff";
ctx2.fillRect(0, height2 - height2 / 2, width2, height2 / 2);
handlePlayerMovement();
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y][0].length; x++) {
ctx1.fillStyle = "#fff";
if (map[y][0][x] == "#") ctx1.fillRect(x * (256 / map[y][0].length), y * (256 / map.length), (256 / map[y][0].length), (256 / map.length));
ctx1.fillStyle = "#666";
ctx1.fillRect(0, y * (256 / map.length), 256, 1);
ctx1.fillRect(x * (256 / map[y][0].length), 0, 1, 256);
}
}
ctx1.fillStyle = "#0f0";
ctx1.beginPath();
ctx1.ellipse(player.x * (256 / map[0][0].length) / 20, player.y * (256 / map.length) / 20, 2, 2, 0, 0, 2 * Math.PI);
ctx1.fill();
let cols = generateColumns();
for (let i = 0; i < columns; i++) {
ctx2.fillStyle = `rgb(0, ${cols[i].fill * 255}, 0)`;
ctx2.fillRect(Math.round(columnWidth * i), height2 / 2 - cols[i].h / 2, columnWidth + 1, cols[i].h);
}
deltaTime = (Date.now() / 1000 - lastFrame / 1000);
lastFrame = Date.now();
requestAnimationFrame(update);
}
update();
window.onkeydown = (e) => {
if (Object.keys(keyMap).includes(e.key.toLowerCase())) keyMap[e.key.toLowerCase()] = true;
}
window.onkeyup = (e) => {
if (Object.keys(keyMap).includes(e.key.toLowerCase())) keyMap[e.key.toLowerCase()] = false;
}
Any help is appreciated. Thanks!

What are the options available for plotting graphs from a table? (Without using any plotting library )

What are the options available for plotting graphs upon selecting different columns from a Data Table. I have worked with ag-grid and I want some thing like that which will come directly out of the box without using any other graph library(like plotly or highcharts) and manually writing code.
As I said in my comment, I recommend looking into Canvas (mdn).
Here's a minimal exmaple that allows panning (mouse 1) and zooming (mouse 2) in addition to callbacks for clicks and mouse moves.
class Chart {
constructor(canvas, hoverCallback, clickCallback) {
this.width = canvas.width;
this.height = canvas.height;
this.ctx = canvas.getContext('2d');
this.ctx.font = '14px serif';
canvas.addEventListener('mousedown', e => {
this.dragged = false;
this.mouseDown = {x: e.offsetX, y: e.offsetY};
});
canvas.addEventListener('mousemove', e => {
hoverCallback?.(this.pixelToCoord(e.offsetX, e.offsetY));
if (!this.mouseDown)
return;
this.dragged = true;
if (e.buttons & 1 && !e.shiftKey)
this.panRange(e.offsetX - this.mouseDown.x, e.offsetY - this.mouseDown.y);
else if (e.buttons & 2 || e.shiftKey)
this.zoomRange(e.offsetX - this.mouseDown.x, e.offsetY - this.mouseDown.y);
this.mouseDown = {x: e.offsetX, y: e.offsetY};
});
canvas.addEventListener('mouseleave', () => hoverCallback?.());
document.addEventListener('mouseup', () => this.mouseDown = null);
canvas.addEventListener('click', e => {
if (this.dragged)
return;
clickCallback?.(this.pixelToCoord(e.offsetX, e.offsetY));
});
canvas.addEventListener('dblclick', e => {
if (this.dragged)
return;
this.resetRange(e.shiftKey);
});
this.pointSets = [];
this.resetRange();
}
set pointSets(value) {
this.pointSets_ = value;
this.draw();
}
resetRange(zeroMins = false) {
let allPoints = this.pointSets_
.flatMap(({points}) => points);
[this.minX, this.deltaX] = Chart.getRange(allPoints.map(({x}) => x), zeroMins);
[this.minY, this.deltaY] = Chart.getRange(allPoints.map(({y}) => y), zeroMins);
this.verifyRange();
this.draw();
}
panRange(x, y) {
this.minX -= x * this.deltaX / this.width;
this.minY += y * this.deltaY / this.height;
this.verifyRange();
this.draw();
}
zoomRange(x, y) {
let dx = x * this.deltaX / this.width;
let dy = -y * this.deltaY / this.height;
this.minX += dx;
this.minY += dy;
this.deltaX -= dx * 2;
this.deltaY -= dy * 2;
this.verifyRange();
this.draw();
}
verifyRange() {
this.minX = Math.max(this.minX, -this.deltaX / 10);
this.minY = Math.max(this.minY, -this.deltaY / 10);
}
draw() {
if (!this.pointSets_ || this.minX === undefined)
return;
this.ctx.fillStyle = 'white';
this.ctx.fillRect(0, 0, this.width, this.height);
this.drawPoints();
this.drawAxis();
}
drawPoints() {
this.pointSets_.forEach(({color, fill, size, points, isPath}) => {
this.ctx.strokeStyle = color;
this.ctx.fillStyle = color;
if (isPath) {
this.ctx.lineWidth = size;
this.ctx.beginPath();
points.forEach((p, i) => {
let {x, y} = this.coordToPixel(p.x, p.y);
if (!i)
this.ctx.moveTo(x, y);
else
this.ctx.lineTo(x, y);
});
if (fill)
this.ctx.fill();
else
this.ctx.stroke();
} else {
points.forEach(p => {
let {x, y} = this.coordToPixel(p.x, p.y);
this.ctx[fill ? 'fillRect' : 'strokeRect'](x - size / 2, y - size / 2, size, size);
});
}
});
}
drawAxis() {
let n = 20;
let step = this.width / n;
let size = 10;
let sizeSmall = 1;
this.ctx.lineWidth = 1;
this.ctx.strokeStyle = `rgb(0, 0, 0)`;
this.ctx.fillStyle = `rgb(0, 0, 0)`;
this.ctx.strokeRect(this.width / n, this.height * (n - 1) / n, this.width * (n - 2) / n, 0); // x axis line
this.ctx.strokeRect(this.width / n, this.height / n, 0, this.width * (n - 2) / n); // y axis line
for (let i = 2; i < n; i += 2) {
let x = i * step;
let y = (n - i) * step;
let xText = Chart.numToPrint(this.minX + i / n * this.deltaX);
let yText = Chart.numToPrint(this.minY + i / n * this.deltaY);
this.ctx.fillText(xText, x - 9, step * (n - 1) + 17); // x axis text
this.ctx.fillText(yText, step - 28, y + 4, 30); // y axis text
this.ctx.fillRect(x - sizeSmall / 2, step * (n - 1) - size / 2, sizeSmall, size); // x axis dots
this.ctx.fillRect(step - size / 2, x - sizeSmall / 2, size, sizeSmall); // y axis dots
}
}
pixelToCoord(x, y) {
return {
x: x / this.width * this.deltaX + this.minX,
y: (1 - y / this.height) * this.deltaY + this.minY,
width: 20 / this.width * this.deltaX,
height: 20 / this.height * this.deltaY
};
}
coordToPixel(x, y) {
return {
x: x === Infinity ? this.width : (x - this.minX) / this.deltaX * this.width,
y: y === Infinity ? 0 : (1 - (y - this.minY) / this.deltaY) * this.height,
};
}
static getRange(values, zeroMin = false, buffer = .1) {
let min = values.length && !zeroMin ? Math.min(...values) : 0;
let max = values.length ? Math.max(...values) : 10;
let delta = max - min + .001;
return [min - delta * buffer, delta + delta * buffer * 2]
}
static numToPrint(n) {
return Math.round(n * 10) / 10;
}
}
let canvas = document.querySelector('canvas');
let chart = new Chart(canvas);
chart.pointSets = [
// e.g. {color: 'rgb(255,0,0)', fill: true, size: 5, points: [{x: 0, y: 1}, ...], isPath: true}
{
color: 'rgb(255,0,0)',
fill: false,
size: 3,
points: [
{x: 0, y: 1},
{x: 1, y: 1},
{x: 1, y: 0},
{x: 0, y: 0},
{x: 0, y: .5},
],
isPath: true,
}, {
color: 'rgb(0,0,255)',
fill: true,
size: 10,
points: [
{x: 0, y: 1},
{x: 0, y: .5},
{x: 3, y: 1},
{x: 6, y: 2},
{x: 7, y: 4},
{x: 6, y: 5},
],
isPath: false,
},
];
chart.resetRange();
canvas {
border: 1px solid;
}
<canvas width="500" height="500"></canvas>

How can i customize the lines of this canvas?

I started to work on this animation and got the base from another animation, i have pretty much customized it all to my needs besides the lines. Currently the lines are pointy and i have gone through the code multiple times trying to find what creates these spiky lines. I would appreciate if someone could check both the provided code and the external code and identify what it is. All help is appreciated thanks.
// Settings
var particleCount = 35,
flareCount = 0,
motion = 0.05,
tilt = 0,
particleSizeBase = 1,
particleSizeMultiplier = 0.5,
flareSizeBase = 100,
flareSizeMultiplier = 100,
glareAngle = -60,
glareOpacityMultiplier = 0.4,
renderParticles = true,
renderParticleGlare = true,
renderFlares = false,
renderLinks = false,
renderMesh = false,
flicker = false,
flickerSmoothing = 15, // higher = smoother flicker
blurSize = 0,
orbitTilt = true,
randomMotion = true,
noiseLength = 1000,
noiseStrength = 3;
document.querySelectorAll('.stars').forEach(canvas => {
var context = canvas.getContext('2d'),
color = canvas.dataset['color'],
mouse = { x: 0, y: 0 },
m = {},
r = 0,
c = 1000, // multiplier for delaunay points, since floats too small can mess up the algorithm
n = 0,
nAngle = (Math.PI * 2) / noiseLength,
nRad = 100,
nScale = 1,
nPos = {x: 0, y: 0},
points = [],
vertices = [],
triangles = [],
links = [],
particles = [],
flares = [];
function init() {
var i, j, k;
// requestAnimFrame polyfill
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
// Size canvas
resize();
mouse.x = canvas.clientWidth / 2;
mouse.y = canvas.clientHeight / 2;
// Create particle positions
for (i = 0; i < particleCount; i++) {
var p = new Particle();
particles.push(p);
points.push([p.x*c, p.y*c]);
}
vertices = Delaunay.triangulate(points);
var tri = [];
for (i = 0; i < vertices.length; i++) {
if (tri.length == 2) {
triangles.push(tri);
tri = [];
}
tri.push(vertices[i]);
}
// Tell all the particles who their neighbors are
for (i = 0; i < particles.length; i++) {
// Loop through all tirangles
for (j = 0; j < triangles.length; j++) {
// Check if this particle's index is in this triangle
k = triangles[j].indexOf(i);
// If it is, add its neighbors to the particles contacts list
if (k !== -1) {
triangles[j].forEach(function(value, index, array) {
if (value !== i && particles[i].neighbors.indexOf(value) == -1) {
particles[i].neighbors.push(value);
}
});
}
}
}
var fps = 60;
var now;
var then = Date.now();
var interval = 1000/fps;
var delta;
// Animation loop
(function animloop(){
requestAnimFrame(animloop);
now = Date.now();
delta = now - then;
if (delta > interval) {
then = now - (delta % interval);
resize();
render();
}
})();
}
function render() {
if (randomMotion) {
n++;
if (n >= noiseLength) {
n = 0;
}
nPos = noisePoint(n);
}
if (renderParticles) {
// Render particles
for (var i = 0; i < particleCount; i++) {
particles[i].render();
}
}
}
function resize() {
canvas.width = window.innerWidth * (window.devicePixelRatio || 1);
canvas.height = canvas.width * (canvas.clientHeight / canvas.clientWidth);
}
// Particle class
var Particle = function() {
this.x = random(-0.1, 1.1, true);
this.y = random(-0.1, 1.1, true);
this.z = random(0,4);
this.color = color;
this.opacity = random(0.1,1,true);
this.flicker = 0;
this.neighbors = []; // placeholder for neighbors
};
Particle.prototype.render = function() {
var pos = position(this.x, this.y, this.z),
r = ((this.z * particleSizeMultiplier) + particleSizeBase) * (sizeRatio() / 1000),
o = this.opacity;
context.fillStyle = this.color;
context.globalAlpha = o;
context.beginPath();
context.fill();
context.closePath();
if (renderParticleGlare) {
context.globalAlpha = o * glareOpacityMultiplier;
context.ellipse(pos.x, pos.y, r * 100, r, (glareAngle - ((nPos.x - 0.5) * noiseStrength * motion)) * (Math.PI / 180), 0, 2 * Math.PI, false);
context.fill();
context.closePath();
}
context.globalAlpha = 1;
};
// Utils
function noisePoint(i) {
var a = nAngle * i,
cosA = Math.cos(a),
sinA = Math.sin(a),
rad = nRad;
return {
x: rad * cosA,
y: rad * sinA
};
}
function position(x, y, z) {
return {
x: (x * canvas.width) + ((((canvas.width / 2) - mouse.x + ((nPos.x - 0.5) * noiseStrength)) * z) * motion),
y: (y * canvas.height) + ((((canvas.height / 2) - mouse.y + ((nPos.y - 0.5) * noiseStrength)) * z) * motion)
};
}
function sizeRatio() {
return canvas.width >= canvas.height ? canvas.width : canvas.height;
}
function random(min, max, float) {
return float ?
Math.random() * (max - min) + min :
Math.floor(Math.random() * (max - min + 1)) + min;
}
// init
if (canvas) init();
});
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
body {
background: #000;
background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0.00) 0%, #000000 100%);
height: 100%;
}
#stars {
display: block;
position: relative;
width: 100%;
height: 100vh;
z-index: 1;
position: absolute;
}
<script src="https://rawgit.com/ironwallaby/delaunay/master/delaunay.js"></script>
<script src="http://requirejs.org/docs/release/2.1.15/minified/require.js"></script>
<canvas id="Stars" class="stars" width="300" height="300" data-color="#fff"></canvas>
// Tell all the particles who their neighbors are
for (i = 0; i < particles.length; i++) {
// Loop through all tirangles
for (j = 0; j < triangles.length; j++) {
// Check if this particle's index is in this triangle
k = triangles[j].indexOf(i);
// If it is, add its neighbors to the particles contacts list
if (k !== -1) {
triangles[j].forEach(function(value, index, array) { // <----- missing ')' here
if (value !== i && particles[i].neighbors.indexOf(value) == -1) {
particles[i].neighbors.push(value);
}
});
}
}
}
The reason you are getting spikes is the radius of the ellipse is too large so the ends are so small they look like points but it's just a small radius.
context.ellipse(pos.x, pos.y, r * 100, r, (glareAngle - ((nPos.x - 0.5) * noiseStrength * motion)) * (Math.PI / 180), 0, 2 * Math.PI, false);
After experimenting with different shapes like rect() and arc():
The code below instead of using ellipse uses a rotated rectangle. Since the shape is different the algorithm for getting the angle you wanted will need more work but this code solves the problem of the pointy ends.
//context.ellipse(pos.x, pos.y, r * 100, r, (glareAngle - ((nPos.x - 0.5) * noiseStrength * motion)) * (Math.PI / 180), 0, 2 * Math.PI, false);
context.rotate((glareAngle - ((nPos.x - 0.5) * noiseStrength * motion)) * (Math.PI / 180));
context.fillRect(pos.x, pos.y, r * 100, r)
context.closePath();
It will take more work to align them to match the first code, but the pointy ends are gone.
Also, rotate() is actually rotating the canvas not the rectangles so keep that in mind. I would start with a simple 45 degree angle and see what that generates.

Zooming in at mouse position p5.js (jsfiddle included)

I'm trying to zoom at mouse position, like say on google maps. It kind of works but it shifts the point i want to zoom in on wherever it matches up with the original.Then when i zoom at that point it works fine. I think I need to translate the point back to the mouse, but I'm not sure how to do it exactly.
This is the code before i draw:
translate(zoomLocation.x, zoomLocation.y);
scale(zoom);
translate(-zoomLocation.x, -zoomLocation.y);
drawGrid();
And this is when I zoom:
event.preventDefault();
zoomLocation = {
x: zoomLocation.x + (mouseX - zoomLocation.x) / zoom,
y: zoomLocation.y + (mouseY - zoomLocation.y) / zoom
};
zoom -= zoomSensitivity * event.delta;
let colors = {
background: 0,
gridLines: "white"
};
let nVariables = 4;
let zoom = 1;
let zoomLocation = {
x: 0,
y: 0
};
let zoomSensitivity = 0.0002;
function draw() {
translate(zoomLocation.x, zoomLocation.y);
scale(zoom);
translate(-zoomLocation.x, -zoomLocation.y);
drawGrid();
stroke("blue");
ellipse(zoomLocation.x + (mouseX - zoomLocation.x) / zoom, zoomLocation.y + (mouseY - zoomLocation.y) / zoom, 10, 10);
stroke("red");
ellipse(zoomLocation.x, zoomLocation.y, 10, 10);
}
function setup() {
zoomLocation = {
x: 0,
y: windowHeight / 2
}
createCanvas(windowWidth, windowHeight);
}
function mouseWheel(event) {
event.preventDefault();
let oldZoom = zoom;
zoomLocation = {
x: zoomLocation.x + (mouseX - zoomLocation.x) / zoom,
y: zoomLocation.y + (mouseY - zoomLocation.y) / zoom
};
zoom -= zoomSensitivity * event.delta;
}
function drawGrid() {
let nCells = 2 ** nVariables;
if (nCells > 2048) {
if (!window.confirm(`You are about to create ${nCells} cells. This might lag your browser. Are you sure?`)) {
return;
}
}
background(colors.background);
let gridWidth = windowWidth - 2;
let gridHeight = min(gridWidth / nCells, windowHeight / 2 - 2);
let gridY = windowHeight / 2;
stroke(colors.gridLines);
line(0, gridY, gridWidth, gridY);
line(0, gridY + gridHeight, gridWidth, gridY + gridHeight);
for (let i = 0; i < nCells + 1; i++) {
line(i * (gridWidth / nCells), gridY, i * (gridWidth / nCells), gridY + gridHeight)
}
let curveHeight = 2;
let drawVariable = (n) => {
let p1 = {
x: 1 / (2 ** (n + 1)) * gridWidth,
y: gridY
};
let c1 = {
x: p1.x,
y: p1.y + gridWidth / (2 ** n) * curveHeight
};
let p2 = {
y: p1.y
};
let c2 = {
y: c1.y
};;
noFill();
stroke("red");
if (n == 0) {
p2.x = gridWidth;
c2.x = p2.x;
c1.y = c2.y = p1.y + gridWidth / 2 * curveHeight;
curve(c1.x, c1.y, p1.x, p1.y, p2.x, p2.y, c2.x, c2.y);
return;
}
for (let i = 3; i < 2 ** (n + 1); i += 2) {
p2.x = i / (2 ** (n + 1)) * gridWidth
c2.x = p2.x;
if ((i - 3) % 4 == 0) {
curve(c1.x, c1.y, p1.x, p1.y, p2.x, p2.y, c2.x, c2.y);
} else {
p1.x = p2.x;
c1.x = c2.x;
}
}
};
for (let i = 0; i < nVariables; i++) {
drawVariable(i);
}
}
body {
margin: 0
}
button {
outline: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/addons/p5.dom.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.js" integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE=" crossorigin="anonymous"></script>
What you're looking for is an application of Affine Transformation. Here's an example where I'm applying transformations incrementally to transform + scale at the mouse pointer location for a Google Maps type zoom effect: Zoom Effect in p5.js Web Editor.
This is a good medium article that explains why this works: Zooming at the Mouse Coordinates with Affine Transformations
This is another good article that has some mathematical explanations on how it achieves the effects: Affine Transformations — Pan, Zoom, Skew

Why circles are vibrating on collision (Canvas)

I have been creating a clone of agar.io and I don't understand why the circles start vibrating when they touch each other. Below is my code:
var
canvas,
ctx,
width = innerWidth,
height = innerHeight,
mouseX = 0,
mouseY = 0;
var
camera = {
x: 0,
y: 0,
update: function(obj) {
this.x = obj.x - width / 2;
this.y = obj.y - height / 2;
}
},
player = {
defaultMass: 54,
x: 0,
y: 0,
blobs: [],
update: function() {
for (var i = 0; i < this.blobs.length; i++) {
var x = mouseX + camera.x - this.blobs[i].x;
var y = mouseY + camera.y - this.blobs[i].y;
var length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
var speed = 54 / this.blobs[i].mass;
this.blobs[i].velX = x / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].velY = y / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].x += this.blobs[i].velX;
this.blobs[i].y += this.blobs[i].velY;
for (var j = 0; j < this.blobs.length; j++) {
if (j != i && this.blobs[i] !== undefined) {
var blob1 = this.blobs[i];
var blob2 = this.blobs[j];
var dist = Math.sqrt(Math.pow(blob2.x - blob1.x, 2) + Math.pow(blob2.y - blob1.y, 2));
if (dist < blob1.mass + blob2.mass) {
if (this.blobs[i].x < this.blobs[j].x) {
this.blobs[i].x--;
} else if (this.blobs[i].x > this.blobs[j].x) {
this.blobs[i].x++;
}
if (this.blobs[i].y < this.blobs[j].y) {
this.blobs[i].y--;
} else if ((this.blobs[i].y > this.blobs[j].y)) {
this.blobs[i].y++;
}
}
}
}
}
this.x += (mouseX - width / 2) / (width / 2) * 1;
this.y += (mouseY - height / 2) / (height / 2) * 1
},
split: function(cell) {
cell.mass /= 2;
this.blobs.push({
x: cell.x,
y: cell.y,
mass: cell.mass
});
},
draw: function() {
for (var i = 0; i < this.blobs.length; i++) {
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(-camera.x + this.blobs[i].x, -camera.y + this.blobs[i].y, this.blobs[i].mass, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
};
function handleMouseMove(e) {
mouseX = e.clientX;
mouseY = e.clientY;
}
function setup() {
canvas = document.getElementById("game");
ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
addEventListener("mousemove", handleMouseMove);
player.blobs.push({
x: 0,
y: 0,
mass: player.defaultMass
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass / 2
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass * 2
});
var loop = function() {
update();
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function update() {
camera.update(player.blobs[0]);
player.update();
}
function draw() {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
player.draw();
}
setup();
body {
margin: 0;
padding: 0;
}
<canvas id="game">kindly update your browser.</canvas>
Separating circles
Your separation code was not correct. Use the vector between them to get the new pos.
The vector between them
To find if two circles are intercepting find the length of the vector from one to the next
The two circles.
var cir1 = {x : 100, y : 100, r : 120}; // r is the radius
var cir2 = {x : 250, y : 280, r : 150}; // r is the radius
The vector from cir2 to cir1
var vx = cir2.x - cir1.x;
var vy = cir2.y - cir1.y;
The length of the vector
var len = Math.sqrt(x * x + y * y);
// or use the ES6 Math.hypot function
/* var len = Math.hypot(x,y); */
The circles overlap if the sum of the radii is greater than the length of the vector between them
if(cir1.r + cir2.r > len){ // circles overlap
Normalise the vector
If they overlap you need to move one away from the other. There are many ways to do this, the simplest way is to move one circle along the line between them.
First normalise the vector from cir1 to cir2 by dividing by its (vector) length.
vx \= len;
vy \= len;
Note that the length could be zero. If this happens then you will get NaN in further calculations. If you suspect you may get one circle at the same location as another the easiest way to deal with the zero move one circle a little.
// replace the two lines above with
if(len === 0){ // circles are on top of each other
vx = 1; // move the circle (abstracted into the vector)
}else{
vx \= len; // normalise the vector
vy \= len;
}
Move circle/s to just touch
Now you have the normalised vector which is 1 unit long you can make it any length you need by multiplying the two scalars vx, vy with the desired length which in this case is the sum of the two circles radii.
var mx = vx * (cir1.r + cir2.r); // move distance
var my = vy * (cir1.r + cir2.r);
.Only use one of the following methods.
You can now position one of the circles the correct distance so that they just touch
// move cir1
cir1.x = cir2.x - mx;
cir1.y = cir2.y - my;
Or move the second circle
cir2.x = cir1.x + mx;
cir2.y = cir1.y + my;
Or move both circles but you will have to first find the proportional center between the two
var pLen = cir1.r / (cir1.r + cir2.r); // find the ratio of the radii
var cx = cir1.x + pLen * vx * len; // find the proportional center between
var cy = cir1.y + pLen * vy * len; // the two circles
Then move both circles away from that point by their radii
cir1.x = cx - vx * cir1.r; // move circle 1 away from the shared center
cir1.y = cy - vy * cir1.r;
cir2.x = cx + vx * cir2.r; // move circle 2 away from the shared center
cir2.y = cy + vy * cir2.r;
DEMO
Copy of OP's snippet with mods to fix problem by moving the the first circle blob1 away from the second blob2 and assuming they will never be at the same spot (no divide by zero)
var
canvas,
ctx,
width = innerWidth,
height = innerHeight,
mouseX = 0,
mouseY = 0;
var
camera = {
x: 0,
y: 0,
update: function(obj) {
this.x = obj.x - width / 2;
this.y = obj.y - height / 2;
}
},
player = {
defaultMass: 54,
x: 0,
y: 0,
blobs: [],
update: function() {
for (var i = 0; i < this.blobs.length; i++) {
var x = mouseX + camera.x - this.blobs[i].x;
var y = mouseY + camera.y - this.blobs[i].y;
var length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
var speed = 54 / this.blobs[i].mass;
this.blobs[i].velX = x / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].velY = y / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].x += this.blobs[i].velX;
this.blobs[i].y += this.blobs[i].velY;
for (var j = 0; j < this.blobs.length; j++) {
if (j != i && this.blobs[i] !== undefined) {
var blob1 = this.blobs[i];
var blob2 = this.blobs[j];
var x = blob2.x - blob1.x; // get the vector from blob1 to blob2
var y = blob2.y - blob1.y; //
var dist = Math.sqrt(x * x + y * y); // get the distance between the two blobs
if (dist < blob1.mass + blob2.mass) { // if the distance is less than the 2 radius
// if there is overlap move blob one along the line between the two the distance of the two radius
x /= dist; // normalize the vector. This makes the vector 1 unit long
y /= dist;
// multiplying the normalised vector by the correct distance between the two
// and subtracting that distance from the blob 2 give the new pos of
// blob 1
blob1.x = blob2.x - x * (blob1.mass + blob2.mass);
blob1.y = blob2.y - y * (blob1.mass + blob2.mass);
}
}
}
}
this.x += (mouseX - width / 2) / (width / 2) * 1;
this.y += (mouseY - height / 2) / (height / 2) * 1
},
split: function(cell) {
cell.mass /= 2;
this.blobs.push({
x: cell.x,
y: cell.y,
mass: cell.mass
});
},
draw: function() {
for (var i = 0; i < this.blobs.length; i++) {
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(-camera.x + this.blobs[i].x, -camera.y + this.blobs[i].y, this.blobs[i].mass, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
};
function handleMouseMove(e) {
mouseX = e.clientX;
mouseY = e.clientY;
}
function setup() {
canvas = document.getElementById("game");
ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
addEventListener("mousemove", handleMouseMove);
player.blobs.push({
x: 0,
y: 0,
mass: player.defaultMass
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass / 2
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass * 2
});
var loop = function() {
update();
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function update() {
camera.update(player.blobs[0]);
player.update();
}
function draw() {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
player.draw();
}
setup();
body {
margin: 0;
padding: 0;
}
<canvas id="game">kindly update your browser.</canvas>
var
canvas,
ctx,
width = innerWidth,
height = innerHeight,
mouseX = 0,
mouseY = 0;
var
camera = {
x: 0,
y: 0,
update: function(obj) {
this.x = obj.x - width / 2;
this.y = obj.y - height / 2;
}
},
player = {
defaultMass: 54,
x: 0,
y: 0,
blobs: [],
update: function() {
for (var i = 0; i < this.blobs.length; i++) {
var x = mouseX + camera.x - this.blobs[i].x;
var y = mouseY + camera.y - this.blobs[i].y;
var length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
var speed = 54 / this.blobs[i].mass;
this.blobs[i].velX = x / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].velY = y / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].x += this.blobs[i].velX;
this.blobs[i].y += this.blobs[i].velY;
for (var j = 0; j < this.blobs.length; j++) {
if (j != i && this.blobs[i] !== undefined) {
var blob1 = this.blobs[i];
var blob2 = this.blobs[j];
var dist = Math.sqrt(Math.pow(blob2.x - blob1.x, 2) + Math.pow(blob2.y - blob1.y, 2));
if (dist < blob1.mass + blob2.mass) {
if (this.blobs[i].x < this.blobs[j].x) {
this.blobs[i].x--;
} else if (this.blobs[i].x > this.blobs[j].x) {
this.blobs[i].x++;
}
if (this.blobs[i].y < this.blobs[j].y) {
this.blobs[i].y--;
} else if ((this.blobs[i].y > this.blobs[j].y)) {
this.blobs[i].y++;
}
}
}
}
}
this.x += (mouseX - width / 2) / (width / 2) * 1;
this.y += (mouseY - height / 2) / (height / 2) * 1
},
split: function(cell) {
cell.mass /= 2;
this.blobs.push({
x: cell.x,
y: cell.y,
mass: cell.mass
});
},
draw: function() {
for (var i = 0; i < this.blobs.length; i++) {
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(-camera.x + this.blobs[i].x, -camera.y + this.blobs[i].y, this.blobs[i].mass, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
};
function handleMouseMove(e) {
mouseX = e.clientX;
mouseY = e.clientY;
}
function setup() {
canvas = document.getElementById("game");
ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
addEventListener("mousemove", handleMouseMove);
player.blobs.push({
x: 0,
y: 0,
mass: player.defaultMass
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass / 2
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass * 2
});
var loop = function() {
update();
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function update() {
camera.update(player.blobs[0]);
player.update();
}
function draw() {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
player.draw();
}
setup();
body {
margin: 0;
padding: 0;
}
<canvas id="game">kindly update your browser.</canvas>

Categories