I tried to use canvas in middle of my html page.but it does't work fine. I want to use ribbon effect in div. it works fine when the there is no div and when there is div or other element it doesn't work. I want use canvas in between two div. I used ribbon pen in codepen and will post the code below. I want to how to use it in my html page.
var TWO_PI = Math.PI * 2;
var HALF_PI = Math.PI * 0.5;
var THICKNESS = 12;
var LENGTH = 10;
var STEP = 0.1;
var FPS = 1000 / 60;
function Particle(x, y, mass) {
this.x = x || 0;
this.y = y || 0;
this.ox = this.x;
this.oy = this.y;
this.mass = mass || 1.0;
this.massInv = 1.0 / this.mass;
this.fixed = false;
this.update = function (dt) {
if (!this.fixed) {
var fx = 0.0000;
var fy = 0.0000;
var tx = this.x,
ty = this.y;
this.x += (this.x - this.ox) + fx * this.massInv * dt * dt;
this.y += (this.y - this.oy) + fy * this.massInv * dt * dt;
this.ox = tx;
this.oy = ty;
}
};
};
function Spring(p1, p2, restLength, strength) {
this.p1 = p1;
this.p2 = p2;
this.restLength = restLength || 10;
this.strength = strength || 1.0;
this.update = function (dt) {
// Compute desired force
var dx = p2.x - p1.x,
dy = p2.y - p1.y,
dd = Math.sqrt(dx * dx + dy * dy) + 0.0001,
tf = (dd - this.restLength) / (dd * (p1.massInv + p2.massInv)) * this.strength,
f;
// Apply forces
if (!p1.fixed) {
f = tf * p1.massInv;
p1.x += dx * f;
p1.y += dy * f;
}
if (!p2.fixed) {
f = -tf * p2.massInv;
p2.x += dx * f;
p2.y += dy * f;
}
}
};
function Sim() {
this.particles = [];
this.springs = [];
this.tick = function (dt) {
var i, n;
for (i = 0, n = this.springs.length; i < n; ++i) {
this.springs[i].update(dt);
}
for (i = 0, n = this.particles.length; i < n; ++i) {
this.particles[i].update(dt);
}
}
};
// Create a new system
var sim = new Sim(),
old = new Date().getTime(),
canvas = document.getElementById('world'),
context = canvas.getContext('2d');
function init() {
var np,
op,
mouse,
anchor,
step = STEP,
length = LENGTH,
count = length / step;
var sx = canvas.width * 0.5;
var sy = canvas.height * 0.5;
for (var i = 0; i < count; ++i) {
//np = new Particle(i*8,i*8,0.1+Math.random()*0.01);
np = new Particle(sx + (Math.random() - 0.5) * 200, sy + (Math.random() - 0.5) * 200, 0.1 + Math.random() * 0.01);
sim.particles.push(np);
if (i > 0) {
s = new Spring(np, op, step, 0.95);
sim.springs.push(s);
}
op = np;
}
// Fix the first particle
anchor = sim.particles[0];
//anchor.fixed = true;
anchor.x = 50;
anchor.y = 50;
// Move last particle with mouse
mouse = sim.particles[count - 1];
mouse.fixed = true;
canvas.addEventListener('mousemove', function (event) {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
};
function step() {
var now = new Date().getTime(),
delta = now - old;
sim.tick(delta);
// Clear canvas
canvas.width = canvas.width;
var points = []; // Midpoints
var angles = []; // Delta angles
var i, n, p1, p2, dx, dy, mx, my, sin, cos, theta;
// Compute midpoints and angles
for (i = 0, n = sim.particles.length - 1; i < n; ++i) {
p1 = sim.particles[i];
p2 = sim.particles[i + 1];
dx = p2.x - p1.x;
dy = p2.y - p1.y;
mx = p1.x + dx * 0.5;
my = p1.y + dy * 0.5;
points[i] = {
x: mx,
y: my
};
angles[i] = Math.atan2(dy, dx);
}
// Render
context.beginPath();
for (i = 0, n = points.length; i < n; ++i) {
p1 = sim.particles[i];
p2 = points[i];
theta = angles[i];
r = Math.sin((i / n) * Math.PI) * THICKNESS;
sin = Math.sin(theta - HALF_PI) * r;
cos = Math.cos(theta - HALF_PI) * r;
context.quadraticCurveTo(
p1.x + cos,
p1.y + sin,
p2.x + cos,
p2.y + sin);
}
for (i = points.length - 1; i >= 0; --i) {
p1 = sim.particles[i + 1];
p2 = points[i];
theta = angles[i];
r = Math.sin((i / n) * Math.PI) * THICKNESS;
sin = Math.sin(theta + HALF_PI) * r;
cos = Math.cos(theta + HALF_PI) * r;
context.quadraticCurveTo(
p1.x + cos,
p1.y + sin,
p2.x + cos,
p2.y + sin);
}
context.strokeStyle = 'rgba(255,255,255,0.1)';
context.lineWidth = 8;
context.stroke();
context.strokeStyle = 'rgba(0,0,0,0.8)';
context.lineWidth = 0.5;
context.stroke();
context.fillStyle = 'rgba(255,255,255,0.9)';
context.fill();
old = now;
setTimeout(step, FPS);
};
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener("resize", resize);
resize();
init();
step();
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'>
<canvas id='world' width='500' height='500'></canvas>
<header><h1>Wiggle your mouse...</h1></header>
Here is one way:
HTML:
<div class="top">
top
</div>
<div class="middle">
<canvas id='world' width='500' height='500'></canvas>
<header><h1>Wiggle your mouse...</h1></header>
</div>
<div class="bottom">
bottom
</div>
CSS:
div {
border: 1px solid black
}
.top, .bottom {
height: 200px
}
The js remains the same. The CSS gives the top and bottom divs some height. The canvas is in the middle div. Here is a jsfiddle: https://jsfiddle.net/av902pcs/
Related
I'm working on a custom button animation for my Wix website it is written in JavaScript and I get this error when I plug the code into an onclick event:
"cannot read property 'requestAnimationFrame' of undefined"
I've tried removing the return and I think the problem has to do with some sort of iframe issue because the original code was built to run in an html <iframe> element but that dident end up working out.
If someone could rewrite this code so that it dosent give this error that would be super helpful.
export function button7_click(event) {
$w.window.requestAnimFrame = (function() {
return $w.window.requestAnimationFrame ||
$w.window.webkitRequestAnimationFrame ||
$w.window.mozRequestAnimationFrame ||
$w.window.oRequestAnimationFrame ||
$w.window.msRequestAnimationFrame ||
function(callback) {
$w.window.setTimeout(callback, 1000 / 60);
};
})();
Math.randMinMax = function(min, max, round) {
var val = min + (Math.random() * (max - min));
if (round) val = Math.round(val);
return val;
};
Math.TO_RAD = Math.PI / 180;
Math.getAngle = function(x1, y1, x2, y2) {
var dx = x1 - x2,
dy = y1 - y2;
return Math.atan2(dy, dx);
};
Math.getDistance = function(x1, y1, x2, y2) {
var xs = x2 - x1,
ys = y2 - y1;
xs *= xs;
ys *= ys;
return Math.sqrt(xs + ys);
};
var FX = {};
(function() {
var canvas = $w.document.getElementById('myCanvas'),
ctx = canvas.getContext('2d'),
lastUpdate = new Date(),
mouseUpdate = new Date(),
lastMouse = [],
width, height;
FX.particles = [];
setFullscreen();
$w.document.getElementById('button').addEventListener('mousedown', buttonEffect);
function buttonEffect() {
var button = $w.document.getElementById('button'),
height = button.offsetHeight,
left = button.offsetLeft,
top = button.offsetTop,
width = button.offsetWidth,
x, y, degree;
for (var i = 0; i < 40; i = i + 1) {
if (Math.random() < 0.5) {
y = Math.randMinMax(top, top + height);
if (Math.random() < 0.5) {
x = left;
degree = Math.randMinMax(-45, 45);
} else {
x = left + width;
degree = Math.randMinMax(135, 225);
}
} else {
x = Math.randMinMax(left, left + width);
if (Math.random() < 0.5) {
y = top;
degree = Math.randMinMax(45, 135);
} else {
y = top + height;
degree = Math.randMinMax(-135, -45);
}
}
createParticle({
x: x,
y: y,
degree: degree,
speed: Math.randMinMax(100, 150),
vs: Math.randMinMax(-4, -1)
});
}
}
$w.window.setTimeout(buttonEffect, 100);
loop();
$w.window.addEventListener('resize', setFullscreen);
function createParticle(args) {
var options = {
x: width / 2,
y: height / 2,
color: 'hsla(' + Math.randMinMax(160, 290) + ', 100%, 50%, ' + (Math.random().toFixed(2)) + ')',
degree: Math.randMinMax(0, 360),
speed: Math.randMinMax(300, 350),
vd: Math.randMinMax(-90, 90),
vs: Math.randMinMax(-8, -5)
};
for ($w.key in args) {
options[$w.key] = args[$w.key];
}
FX.particles.push(options);
}
function loop() {
var thisUpdate = new Date(),
delta = (lastUpdate - thisUpdate) / 1000,
amount = FX.particles.length,
size = 2,
i = 0,
p;
ctx.fillStyle = 'rgba(15,15,15,0.25)';
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeStyle = 'lighter';
for (; i < amount; i = i + 1) {
p = FX.particles[i];
p.degree += (p.vd * delta);
p.speed += (p.vs); // * delta);
if (p.speed < 0) continue;
p.x += Math.cos(p.degree * Math.TO_RAD) * (p.speed * delta);
p.y += Math.sin(p.degree * Math.TO_RAD) * (p.speed * delta);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.degree * Math.TO_RAD);
ctx.fillStyle = p.color;
ctx.fillRect(-size, -size, size * 2, size * 2);
ctx.restore();
}
lastUpdate = thisUpdate;
$w.requestAnimFrame(loop);
}
function setFullscreen() {
width = canvas.width = $w.window.innerWidth;
height = canvas.height = $w.window.innerHeight;
};
})();
}
The button once clicked should run the JavaScript code and play the animation. Thanks for your help.
EDIT: I'm trying to get a single line colored, not the whole grid
So, what I'm trying to do is that when the canvas gets clicked, the lines selected move and get colored.
I already have the lines moving but I cannot get them colored. I know I have to use the strokeStyle property, but how do I get the specific line colored?
I tried putting the strokeStyle under the draw_each method, but that doesn't work, and I don't know why. Here's my code:
//draw each line in array
function draw_each(p1, p2, p3, p4) {
$.moveTo(p1.x, p1.y);
$.lineTo(p2.x, p2.y);
$.moveTo(p1.x, p1.y);
$.lineTo(p4.x, p4.y);
if (p1.ind_x == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p4.x, p4.y);
}
if (p1.ind_y == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p2.x, p2.y);
}
}
https://codepen.io/diazabdulm/pen/qJyrZP?editors=0010
It seems to me that what you want is actually to generate a color per segment based on the force that's been applied to it, so that the wave effect keeps its effect.
What you can do, is to save the median of each node's velocity in both axis as a mean to represent the force they're currently experiencing, and in your drawing part to get the median of all nodes that will compose each segment of your grid and compose a color from it.
But this comes with a drawback: your code was well written enough to compose the whole grid a single sub-path, thus limiting the number of drawings necessary. But now, we have to make each segment its own sub-path (since they'll got their own color), so we'll loose a lot in terms of performances...
var gnum = 90; //num grids / frame
var _x = 2265; //x width (canvas width)
var _y = 1465; //y height (canvas height)
var w = _x / gnum; //grid sq width
var h = _y / gnum; //grid sq height
var $; //context
var parts; //particles
var frm = 0; //value from
var P1 = 0.0005; //point one
var P2 = 0.01; //point two
var n = 0.98; //n value for later
var n_vel = 0.03; //velocity
var ŭ = 0; //color update
var msX = 0; //mouse x
var msY = 0; //mouse y
var msdn = false; //mouse down flag
var Part = function() {
this.x = 0; //x pos
this.y = 0; //y pos
this.vx = 0; //velocity x
this.vy = 0; //velocity y
this.ind_x = 0; //index x
this.ind_y = 0; //index y
};
Part.prototype.frame = function() {
if (this.ind_x == 0 || this.ind_x == gnum - 1 || this.ind_y == 0 || this.ind_y == gnum - 1) {
return;
}
var ax = 0; //angle x
var ay = 0; //angle y
//off_dx, off_dy = offset distance x, y
var off_dx = this.ind_x * w - this.x;
var off_dy = this.ind_y * h - this.y;
ax = P1 * off_dx;
ay = P1 * off_dy;
ax -= P2 * (this.x - parts[this.ind_x - 1][this.ind_y].x);
ay -= P2 * (this.y - parts[this.ind_x - 1][this.ind_y].y);
ax -= P2 * (this.x - parts[this.ind_x + 1][this.ind_y].x);
ay -= P2 * (this.y - parts[this.ind_x + 1][this.ind_y].y);
ax -= P2 * (this.x - parts[this.ind_x][this.ind_y - 1].x);
ay -= P2 * (this.y - parts[this.ind_x][this.ind_y - 1].y);
ax -= P2 * (this.x - parts[this.ind_x][this.ind_y + 1].x);
ay -= P2 * (this.y - parts[this.ind_x][this.ind_y + 1].y);
this.vx += (ax - this.vx * n_vel);
this.vy += (ay - this.vy * n_vel);
//EDIT\\
// store the current velocity (here base on 100 since it will be used with hsl())
this.color = (Math.abs(this.vx)+Math.abs(this.vy)) * 50;
this.x += this.vx * n;
this.y += this.vy * n;
if (msdn) {
var dx = this.x - msX;
var dy = this.y - msY;
var ɋ = Math.sqrt(dx * dx + dy * dy);
if (ɋ > 50) {
ɋ = ɋ < 10 ? 10 : ɋ;
this.x -= dx / ɋ * 5;
this.y -= dy / ɋ * 5;
}
}
};
function go() {
parts = []; //particle array
for (var i = 0; i < gnum; i++) {
parts.push([]);
for (var j = 0; j < gnum; j++) {
var p = new Part();
p.ind_x = i;
p.ind_y = j;
p.x = i * w;
p.y = j * h;
parts[i][j] = p;
}
}
}
//move particles function
function mv_part() {
for (var i = 0; i < gnum; i++) {
for (var j = 0; j < gnum; j++) {
var p = parts[i][j];
p.frame();
}
}
}
//draw grid function
function draw() {
//EDIT
// we unfortunately have to break the drawing part
// since each segment has its own color, we can't have a single sub-path anymore...
ŭ -= .5;
for (var i = 0; i < gnum - 1; i += 1) {
for (var j = 0; j < gnum - 1; j += 1) {
var p1 = parts[i][j];
var p2 = parts[i][j + 1];
var p3 = parts[i + 1][j + 1];
var p4 = parts[i + 1][j];
draw_each(p1, p2, p3, p4);
}
}
}
//draw each in array
function draw_each(p1, p2, p3, p4) {
// for each segment we set the color
$.strokeStyle = `hsl(0deg, ${(p1.color+p2.color+p3.color+p4.color) / 4}%, 50%)`;
// begin a new sub-path
$.beginPath();
$.moveTo(p1.x, p1.y);
$.lineTo(p2.x, p2.y);
$.moveTo(p1.x, p1.y);
$.lineTo(p4.x, p4.y);
if (p1.ind_x == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p4.x, p4.y);
}
if (p1.ind_y == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p2.x, p2.y);
}
// and stroke it
$.stroke();
}
//call functions to run
function calls() {
$.fillStyle = "hsla(0, 0%, 7%, 1)";
$.fillRect(0, 0, _x, _y);
mv_part();
draw();
frm++;
}
var c = document.getElementById('canv');
var $ = c.getContext('2d');
$.fillStyle = "hsla(0, 0%, 7%, 1)";
$.fillRect(0, 0, _x, _y);
function resize() {
if (c.width < window.innerWidth) {
c.width = window.innerWidth;
}
if (c.height < window.innerHeight) {
c.height = window.innerHeight;
}
}
requestAnimationFrame(go);
document.addEventListener('click', MSMV, false);
document.addEventListener('click', MSDN, false);
function MSDN(e) {
msdn = true;
window.setTimeout(function() {
msdn = false;
}, 100);
}
function MSUP(e) {
msdn = false;
}
function MSMV(e) {
var rect = e.target.getBoundingClientRect();
msX = e.clientX - rect.left;
msY = e.clientY - rect.top;
}
window.onload = function() {
run();
function run() {
requestAnimationFrame(calls);
requestAnimationFrame(run);
}
resize();
};
onresize = resize;
body {
width: 100%;
overflow: hidden;
cursor:move;
}
<canvas id="canv" width="150" height="150"></canvas>
Now, a more performant way, but less good looking, would be to draw a radial-gradient where the click happened, with a bit of compositing, you'd be able to get something cheap that might work, but it would be quite a lot of coding actually to get multiple such gradients at the same time...
That works for me:
//draw each line in array
function draw_each(p1, p2, p3, p4) {
$.moveTo(p1.x, p1.y);
$.lineTo(p2.x, p2.y);
$.moveTo(p1.x, p1.y);
$.lineTo(p4.x, p4.y);
if (p1.ind_x == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p4.x, p4.y);
}
if (p1.ind_y == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p2.x, p2.y);
}
$.strokeStyle = '#458920';
}
I have this code for separating two circles. the problem is that whenever the radius of one of the circle changes (ie becomes bigger or smaller than the other) the circles start moving somewhere else. i am trying to move the circles apart together (). i dont know much about vectors. i got the vector posrtion of my code from Why circles are vibrating on collision (Canvas)
CollisionHandler.pushApart = function(cell, check) {
var x = check.x - cell.x;
var y = check.y - cell.y;
var distance = Math.hypot(x, y);
var cSz = Blob.getSize(cell.mass);
var chSz = Blob.getSize(check.mass);
var maxDist = cSz + chSz;
var boosting = Math.abs(cell.move.x) > 2 ||
Math.abs(cell.move.y) > 2 ||
Math.abs(check.move.x) > 2 ||
Math.abs(check.move.y) > 2;
if (distance < maxDist && !boosting) {
x /= distance;
y /= distance;
var pLen = cSz / (cSz + chSz);
var cx = cell.x + pLen * x * distance;
var cy = cell.y + pLen * y * distance;
cell.x += (cx - x * chSz - cell.x) * animationConstant;
cell.y += (cy - y * chSz - cell.y) * animationConstant;
check.x += (cx + x * cSz - check.x) * animationConstant;
check.y += (cy + y * cSz - check.y) * animationConstant;
/*var targetX = check.x - maxDistance * x;
var targetY = check.y - maxDistance * y;
cell.x += (targetX - cell.x) * animationConstant;
cell.y += (targetY - cell.y) * animationConstant;*/
}
};
<!DOCTYPE html>
<html>
<head>
<title>this is my clone of agar.io.</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas></canvas>
<script>
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
var width = innerWidth;
var height = innerHeight;
canvas.width = width;
canvas.height = height;
var blobsFood = [];
var blobsVirus = [];
var blobsEject = [];
var blobsPlayer = [];
function getRandom(n) {
return Math.random() * n;
};
var gameWidth = 10000;
var gameHeight = 10000;
var massFood = 5;
var animationConstant = 0.2;
var maxSplit = 64;
function Blob(x, y, mass, hue) {
this.x = x;
this.y = y;
this.mass = mass;
this._mass = mass;
this.hue = hue;
this.time = Date.now();
this.move = {
x: 0,
y: 0,
angle: 0
};
};
Blob.getSize = function(mass) {
return Math.sqrt(mass * 100);
};
Blob.getSpeed = function(mass) {
return 15 * 1.6/Math.pow(mass, 0.32);
};
Blob.getBoostSpeed = function(mass) {
return 15 * 2.6 * Math.pow(mass, 0.0122);
};
Blob.prototype.boostMove = function() {
this.x += this.move.x;
this.y += this.move.y;
this.move.x *= 0.95;
this.move.y *= 0.95;
};
Blob.prototype.getAngle = function() {
return this.move.angle;
};
Blob.prototype.draw = function() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.beginPath();
this._mass += (this.mass - this._mass) * animationConstant;
ctx.arc(0, 0, Blob.getSize(this._mass), 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = "hsl(" + this.hue + ", 100%, 60%)";
ctx.strokeStyle = "hsl(" + this.hue + ", 100%, 50%)";
ctx.lineWidth = 5;
ctx.fill();
ctx.stroke();
ctx.restore();
};
Blob.prototype.onEaten = function(eater) {
// For virus and other cells
};
Blob.prototype.onEat = function(prey) {
this.mass += prey.mass;
};
function Virus() {
this.x = Math.random() * gameWidth;
this.y = Math.random() * gameWidth;
this.mass = 100;
this._mass = 100;
this.maxMass = 150;
this.hue = Math.random() * 360;
};
Virus.prototype = new Blob();
Virus.prototype.onEat = function(prey) {
this.mass += prey.mass;
if(this.mass >= this.maxMass) {
shootVirus(this, prey.getAngle());
}
};
Virus.prototype.onEaten = function(eater) {
var numSplits = maxSplit - blobsPlayer.length;
if(numSplits <= 0)
return;
var massLeft = eater.mass;
var splitAmount = 1;
var smallMass = 50;
while(massLeft > 0) {
splitAmount *= 2;
massLeft = eater.mass - splitAmount * 30;
};
var splitMass = eater.mass/splitAmount;
for(var i = 0; i < Math.min(splitAmount, numSplits); i++) {
if (eater.mass <= smallMass)
break;
var angle = Math.random() * 2 * Math.PI;
addPlayer(eater.x, eater.y, splitMass, angle, eater.hue);
eater.mass -= splitMass;
};
};
function CollisionHandler() {
console.log("CollisionHandler started.");
};
CollisionHandler.eatFactor = 0.02;
CollisionHandler.canEat = function(eater, check) {
var x = eater.x - check.x;
var y = eater.y - check.y;
var distance = Math.hypot(x, y);
var maxDistance = Blob.getSize(eater.mass + check.mass);
var minMass = CollisionHandler.eatFactor * check.mass + check.mass;
if(distance < maxDistance && eater.mass > minMass) {
return true;
} else {
return false;
}
};
CollisionHandler.pushApart = function(cell, check) {
var x = check.x - cell.x;
var y = check.y - cell.y;
var distance = Math.hypot(x, y);
var cSz = Blob.getSize(cell.mass);
var chSz = Blob.getSize(check.mass);
var maxDist = cSz + chSz;
var boosting = Math.abs(cell.move.x) > 2 ||
Math.abs(cell.move.y) > 2 ||
Math.abs(check.move.x) > 2 ||
Math.abs(check.move.y) > 2;
if(distance < maxDist && !boosting) {
x /= distance;
y /= distance;
var pLen = cSz / (cSz + chSz);
var cx = cell.x + pLen * x * distance;
var cy = cell.y + pLen * y * distance;
cell.x += (cx - x * chSz - cell.x) * animationConstant;
cell.y += (cy - y * chSz - cell.y) * animationConstant;
check.x += (cx + x * cSz - check.x) * animationConstant;
check.y += (cy + y * cSz - check.y) * animationConstant;
/*var targetX = check.x - maxDistance * x;
var targetY = check.y - maxDistance * y;
cell.x += (targetX - cell.x) * animationConstant;
cell.y += (targetY - cell.y) * animationConstant;*/
}
};
function addFood(n) {
for(var i = 0; i < n ; i++) {
var blob = new Blob(
getRandom(gameWidth),
getRandom(gameHeight),
massFood,
getRandom(360)
);
blobsFood.push(blob);
};
console.log(blobsFood.length);
};
var massVirus = 80;
var massVirusMax = 120;
function addVirus(n) {
for(var i = 0; i < n ; i++) {
var blob = new Virus();
blobsVirus.push(blob);
};
console.log(blobsVirus.length);
};
var massEject = 10;
function addEject(blob) {
if(blob.mass < 20)
return;
blob.mass -= massEject;
var angle = getMouseAngle(blob.x, blob.y);
var r = Blob.getSize(blob.mass);
var blob = new Blob(
blob.x + Math.cos(angle) * r,
blob.y + Math.sin(angle) * r,
massEject,
blob.hue
);
blob.move.x = Math.cos(angle) * 30;
blob.move.y = Math.sin(angle) * 30;
blob.move.angle = angle;
blobsEject.push(blob);
};
function addPlayer(x, y, mass, angle, hue) {
var moveX = Math.cos(angle) * Blob.getBoostSpeed(mass);
var moveY = Math.sin(angle) * Blob.getBoostSpeed(mass);
var blob = new Blob(x, y, mass, hue);
blob.move.x = moveX;
blob.move.y = moveY;
blob.move.angle = angle;
blobsPlayer.push(blob);
};
var mouseX = 0;
var mouseY = 0;
var cameraX = 0;
var cameraY = 0;
var _cameraX = 0;
var _cameraY = 0;
var zoom = 1;
var _zoom = 1;
function updateCamera() {
var x = 0;
var y = 0;
var m = 0;
var len = blobsPlayer.length;
blobsPlayer.forEach(function(blob) {
x += blob.x;
y += blob.y;
m += Blob.getSize(blob.mass);
});
cameraX = x/len;
cameraY = y/len;
zoom = 1/(Math.sqrt(m)/Math.log(m));
};
document.onmousemove = function(evt) {
mouseX = evt.clientX;
mouseY = evt.clientY;
};
document.onkeydown = function(evt) {
var key = evt.keyCode;
if(key == 32) {
var len = blobsPlayer.length;
for(var i = 0; i < len; i++) {
var blob = blobsPlayer[i];
splitPlayerBlob(blob);
};
}
if(key == 87) {
var len = blobsPlayer.length;
for(var i = 0; i < len; i++) {
var blob = blobsPlayer[i];
addEject(blob);
};
}
};
function getMouseAngle(x, y) {
var x = mouseX - width/2 + (cameraX - x) * zoom;
var y = mouseY - height/2 + (cameraY - y) * zoom;
return Math.atan2(y, x);
};
function splitPlayerBlob(blob) {
var numSplits = 16 - blobsPlayer.length;
if(numSplits <= 0) return;
if(blob.mass >= 20) {
blob.mass /= 2;
var angle = getMouseAngle(blob.x, blob.y);
addPlayer(blob.x, blob.y, blob.mass, angle, blob.hue);
}
};
function mouseMovePlayer() {
blobsPlayer.forEach(function(blob) {
var angle = getMouseAngle(blob.x, blob.y);
var speed = Blob.getSpeed(blob.mass);
var x = mouseX - width/2 + (cameraX - blob.x) * zoom;
var y = mouseY - height/2 + (cameraY - blob.y) * zoom;
// blob.x += Math.cos(angle) * speed * Math.min(1, Math.pow(x/toRadius(blob.mass), 2));
// blob.y += Math.sin(angle) * speed * Math.min(1, Math.pow(y/toRadius(blob.mass), 2));
blob.x += Math.cos(angle) * speed;
blob.y += Math.sin(angle) * speed;
});
};
function movePlayer() {
blobsPlayer.forEach(function(blob) {
blob.boostMove();
});
};
function moveEject() {
blobsEject.forEach(function(blob) {
blob.boostMove();
});
};
function separateEject() {
blobsEject.forEach(function(a, i) {
blobsEject.forEach(function(b, j) {
if(i == j)
return;
CollisionHandler.pushApart(a, b);
});
});
};
function separatePlayer() {
blobsPlayer.forEach(function(a, i) {
blobsPlayer.forEach(function(b, j) {
if(i == j)
return;
var ref1 = a;
var ref2 = b;
CollisionHandler.pushApart(ref1, ref2);
});
});
};
function eatFood() {
blobsPlayer.forEach(function(player) {
blobsFood.forEach(function(food, i) {
if(CollisionHandler.canEat(player, food)) {
player.onEat(food);
blobsFood.splice(i, 1);
}
});
});
};
function eatEject() {
blobsPlayer.forEach(function(player) {
blobsEject.forEach(function(eject, i) {
if(CollisionHandler.canEat(player, eject)) {
player.onEat(eject);
blobsEject.splice(i, 1);
}
});
});
blobsVirus.forEach(function(virus) {
blobsEject.forEach(function(eject, i) {
if(CollisionHandler.canEat(virus, eject)) {
virus.onEat(eject);
blobsEject.splice(i, 1);
}
});
});
};
function shootVirus(virus, angle) {
virus.mass = massVirus;
var speed = Blob.getSpeed(massVirus);
var blob = new Blob(virus.x, virus.y, massVirus, virus.hue);
var x = Math.cos(angle) * 30;
var y = Math.sin(angle) * 30;
blob.move.x = x;
blob.move.y = y;
blob.move.angle = angle;
blobsVirus.push(blob);
};
function moveVirus() {
blobsVirus.forEach(function(blob) {
blob.x += blob.move.x;
blob.y += blob.move.y;
blob.move.x *= 0.95;
blob.move.y *= 0.95;
});
};
function separateVirus() {
blobsVirus.forEach(function(a, i) {
blobsVirus.forEach(function(b, j) {
if(i == j)
return;
CollisionHandler.pushApart(a, b);
});
});
};
function eatVirus() {
blobsPlayer.forEach(function(player) {
blobsVirus.forEach(function(virus, i) {
if(CollisionHandler.canEat(player, virus)) {
player.onEat(virus);
virus.onEaten(player);
blobsVirus.splice(i, 1);
}
});
});
};
var baseTime = 10000;
function canCombine(player) {
var t = Math.floor(baseTime + (0.02 * player.mass));
var now = Date.now() - player.time;
return t < now;
};
function combinePlayer() {
blobsPlayer.forEach(function(a, i) {
blobsPlayer.forEach(function(b, j) {
if(i == j)
return;
var x = a.x - b.x;
var y = a.y - b.y;
var d = Math.hypot(x, y);
var r = Blob.getSize(a.mass + b.mass);
if(d < r && canCombine(a) && canCombine(b)) {
if(a.mass > b.mass) {
a.mass += b.mass;
blobsPlayer.splice(j, 1);
} else {
b.mass += a.mass;
blobsPlayer.splice(i, 1);
}
}
});
});
};
function updateGame() {
movePlayer();
mouseMovePlayer();
separatePlayer();
combinePlayer();
moveEject();
separateEject();
moveVirus();
separateVirus();
eatFood();
eatEject();
eatVirus();
updateCamera();
};
function drawGame() {
clearCanvas();
ctx.save();
_zoom += (zoom - _zoom) * 0.1;
_cameraX += (cameraX - _cameraX) * 0.1;
_cameraY += (cameraY - _cameraY) * 0.1;
ctx.translate(-_cameraX * _zoom + width/2, -_cameraY * _zoom + height/2);
ctx.scale(_zoom, _zoom);
drawAllBlobs();
ctx.restore();
};
function clearCanvas() {
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
};
function drawAllBlobs() {
var blobs = blobsFood
.concat(blobsEject)
.concat(blobsVirus)
.concat(blobsPlayer);
var blobs = blobs.sort(function(a, b) {
return a.mass - b.mass;
});
blobs.forEach(function(blob) {
blob.draw();
});
};
function loop() {
updateGame();
drawGame();
requestAnimationFrame(loop);
};
addFood(300);
addVirus(15);
addPlayer(0, 0, 3000, 0, 45)
loop();
</script>
</body>
</html>
I have written some JS code to plot a 3D wireframe sphere into a HTML5 canvas.
I started from this post and improved it by using Qt3D vertices generation for a sphere mesh. The JS code does 2 passes on the vertices: the first to display rings, and the second to display slices. Normally OpenGL would connect all the vertices automatically with triangles.
I kept the slices/rings configurable but I have issues with the transformation code, for example when I rotate the sphere along the X axis.
So, starting from the basics. Here's a 1-pass, 4 rings, 4 slices, no transformation:
Seems all good. Now 2-passes, 10 rings, 10 slices, no transformation:
Still good, but if I rotate it 30° on the X Axis, the top and bottom vertices (Y position only apparently) get messed up.
I suspect there is something wrong in the rotation functions, or in the projection function.
Can someone please help me to figure out what's going on here ?
(Note that I don't want to use Three.js cause my goal is to port this in a QML application)
Here's the full code.
var sphere = new Sphere3D();
var rotation = new Point3D();
var distance = 1000;
var lastX = -1;
var lastY = -1;
function Point3D() {
this.x = 0;
this.y = 0;
this.z = 0;
}
function Sphere3D(radius) {
this.vertices = new Array();
this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
this.rings = 10;
this.slices = 10;
this.numberOfVertices = 0;
var M_PI_2 = Math.PI / 2;
var dTheta = (Math.PI * 2) / this.slices;
var dPhi = Math.PI / this.rings;
// Iterate over latitudes (rings)
for (var lat = 0; lat < this.rings + 1; ++lat) {
var phi = M_PI_2 - lat * dPhi;
var cosPhi = Math.cos(phi);
var sinPhi = Math.sin(phi);
// Iterate over longitudes (slices)
for (var lon = 0; lon < this.slices + 1; ++lon) {
var theta = lon * dTheta;
var cosTheta = Math.cos(theta);
var sinTheta = Math.sin(theta);
p = this.vertices[this.numberOfVertices] = new Point3D();
p.x = this.radius * cosTheta * cosPhi;
p.y = this.radius * sinPhi;
p.z = this.radius * sinTheta * cosPhi;
this.numberOfVertices++;
}
}
}
function rotateX(point, radians) {
var y = point.y;
point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateY(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateZ(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}
function projection(xy, z, xyOffset, zOffset, distance) {
return ((distance * xy) / (z - zOffset)) + xyOffset;
}
function strokeSegment(index, ctx, width, height) {
var x, y;
var p = sphere.vertices[index];
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
x = projection(p.x, p.z, width / 2.0, 100.0, distance);
y = projection(p.y, p.z, height / 2.0, 100.0, distance);
if (lastX == -1 && lastY == -1) {
lastX = x;
lastY = y;
return;
}
if (x >= 0 && x < width && y >= 0 && y < height) {
if (p.z < 0) {
ctx.strokeStyle = "gray";
} else {
ctx.strokeStyle = "white";
}
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
lastX = x;
lastY = y;
}
}
function render() {
var canvas = document.getElementById("sphere3d");
var width = canvas.getAttribute("width");
var height = canvas.getAttribute("height");
var ctx = canvas.getContext('2d');
var p = new Point3D();
ctx.fillStyle = "black";
ctx.clearRect(0, 0, width, height);
ctx.fillRect(0, 0, width, height);
// draw each vertex to get the first sphere skeleton
for (i = 0; i < sphere.numberOfVertices; i++) {
strokeSegment(i, ctx, width, height);
}
// now walk through rings to draw the slices
for (i = 0; i < sphere.slices + 1; i++) {
for (var j = 0; j < sphere.rings + 1; j++) {
strokeSegment(i + (j * (sphere.slices + 1)), ctx, width, height);
}
}
}
function init() {
rotation.x = Math.PI / 6;
render();
}
canvas {
background: black;
display: block;
}
<body onLoad="init();">
<canvas id="sphere3d" width="500" height="500">
Your browser does not support HTML5 canvas.
</canvas>
</body>
Your problem is that the contents of your sphere.vertices[] array is being modified inside your strokeSegment() call so the rotation gets applied twice when you invoke it the second time on each point. So, in strokeSegment() you need to replace:
var p = sphere.vertices[index];
with:
var p = new Point3D();
p.x = sphere.vertices[index].x;
p.y = sphere.vertices[index].y;
p.z = sphere.vertices[index].z;
Then it works perfectly as shown below:
var sphere = new Sphere3D();
var rotation = new Point3D();
var distance = 1000;
var lastX = -1;
var lastY = -1;
function Point3D() {
this.x = 0;
this.y = 0;
this.z = 0;
}
function Sphere3D(radius) {
this.vertices = new Array();
this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
this.rings = 10;
this.slices = 10;
this.numberOfVertices = 0;
var M_PI_2 = Math.PI / 2;
var dTheta = (Math.PI * 2) / this.slices;
var dPhi = Math.PI / this.rings;
// Iterate over latitudes (rings)
for (var lat = 0; lat < this.rings + 1; ++lat) {
var phi = M_PI_2 - lat * dPhi;
var cosPhi = Math.cos(phi);
var sinPhi = Math.sin(phi);
// Iterate over longitudes (slices)
for (var lon = 0; lon < this.slices + 1; ++lon) {
var theta = lon * dTheta;
var cosTheta = Math.cos(theta);
var sinTheta = Math.sin(theta);
p = this.vertices[this.numberOfVertices] = new Point3D();
p.x = this.radius * cosTheta * cosPhi;
p.y = this.radius * sinPhi;
p.z = this.radius * sinTheta * cosPhi;
this.numberOfVertices++;
}
}
}
function rotateX(point, radians) {
var y = point.y;
point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateY(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateZ(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}
function projection(xy, z, xyOffset, zOffset, distance) {
return ((distance * xy) / (z - zOffset)) + xyOffset;
}
function strokeSegment(index, ctx, width, height) {
var x, y;
var p = new Point3D();
p.x = sphere.vertices[index].x;
p.y = sphere.vertices[index].y;
p.z = sphere.vertices[index].z;
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
x = projection(p.x, p.z, width / 2.0, 100.0, distance);
y = projection(p.y, p.z, height / 2.0, 100.0, distance);
if (lastX == -1 && lastY == -1) {
lastX = x;
lastY = y;
return;
}
if (x >= 0 && x < width && y >= 0 && y < height) {
if (p.z < 0) {
ctx.strokeStyle = "gray";
} else {
ctx.strokeStyle = "white";
}
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
lastX = x;
lastY = y;
}
}
function render() {
var canvas = document.getElementById("sphere3d");
var width = canvas.getAttribute("width");
var height = canvas.getAttribute("height");
var ctx = canvas.getContext('2d');
var p = new Point3D();
ctx.fillStyle = "black";
ctx.clearRect(0, 0, width, height);
ctx.fillRect(0, 0, width, height);
// draw each vertex to get the first sphere skeleton
for (i = 0; i < sphere.numberOfVertices; i++) {
strokeSegment(i, ctx, width, height);
}
// now walk through rings to draw the slices
for (i = 0; i < sphere.slices + 1; i++) {
for (var j = 0; j < sphere.rings + 1; j++) {
strokeSegment(i + (j * (sphere.slices + 1)), ctx, width, height);
}
}
}
function init() {
rotation.x = Math.PI / 3;
render();
}
canvas {
background: black;
display: block;
}
<body onLoad="init();">
<canvas id="sphere3d" width="500" height="500">
Your browser does not support HTML5 canvas.
</canvas>
</body>
Short answer
The bug is in strokeSegment function
function strokeSegment(index, ctx, width, height) {
var x, y;
var p = sphere.vertices[index];
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
...
The bug is that all the rotate functions modify p inplace and thus the modify the value stored in sphere.vertices! So the way to fix it is simply to clone the point:
function strokeSegment(index, ctx, width, height) {
var x, y;
var p0 = sphere.vertices[index];
var p = new Point3D();
p.x = p0.x;
p.y = p0.y;
p.z = p0.z;
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
...
You may find demo with fixed code at https://plnkr.co/edit/zs5ZxbglFxo9cbwA6MI5?p=preview
Longer addition
Before I found this issue I played with your code a bit and I think improved it. Improved version is available at https://plnkr.co/edit/tpTZ8GH9eByVARUIYZBi?p=preview
var sphere = new Sphere3D();
var rotation = new Point3D(0, 0, 0);
var distance = 1000;
var EMPTY_VALUE = Number.MIN_VALUE;
function Point3D(x, y, z) {
if (arguments.length == 3) {
this.x = x;
this.y = y;
this.z = z;
}
else if (arguments.length == 1) {
fillPointFromPoint(this, x); // 1 argument means point
}
else {
clearPoint(this); // no arguments mean creat empty
}
}
function fillPointFromPoint(target, src) {
target.x = src.x;
target.y = src.y;
target.z = src.z;
}
function clearPoint(p) {
p.x = EMPTY_VALUE;
p.y = EMPTY_VALUE;
p.z = EMPTY_VALUE;
}
function Sphere3D(radius) {
this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
this.innerRingsCount = 9; // better be odd so we have explicit Equator
this.slicesCount = 8;
var M_PI_2 = Math.PI / 2;
var dTheta = (Math.PI * 2) / this.slicesCount;
var dPhi = Math.PI / this.innerRingsCount;
this.rings = [];
// always add both poles
this.rings.push([new Point3D(0, this.radius, 0)]);
// Iterate over latitudes (rings)
for (var lat = 0; lat < this.innerRingsCount; ++lat) {
var phi = M_PI_2 - lat * dPhi - dPhi / 2;
var cosPhi = Math.cos(phi);
var sinPhi = Math.sin(phi);
console.log("lat = " + lat + " phi = " + (phi / Math.PI) + " sinPhi = " + sinPhi);
var vertices = [];
// Iterate over longitudes (slices)
for (var lon = 0; lon < this.slicesCount; ++lon) {
var theta = lon * dTheta;
var cosTheta = Math.cos(theta);
var sinTheta = Math.sin(theta);
var p = new Point3D();
p.x = this.radius * cosTheta * cosPhi;
p.y = this.radius * sinPhi;
p.z = this.radius * sinTheta * cosPhi;
vertices.push(p);
}
this.rings.push(vertices);
}
// always add both poles
this.rings.push([new Point3D(0, -this.radius, 0)]);
}
function rotateX(point, radians) {
var y = point.y;
point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateY(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateZ(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}
function projection(xy, z, xyOffset, zOffset, distance) {
return ((distance * xy) / (z - zOffset)) + xyOffset;
}
var lastP = new Point3D();
var firstP = new Point3D();
function startRenderingPortion() {
clearPoint(lastP);
clearPoint(firstP);
}
function closeRenderingPortion(ctx, width, height) {
strokeSegmentImpl(ctx, firstP.x, firstP.y, firstP.z, width, height);
clearPoint(lastP);
clearPoint(firstP);
}
function strokeSegmentImpl(ctx, x, y, z, width, height) {
if (x >= 0 && x < width && y >= 0 && y < height) {
// as we work with floating point numbers, there might near zero that != 0
// choose gray if one of two points is definitely (z < 0) and other has (z <= 0)
// Note also that in term of visibility this is a wrong logic! Line is invisible
// only if it is shadowed by another polygon and this depends on relative "Z" not
// absolute values
var eps = 0.01;
if (((z < -eps) && (lastP.z < eps))
|| ((z < eps) && (lastP.z < -eps))) {
ctx.strokeStyle = "gray";
} else {
ctx.strokeStyle = "white";
}
if ((x === lastP.x) && (y == lastP.y)) {
ctx.beginPath();
// draw single point
ctx.moveTo(x, y);
ctx.lineTo(x + 1, y + 1);
ctx.stroke();
ctx.closePath();
} else {
ctx.beginPath();
ctx.moveTo(lastP.x, lastP.y);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
}
lastP.x = x;
lastP.y = y;
lastP.z = z;
}
}
function strokeSegment(p0, ctx, width, height) {
var p = new Point3D(p0); // clone original point to not mess it up with rotation!
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
var x, y;
x = projection(p.x, p.z, width / 2.0, 100.0, distance);
y = projection(p.y, p.z, height / 2.0, 100.0, distance);
if (lastP.x === EMPTY_VALUE && lastP.y === EMPTY_VALUE) {
lastP = new Point3D(x, y, p.z);
fillPointFromPoint(firstP, lastP);
return;
}
strokeSegmentImpl(ctx, x, y, p.z, width, height);
}
function renderSphere(ctx, width, height, sphere) {
var i, j;
var vertices;
// draw each vertex to get the first sphere skeleton
for (i = 0; i < sphere.rings.length; i++) {
startRenderingPortion();
vertices = sphere.rings[i];
for (j = 0; j < vertices.length; j++) {
strokeSegment(vertices[j], ctx, width, height);
}
closeRenderingPortion(ctx, width, height);
}
// now walk through rings to draw the slices
for (i = 0; i < sphere.slicesCount; i++) {
startRenderingPortion();
for (j = 0; j < sphere.rings.length; j++) {
vertices = sphere.rings[j];
var p = vertices[i % vertices.length];// for top and bottom vertices.length = 1
strokeSegment(p, ctx, width, height);
}
//closeRenderingPortion(ctx, width, height); // don't close back!
}
}
function render() {
var canvas = document.getElementById("sphere3d");
var width = canvas.getAttribute("width");
var height = canvas.getAttribute("height");
var ctx = canvas.getContext('2d');
ctx.fillStyle = "black";
ctx.clearRect(0, 0, width, height);
ctx.fillRect(0, 0, width, height);
renderSphere(ctx, width, height, sphere);
}
function init() {
rotation.x = Math.PI / 6;
//rotation.y = Math.PI / 6;
rotation.z = Math.PI / 6;
render();
}
Main changes are:
I explicitly separated plain array vertices in array of arrays rings and also explicitly add both poles to it.
Separation of rings allowed me to clear lastX/Y more often to avoid some spurious lines by introducing startRenderingPortion.
I also introduced closeRenderingPortion that is logically similar to closePath. Using this method I was able to remove duplication of points that you needed.
Generally I tried to avoid more-OOP-ish style as you do in your code (see renderSphere or clearPoint) but I changed Point3D constructor to support 3 modes: (x,y,z), point, empty.
Use more explicit marker value for empty lastX/Y var EMPTY_VALUE = Number.MIN_VALUE;. -1 is a possible value
Note also that there is a potential bug with your gray/white color selection that I didn't fix. I assume your color should reflect "invisible" lines and simple logic of Z > 0 vs Z < 0 doesn't solve this issue properly. Actually single line might be visible only partially if it is obscured by other things in the scene.
I need to detect collision circle with any line. I have array with verticles of polygon (x, y) and draw this polygon in loop. For detection I use algorithm, which calculate triangle height. Then I check if this height < 0, then circle collided with line.
The picture, that describe this method:
But I have unexpected result. My circle collide with transparent line (what?). I can't explain how it happens.
Demo at jsfiddle: https://jsfiddle.net/f458rdz6/1/
Function, which check the collisions and response it:
var p = polygonPoints;
for (var i = 0, n = p.length; i < n; i++) {
var start = i;
var end = (i + 1) % n;
var x0 = p[start].x;
var y0 = p[start].y;
var x1 = p[end].x;
var y1 = p[end].y;
// detection collision
var dx = x1 - x0;
var dy = y1 - y0;
var len = Math.sqrt(dx * dx + dy * dy);
var dist = (dx * (this.y - y0) - dy * (this.x - x0)) / len;
if (dist < this.radius) {
continue;
}
// calculate reflection, because collided
var wallAngle = Math.atan2(dy, dx);
var wallNormalX = Math.sin(wallAngle);
var wallNormalY = -Math.cos(wallAngle);
var d = 2 * (this.velocityX * wallNormalX + this.velocityY * wallNormalY);
this.x -= d * wallNormalX;
this.y -= d * wallNormalY;
}
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var polygonPoints = [
{
x: 240,
y: 130
},
{
x: 140,
y: 100
},
{
x: 180,
y: 250
},
{
x: 320,
y: 280
},
{
x: 400,
y: 50
}
];
var game = {
ball: new Ball()
};
function Ball() {
this.x = canvas.width / 2;
this.y = canvas.height - 100;
this.oldX = this.x - 1;
this.oldY = this.y + 1;
this.velocityX = 0;
this.velocityY = 0;
this.radius = 8;
};
Ball.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
};
Ball.prototype.update = function() {
var x = this.x;
var y = this.y;
this.velocityX = this.x - this.oldX;
this.velocityY = this.y - this.oldY;
this.x += this.velocityX;
this.y += this.velocityY;
this.oldX = x;
this.oldY = y;
};
Ball.prototype.collision = function() {
var p = polygonPoints;
for (var i = 0, n = p.length; i < n; i++) {
var start = i;
var end = (i + 1) % n;
var x0 = p[start].x;
var y0 = p[start].y;
var x1 = p[end].x;
var y1 = p[end].y;
// detection collision
var dx = x1 - x0;
var dy = y1 - y0;
var len = Math.sqrt(dx * dx + dy * dy);
var dist = (dx * (this.y - y0) - dy * (this.x - x0)) / len;
if (dist < this.radius) {
continue;
}
// calculate reflection, because collided
var wallAngle = Math.atan2(dy, dx);
var wallNormalX = Math.sin(wallAngle);
var wallNormalY = -Math.cos(wallAngle);
var d = 2 * (this.velocityX * wallNormalX + this.velocityY * wallNormalY);
this.x -= d * wallNormalX;
this.y -= d * wallNormalY;
}
};
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI*2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPolygon() {
ctx.beginPath();
ctx.strokeStyle = '#333';
ctx.moveTo(polygonPoints[0].x, polygonPoints[0].y);
for (var i = 1, n = polygonPoints.length; i < n; i++) {
ctx.lineTo(polygonPoints[i].x, polygonPoints[i].y);
}
ctx.lineTo(polygonPoints[0].x, polygonPoints[0].y);
ctx.stroke();
ctx.closePath();
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawPolygon();
game.ball.draw();
game.ball.update();
game.ball.collision();
window.requestAnimationFrame(render);
}
render();
canvas {
border: 1px solid #333;
}
<canvas id="myCanvas" width="480" height="320"></canvas>
What the problem? Maybe I need use other method for detect collision? I tried to use this one, but if my circle has high speed this method not working.
Thank you.
Circle line segment intercept
UPDATE
This answer includes line line intercept, moving a line along its normal, distance point (circle) to line, and circle line intercept.
The circle is
var circle = {
radius : 500,
center : point(1000,1000),
}
The line segment is
var line = {
p1 : point(500,500),
p2 : point(2000,1000),
}
A point is
var point = {
x : 100,
y : 100,
}
Thus the function to find the intercept of a line segment width a circle
The function returns an array of up to two point on the line segment. If no points found returns an empty array.
function inteceptCircleLineSeg(circle, line){
var a, b, c, d, u1, u2, ret, retP1, retP2, v1, v2;
v1 = {};
v2 = {};
v1.x = line.p2.x - line.p1.x;
v1.y = line.p2.y - line.p1.y;
v2.x = line.p1.x - circle.center.x;
v2.y = line.p1.y - circle.center.y;
b = (v1.x * v2.x + v1.y * v2.y);
c = 2 * (v1.x * v1.x + v1.y * v1.y);
b *= -2;
d = Math.sqrt(b * b - 2 * c * (v2.x * v2.x + v2.y * v2.y - circle.radius * circle.radius));
if(isNaN(d)){ // no intercept
return [];
}
u1 = (b - d) / c; // these represent the unit distance of point one and two on the line
u2 = (b + d) / c;
retP1 = {}; // return points
retP2 = {}
ret = []; // return array
if(u1 <= 1 && u1 >= 0){ // add point if on the line segment
retP1.x = line.p1.x + v1.x * u1;
retP1.y = line.p1.y + v1.y * u1;
ret[0] = retP1;
}
if(u2 <= 1 && u2 >= 0){ // second add point if on the line segment
retP2.x = line.p1.x + v1.x * u2;
retP2.y = line.p1.y + v1.y * u2;
ret[ret.length] = retP2;
}
return ret;
}
UPDATE
Line line intercept.
Returns a point if found else returns undefined.
function interceptLines(line,line1){
var v1, v2, c, u;
v1 = {};
v2 = {};
v3 = {};
v1.x = line.p2.x - line.p1.x; // vector of line
v1.y = line.p2.y - line.p1.y;
v2.x = line1.p2.x - line1.p1.x; //vector of line2
v2.y = line1.p2.y - line1.p1.y;
var c = v1.x * v2.y - v1.y * v2.x; // cross of the two vectors
if(c !== 0){
v3.x = line.p1.x - line1.p1.x;
v3.y = line.p1.y - line1.p1.y;
u = (v2.x * v3.y - v2.y * v3.x) / c; // unit distance of intercept point on this line
return {x : line.p1.x + v1.x * u, y : line.p1.y + v1.y * u};
}
return undefined;
}
Lift Line
Move line along its normal
function liftLine(line,dist){
var v1,l
v1 = {};
v1.x = line.p2.x - line.p1.x; // convert line to vector
v1.y = line.p2.y - line.p1.y;
l = Math.sqrt(v1.x * v1.x + v1.y * v1.y); // get length;
v1.x /= l; // Assuming you never pass zero length lines
v1.y /= l;
v1.x *= dist; // set the length
v1.y *= dist;
// move the line along its normal the required distance
line.p1.x -= v1.y;
line.p1.y += v1.x;
line.p2.x -= v1.y;
line.p2.y += v1.x;
return line; // if needed
}
Distance circle (or point) to a line segment
Returns the closest distance to the line segment. It is just the circle center that I am using. So you can replace circle with a point
function circleDistFromLineSeg(circle,line){
var v1, v2, v3, u;
v1 = {};
v2 = {};
v3 = {};
v1.x = line.p2.x - line.p1.x;
v1.y = line.p2.y - line.p1.y;
v2.x = circle.center.x - line.p1.x;
v2.y = circle.center.y - line.p1.y;
u = (v2.x * v1.x + v2.y * v1.y) / (v1.y * v1.y + v1.x * v1.x); // unit dist of point on line
if(u >= 0 && u <= 1){
v3.x = (v1.x * u + line.p1.x) - circle.center.x;
v3.y = (v1.y * u + line.p1.y) - circle.center.y;
v3.x *= v3.x;
v3.y *= v3.y;
return Math.sqrt(v3.y + v3.x); // return distance from line
}
// get distance from end points
v3.x = circle.center.x - line.p2.x;
v3.y = circle.center.y - line.p2.y;
v3.x *= v3.x; // square vectors
v3.y *= v3.y;
v2.x *= v2.x;
v2.y *= v2.y;
return Math.min(Math.sqrt(v2.y + v2.x), Math.sqrt(v3.y + v3.x)); // return smaller of two distances as the result
}