Related
I'm trying to make a simple interactive game where there are circles of different colours moving in the canvas and when the user clicks on the blue circles, it logs the number of clicks on the screen. When clicking circles with any other colour, the animation stops.
I'm very new to javascript but this is what I have for now. I've made a function with random coloured circles and a function with blue circles moving but I'm totally stuck on how to stop the animation when clicking on the function with a random coloured circles and logging the amount of clicks on the blue circles. If someone could help me move forward with it in any way (doesn't have to be the full thing), that would be awesome, thanks.
JS
var canvas;
var ctx;
var w = 1000;
var h = 600;
var colours = ["red", "blue"];
var allCircles = [];
for(var i=0; i<1; i++){
setTimeout(function(){console.log(i)},1000);
}
document.querySelector("#myCanvas").onclick = click;
createData(2);
createDataTwo(20);
setUpCanvas();
animationLoop();
function animationLoop(){
clear();
for(var i = 0; i<allCircles.length; i++){
circle(allCircles[i]);
forward(allCircles[i], 5)
turn(allCircles[i], randn(30));
collisionTestArray(allCircles[i],allCircles)
bounce(allCircles[i]);
}
requestAnimationFrame(animationLoop);
}
function collisionTestArray(o, a){
for(var i=0; i<a.length; i++){
if(o !=a[i]){
collision(o,a[i]);
}
}
}
function collision(o1,o2){
if(o1 && o2){
var differencex = Math.abs(o1.x-o2.x);
var differencey = Math.abs(o1.y-o2.y);
var hdif = Math.sqrt(differencex*differencex+differencey*differencey);
if(hdif<o1.r+o2.r){
if(differencex < differencey){
turn(o1, 180-2*o1.angle);
turn(o2, 180-2*o2.angle);
}else{
turn(o1, 360-2*o1.angle);
turn(o2, 360-2*o2.angle);
}
turn(o1, 180);
turn(o2, 180);
console.log("collision");
};
}
}
function click(event){
clear()
}
function bounce (o){
if(o.x > w || o.x < 0){
turn(o, 180-2*o.angle);
};
if(o.y > h || o.y < 0){
turn(o, 360-2*o.angle);
}
}
function clear(){
ctx.clearRect(0,0,w,h);
}
function stop (){
o1.changex = 0;
o1.changey = 0;
o2.changex = 0;
o2.changey = 0;
}
function circle (o){
var x = o.x;
var y = o.y;
var a = o.angle;
var d = o.d;
ctx.beginPath();
ctx.arc(o.x,o.y,o.r,0,2*Math.PI);
ctx.fillStyle = "hsla("+o.c+",100%,50%, "+o.a+")";
ctx.fill();
o.x = x;
o.y = y;
o.angle = a;
o.d = d;
}
function createData(num){
for(var i=0; i<num; i++){
allCircles.push({
"x": rand(w),
"changex": rand(10),
"y":rand(h),
"changex": rand(10),
"w": randn(w),
"h": randn(h),
"d": 1,
"a": 1,
"angle": 0,
"changle":15,
"c":216,
"r": 50
}
)
}
}
function createDataTwo(num){
for(var i=0; i<num; i++){
allCircles.push({
"x": rand(w),
"changex": rand(10),
"y":rand(h),
"changex": rand(10),
"w": randn(w),
"h": randn(h),
"d": 1,
"a": 1,
"angle": 0,
"changle":15,
"c":rand(90),
"r": 50
}
)
}
}
function turn(o,angle){
if(angle != undefined){
o.changle=angle;
};
o.angle+=o.changle;
}
function forward(o,d){
var changeX;
var changeY;
var oneDegree = Math.PI/180;
if(d != undefined){
o.d = d;
};
changeX= o.d*Math.cos(o.angle*oneDegree);
changeY = o.d*Math.sin(o.angle*oneDegree);
o.x+=changeX;
o.y+=changeY;
}
function randn(r){
var result = Math.random()*r - r/2
return result
}
function randi(r) {
var result = Math.floor(Math.random()*r);
return result
}
function rand(r){
return Math.random()*r
}
function setUpCanvas(){
canvas = document.querySelector("#myCanvas");
ctx = canvas.getContext("2d");
canvas.width = w;
canvas.height = h;
canvas.style.border = "5px solid orange"
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, w, h);
}
console.log("assi4")
HTML
<html>
<head>
<link rel="stylesheet" type="text/css" href="../modules.css">
</head>
<body>
<div id="container">
<h1>Click the Blue Circles Only</h1>
<canvas id = "myCanvas"></canvas>
</div>
<script src="assi5.js"></script>
</body>
</html>
CSS
#container {
margin: auto;
width: 75%;
text-align: center;
}
You can use cancelAnimationFrame to stop the animation when a non-blue circle is clicked
You need to pass it a reference to the frame ID returned from requestAnimationFrame for it to work.
In order to tell if a circle was clicked, you need to check the coordinates of each circle against the coordinates of the click.
I have an example below if you had your blue circles in an array "blue", and other circles in array "other", the ID returned by requestAnimationFrame as "frame".
The check function returns the number of blue circles hit (the points scored) and if any other circles were hit, it stops the animation.
getCoords returns the coordinates of the click on the canvas from the click event.
canvas.addEventListener('click', event=>{
points += check(getCoords(event), blue, other, frame);
document.getElementById('points').textContent = points;
})
function check({x, y}, blue, other, frame) {
other.filter(circle=>circle.isWithin(x, y))
.length && cancelAnimationFrame(frame); // This is where animation stops
return blue.filter(circle=>circle.isWithin(x, y)).length;
}
function getCoords(event) {
const canvas = event.target;
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return { x, y };
}
I have an example that works below where I changed the circles to the result of a function rather than an inline object, and moved the functions you use on them into their own class. You don't have to do this, but I find it a lot easier to understand.
function main() {
const canvas = document.getElementById('canvas');
const context = canvas.getContext("2d");
const clear = () => context.clearRect(0, 0, canvas.width, canvas.height);
const blue = new Array(2).fill().map(() => new Circle(context, 216));
const other = new Array(10).fill().map(() => new Circle(context));
let circles = [...blue, ...other];
let frame = 0;
let points = 0;
// Move the circle a bit and check if it needs to bounce
function update(circle) {
circle.forward(1)
circle.turn(30, true)
circle.collisionTestArray(circles)
circle.bounce();
}
// Main game loop, clear canvas, update circle positions, draw circles
function loop() {
clear();
circles.filter(circle => circle.free).forEach(update);
circles.forEach(circle => circle.draw());
frame = requestAnimationFrame(loop);
}
loop();
canvas.addEventListener('click', event => {
points += check(getCoords(event), blue, other, frame, circles);
document.getElementById('points').textContent = points;
})
}
function check({ x, y }, blue, other, frame) {
other.filter(circle => circle.isWithin(x, y))
// .map(circle=>circle.toggle())
.length && cancelAnimationFrame(frame); // This is where animation stops
return blue.filter(circle => circle.isWithin(x, y)).length;
}
function getCoords(event) {
const canvas = event.target;
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return { x, y };
}
main();
function Circle(context, c) {
const randn = r => rand(r) - r / 2;
const randi = r => Math.floor(randi(r));
const rand = r => Math.random() * r;
// These are for easily stopping and starting a circle;
this.free = true;
this.stop = () => this.free = false;
this.release = () => this.free = true;
this.toggle = () => this.free = !this.free;
const {
width,
height
} = context.canvas;
// These are the same properties you were using in your code
this.x = rand(width);
this.changex = rand(10);
this.y = rand(height);
this.changey = rand(10);
this.w = randn(width);
this.h = randn(height);
this.d = 1;
this.a = 1;
this.angle = 0;
this.changle = 15;
this.c = c || rand(90); // This is the only difference between blue and other circles
this.r = 50;
// These next functions you had in your code, I just moved them into the circle definition
this.draw = () => {
const { x, y, r, c } = this;
context.beginPath();
context.arc(x, y, r, 0, 2 * Math.PI);
context.fillStyle = "hsla(" + c + ",100%,50%, 1)";
context.fill();
}
this.bounce = () => {
const { x, y, angle } = this;
if (x > width || x < 0) {
this.turn(180 - 2 * angle);
}
if (y > height || y < 0) {
this.turn(360 - 2 * angle);
}
}
this.turn = (angle, random = false) => {
this.changle = random ? randn(angle) : angle;
this.angle += this.changle;
}
this.forward = d => {
this.d = d;
this.x += this.d * Math.cos(this.angle * Math.PI / 180);
this.y += this.d * Math.sin(this.angle * Math.PI / 180);
}
this.collisionTestArray = a => a
.filter(circle => circle != this)
.forEach(circle => this.collision(circle));
this.collision = circle => {
var differencex = Math.abs(this.x - circle.x);
var differencey = Math.abs(this.y - circle.y);
var hdif = Math.sqrt(differencex ** 2 + differencey ** 2);
if (hdif < this.r + circle.r) {
if (differencex < differencey) {
this.turn(180 - 2 * this.angle);
circle.turn(180 - 2 * circle.angle);
} else {
this.turn(360 - 2 * this.angle);
circle.turn(360 - 2 * circle.angle);
}
this.turn(180);
circle.turn(180);
}
}
// These 2 functions I added to check if the circle was clicked
this.distanceFrom = (x, y) => Math.sqrt((this.x - x) ** 2 + (this.y - y) ** 2);
this.isWithin = (x, y) => this.r > this.distanceFrom(x, y);
}
#canvas {
border: 5px solid orange;
}
#container {
margin: auto;
width: 75%;
text-align: center;
}
<div id="container">
<h1>Click the Blue Circles Only</h1>
<canvas id="canvas" width="1000" height="600"></canvas>
<p>
Points: <span id="points">0</span>
</p>
</div>
using OOP is better in this situation and will save you a lot of time
I have written the OOP version of your game, I wrote it in harry so you may find some bugs but it is good as a starting point
const canvas = document.querySelector("canvas")
const ctx = canvas.getContext("2d")
let h = canvas.height = 600
let w = canvas.width = 800
const numberOfCircles = 20
let circles = []
// running gameover
let gameStatus = "running"
let score = 0
canvas.addEventListener("click", (e) => {
if(gameStatus === "gameOver") {
document.location.reload()
return;
}
const mouse = {x: e.offsetX, y: e.offsetY}
for(let circle of circles) {
if(distance(mouse, circle) <= circle.radius) {
if(circle.color == "blue") {
gameStatus = "running"
score += 1
} else {
gameStatus = "gameOver"
}
}
}
})
class Circle {
constructor(x, y, color, angle) {
this.x = x
this.y = y
this.color = color
this.radius = 15
this.angle = angle
this.speed = 3
}
draw(ctx) {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
move(circles) {
this.check(circles)
this.x += Math.cos(this.angle) * this.speed
this.y += Math.sin(this.angle) * this.speed
}
check(circles) {
if(this.x + this.radius > w || this.x - this.radius < 0) this.angle += Math.PI
if(this.y + this.radius > h || this.y - this.radius < 0) this.angle += Math.PI
for(let circle of circles) {
if(circle === this) continue
if(distance(this, circle) <= this.radius + circle.radius) {
// invert angles or any other effect
// there are much better soultion for resolving colliusions
circle.angle += Math.PI / 2
this.angle += Math.PI / 2
}
}
}
}
}
setUp()
gameLoop()
function gameLoop() {
ctx.clearRect(0,0,w,h)
if(gameStatus === "gameOver") {
ctx.font = "30px Comic"
ctx.fillText("Game Over", w/2 - 150, h/2 - 100)
ctx.fillText("you have scored : " + score, w/2 - 150, h/2)
return;
}
ctx.font = "30px Comic"
ctx.fillText("score : " + score, 20, 30)
for (let i = 0; i < circles.length; i++) {
const cirlce = circles[i]
cirlce.draw(ctx)
cirlce.move(circles)
}
requestAnimationFrame(gameLoop)
}
function random(to, from = 0) {
return Math.floor(Math.random() * (to - from) + from)
}
function setUp() {
gameStatus = "running"
score = 0
circles = []
for (var i = 0; i < numberOfCircles; i++) {
const randomAngle = random(360) * Math.PI / 180
circles.push(new Circle(random(w, 20), random(h, 20), randomColor(), randomAngle))
}
}
function randomColor() {
const factor = random(10)
if(factor < 3) return "blue"
return `rgb(${random(255)}, ${random(255)}, ${random(100)})`
}
function distance(obj1, obj2) {
const xDiff = obj1.x - obj2.x
const yDiff = obj1.y - obj2.y
return Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2))
}
N-Body gravity simulation seems to be working fine at first glance, and the same is true for body collisions, but once gravitationally attracted objects start to collide, they start to spiral around each other frantically and the collection of them as a whole have very erratic motion... The code (html-javascript) will be included below, and to reproduce what I'm talking about, you can create a new body by clicking in a random location on the screen.
The math for gravitational attraction is done in the Body.prototype.gravityCalc() method of the Body object type (line 261). The math for the collision resolution is found in the dynamic collision section of the bodyHandle() function (line 337).
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// event handling
document.addEventListener('keydown', keyDown);
document.addEventListener('mousedown', mouseDown)
document.addEventListener('mouseup', mouseUp)
document.addEventListener('mousemove', mouseMove);
document.addEventListener('touchstart', touchStart);
document.addEventListener('touchmove', touchMove);
document.addEventListener('touchend', touchEnd);
window.addEventListener('resize', resize);
window.onload = function() {reset()}
mouseDown = false;
nothingGrabbed = true;
mouseX = 0;
mouseY = 0;
function keyDown(data) {
if(data.key == "r") {
clearInterval(loop);
reset();
}
else if(data.key == 'g') {
gravityOn = !gravityOn;
}
else if(data.key == 'Delete') {
for(i = 0; i < bodies.length ; i++) {
if(((mouseX - bodies[i].x)**2 + (mouseY - bodies[i].y)**2) <= bodies[i].radius**2) {
bodies.splice(i, 1);
}
}
}
else if(data.key == 'c') {
gravity_c *= -1;
}
else if(data.key == 'f') {
falling = !falling;
}
else if(data.key == 'a') {
acceleration *= -1;
}
}
function mouseDown(data) {
mouseDown = true;
nothingGrabbed = true;
mouseX = data.clientX;
mouseY = canvas.height - data.clientY;
}
function mouseUp(data) {
mouseDown = false;
nothingGrabbed = true;
for(i = 0; i < bodies.length; i++) {
bodies[i].grabbed = false
}
}
function mouseMove(data) {
mouseX = data.clientX;
mouseY = canvas.height - data.clientY;
}
function touchStart(data) {
mouseDown = true;
nothingGrabbed = true;
mouseX = data.touches[0].clientX;
mouseY = canvas.height - data.touches[0].clientY;
}
function touchMove(data) {
mouseX = data.touches[0].clientX;
mouseY = canvas.height - data.touches[0].clientY;
}
function touchEnd(data) {
mouseDown = false;
nothingGrabbed = true;
for(i=0;i<bodies.length;i++) {
bodies[i].grabbed = false;
}
}
function resize(data) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Initialize Variables
function reset() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.color = 'rgb(70, 70, 70)';
scale = Math.min(canvas.width, canvas.height);
fps = 120;
running = true;
loop = setInterval(main, 1000/fps);
gravityOn = true // if true, objects are gravitationally attracted to each other
gravity_c = 334000 // universe's gravitational constant
boundaryCollision = true // if true, objects collide with edges of canvas
wallDampen = 0.7 // number to multiply by when an objects hit a wall
bodyCollision = true // if true, bodies will collide with each other
bodyDampen = 0.4 // number to multiply when two objects collide
falling = false // if true, objects will fall to the bottom of the screen
acceleration = 400
bodies = [] // a list of each Body object
collidingPairs = [] // a list of pairs of colliding bodies
/*
var bounds = 200;
for(i = 0; i<70; i++) { // randomly place bodies
Body.create({
x: Math.floor(Math.random()*canvas.width),
y: Math.floor(Math.random()*canvas.height),
a: Math.random()*Math.PI*2,
xV: Math.floor(Math.random() * (bounds - -bounds)) + -bounds,
yV: Math.floor(Math.random() * (bounds - -bounds)) + -bounds,
mass: Math.ceil(Math.random()*23)
})
} */
/*
Body.create({
x: canvas.width/2 - 50,
xV: 10,
yV: 0,
aV: 3,
y: canvas.height/2 + 0,
mass: 10
});
Body.create({
x: canvas.width/2 + 50,
xV: 0,
aV: 0,
y: canvas.height/2,
mass: 10
});
*/
Body.create({
x: canvas.width/2,
y: canvas.height/2,
mass: 24,
xV: -10.83
});
Body.create({
x: canvas.width/2,
y: canvas.height/2 + 150,
mass: 1,
xV: 260,
color: 'teal'
});
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Body Type Object
function Body(params) {
this.x = params.x || canvas.width/2;
this.y = params.y || canvas.height/2;
this.a = params.a || 0;
this.xV = params.xV || 0;
this.yV = params.yV || 0;
this.aV = params.aV || 0;
this.xA = params.xA || 0;
this.yA = params.yA || 0;
this.aA = params.aA || 0;
this.grabbed = false;
this.edgeBlock = params.edgeBlock || boundaryCollision;
this.gravity = params.gravityOn || gravityOn;
this.mass = params.mass || 6;
this.density = params.density || 0.008;
this.radius = params.radius || (this.mass/(Math.PI*this.density))**0.5;
this.color = params.color || 'crimson';
this.lineWidth = params.lineWidth || 2;
}
Body.create = function(params) {
bodies.push(new Body(params));
}
Body.prototype.move = function() {
this.xV += this.xA/fps;
this.yV += this.yA/fps;
this.aV += this.aA/fps;
this.x += this.xV/fps;
this.y += this.yV/fps;
this.a += this.aV/fps;
if(this.edgeBlock) {
if(this.x + this.radius > canvas.width) {
this.x = canvas.width - this.radius;
this.xV *= -wallDampen
}
else if(this.x - this.radius < 0) {
this.x = this.radius;
this.xV *= -wallDampen;
}
if(this.y + this.radius > canvas.height) {
this.y = canvas.height - this.radius;
this.yV *= -wallDampen;
}
else if(this.y - this.radius < 0) {
this.y = this.radius;
this.yV *= -wallDampen;
}
}
if(this.grabbed) {
this.xA = 0;
this.yA = 0;
this.xV = 0;
this.yV = 0;
this.x = mouseX;
this.y = mouseY;
}
}
Body.prototype.draw = function() {
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.lineWidth = this.lineWidth;
ctx.fillStyle = this.color;
ctx.arc(this.x, canvas.height - this.y, this.radius, 0, Math.PI*2, true);
ctx.fill();
ctx.stroke();
ctx.closePath()
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.lineWidth = this.linewidth;
ctx.moveTo(this.x, canvas.height - this.y);
ctx.lineTo(this.x + this.radius*Math.cos(this.a), canvas.height - (this.y + this.radius*Math.sin(this.a)))
ctx.stroke();
ctx.closePath();
}
// calculates gravitational attraction to 'otherObject'
Body.prototype.gravityCalc = function(otherObject) {
var x1 = this.x;
var y1 = this.y;
var x2 = otherObject.x;
var y2 = otherObject.y;
var distSquare = ((x2-x1)**2 + (y2-y1)**2);
var val = (gravity_c*otherObject.mass)/((distSquare)**(3/2));
var xA = val * (x2 - x1);
var yA = val * (y2 - y1);
return [xA, yA]
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Physics Code
function bodyHandle() {
for(i = 0; i < bodies.length; i++) {
if(mouseDown && nothingGrabbed) {
if(Math.abs((mouseX - bodies[i].x)**2 + (mouseY - bodies[i].y)**2) <= bodies[i].radius**2) {
bodies[i].grabbed = true;
nothingGrabbed = false;
}
}
bodies[i].draw()
if(running) {
if(falling) {
bodies[i].yV -= acceleration/fps;
}
bodies[i].move();
}
bodies[i].xA = 0;
bodies[i].yA = 0;
collidingPairs = []
if(gravityOn || bodyCollision) {
for(b = 0; b < bodies.length; b++) {
if(i != b) {
if(bodyCollision) {
var x1 = bodies[i].x;
var y1 = bodies[i].y;
var x2 = bodies[b].x;
var y2 = bodies[b].y;
var rSum = bodies[i].radius + bodies[b].radius;
var dist = { // vector
i: x2 - x1,
j: y2 - y1,
mag: ((x2-x1)**2 + (y2-y1)**2)**0.5,
norm: {
i: (x2-x1)/(((x2-x1)**2 + (y2-y1)**2)**0.5),
j: (y2-y1)/(((x2-x1)**2 + (y2-y1)**2)**0.5)
}
}
if(dist.mag <= rSum) { // static collision
var overlap = rSum - dist.mag;
bodies[i].x -= overlap/2 * dist.norm.i;
bodies[i].y -= overlap/2 * dist.norm.j;
bodies[b].x += overlap/2 * dist.norm.i;
bodies[b].y += overlap/2 * dist.norm.j;
collidingPairs.push([bodies[i], bodies[b]]);
}
}
if(gravityOn) {
if(bodies[i].gravity) {
var accel = bodies[i].gravityCalc(bodies[b]);
bodies[i].xA += accel[0];
bodies[i].yA += accel[1];
}
}
}
}
}
for(c = 0; c < collidingPairs.length; c++) { // dynamic collision
var x1 = collidingPairs[c][0].x;
var y1 = collidingPairs[c][0].y;
var r1 = collidingPairs[c][0].radius;
var x2 = collidingPairs[c][1].x;
var y2 = collidingPairs[c][1].y;
var r2 = collidingPairs[c][1].radius;
var dist = { // vector from b1 to b2
i: x2 - x1,
j: y2 - y1,
mag: ((x2-x1)**2 + (y2-y1)**2)**0.5,
norm: {
i: (x2-x1)/(((x2-x1)**2 + (y2-y1)**2)**0.5),
j: (y2-y1)/(((x2-x1)**2 + (y2-y1)**2)**0.5)
}
}
var m1 = collidingPairs[c][0].mass;
var m2 = collidingPairs[c][1].mass;
var norm = { // vector normal along 'wall' of collision
i: -dist.j/(((dist.i)**2 + (-dist.j)**2)**0.5),
j: dist.i/(((dist.i)**2 + (-dist.j)**2)**0.5)
}
var perp = { // vector normal pointing from b1 to b2
i: dist.norm.i,
j: dist.norm.j
}
var vel1 = { // vector of b1 velocity
i: collidingPairs[c][0].xV,
j: collidingPairs[c][0].yV,
dot: function(vect) {
return collidingPairs[c][0].xV * vect.i + collidingPairs[c][0].yV * vect.j
}
}
var vel2 = { // vector of b2 velocity
i: collidingPairs[c][1].xV,
j: collidingPairs[c][1].yV,
dot: function(vect) {
return collidingPairs[c][1].xV * vect.i + collidingPairs[c][1].yV * vect.j
}
}
// new velocities along perp^ of b1 and b2
var nV1Perp = (vel1.dot(perp))*(m1-m2)/(m1+m2) + (vel2.dot(perp))*(2*m2)/(m1+m2);
var nV2Perp = (vel1.dot(perp))*(2*m1)/(m1+m2) + (vel2.dot(perp))*(m2-m1)/(m1+m2);
/* testing rotation after collision
// velocities of the points of collision on b1 and b2
var pVel1M = vel1.dot(norm) + collidingPairs[c][0].aV*r1;
var pVel2M = vel2.dot(norm) + collidingPairs[c][1].aV*r2;
// moment of inertia for b1 and b2
var I1 = 1/2 * m1 * r1**2;
var I2 = 1/2 * m2 * r2**2;
// new velocities of the points of collisions on b1 and b2
var newpVel1M = ((I1-I2)/(I1+I2))*pVel1M + ((2*I2)/(I1+I2))*pVel2M;
var newpVel2M = ((2*I1)/(I1+I2))*pVel1M + ((I2-I1)/(I1+I2))*pVel2M;
var vectToCol1 = { // vector from x1,y1 to point of collision on b1
i: r1*perp.i,
j: r1*perp.j
};
var vectToCol2 = { // vector from x2,y2 to point of collision on b2
i: r2*-perp.i,
j: r2*-perp.j
};
// sign of cross product of pVelM and vectToCol
var vCrossR1 = (pVel1M*norm.i)*(vectToCol1.j) - (pVel1M*norm.j)*(vectToCol1.i);
vCrossR1 = vCrossR1/Math.abs(vCrossR1);
var vCrossR2 = (pVel2M*norm.i)*(vectToCol2.j) - (pVel2M*norm.j)*(vectToCol2.i);
vCrossR2 = vCrossR2/Math.abs(vCrossR2);
collidingPairs[c][0].aV = vCrossR1 * (newpVel1M)/r1;
collidingPairs[c][1].aV = vCrossR2 * (newpVel2M)/r2;
/* draw collision point velocity vectors [debugging]
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.moveTo(x1 + vectToCol1.i, canvas.height - (y1 + vectToCol1.j));
ctx.lineTo((x1+vectToCol1.i) + pVel1M*norm.i, (canvas.height- (y1+vectToCol1.j + pVel1M*norm.j)));
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.strokeStyle = 'white';
ctx.moveTo(x2 + vectToCol2.i, canvas.height - (y2 + vectToCol2.j));
ctx.lineTo((x2+vectToCol2.i) + pVel2M*norm.i, (canvas.height- (y2+vectToCol2.j + pVel2M*norm.j)));
ctx.stroke();
ctx.closePath();
console.log(pVel1M, pVel2M);
clearInterval(loop);
*/
collidingPairs[c][0].xV = vel1.dot(norm)*norm.i + nV1Perp*perp.i * bodyDampen;
collidingPairs[c][0].yV = vel1.dot(norm)*norm.j + nV1Perp*perp.j * bodyDampen;
collidingPairs[c][1].xV = vel2.dot(norm)*norm.i + nV2Perp*perp.i * bodyDampen;
collidingPairs[c][1].yV = vel2.dot(norm)*norm.j + nV2Perp*perp.j * bodyDampen;
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Main Loop
function main() {
// blank out canvas
ctx.fillStyle = canvas.color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
bodyHandle();
if(nothingGrabbed && mouseDown) {
bodies.push(new Body({x: mouseX,
y: mouseY,
mass: 90}));
bodies[bodies.length-1].move();
bodies[bodies.length-1].draw();
}
}
<html>
<meta name='viewport' content='width=device-width,height=device-height'>
<body>
<canvas id="canvas" width='300px' height='300px'></canvas>
<style>
body {
padding: 0;
margin: 0;
}
canvas {
padding: 0;
margin: 0;
}
</style>
</html>
I cannot tell you much about the code. Personally it seems to me that the animations could be correct.
If you want to test your code you could try to test if laws of conservation of energy and momentum are respected. You could, for example, sum the momentum of every object (mass times velocity) and see if the number are maintained constant when there are no forces from the outside (collisions with the wall). To do this I would suggest to make the free space available larger. Another quantity is the total energy (kinetic plus potential) which is a bit harder, but still easy to compute (to compute tot. pot. energy you have to sum over all pairs).
I'm working on a certain layout where I need to draw a hexagon which needs to be clickable. I'm using the Path2D construct and isPointInPath function. I'm constructing an animation where a number of hexagons is created and then each moved to a certain position. After the movement is done, I am attaching onclick event handlers to certain hexagons. However there is weird behaviour.
Some initialized variables
const COLOR_DARK = "#73b6c6";
const COLOR_LIGHT = "#c3dadd";
const COLOR_PRIMARY = "#39a4c9";
const TYPE_PRIMARY = 'primary';
let hexagons = [];
Below is the function which draws the hexagons.
function drawHex(ctx, x, y, hexProps, stroke, color) {
let myPath = new Path2D();
myPath.moveTo(x + hexProps.width*0.5, y);
myPath.lineTo(x, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width*0.5, y + hexProps.height);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x + hexProps.width*0.5, y);
myPath.closePath();
if (stroke){
ctx.strokeStyle = color;
ctx.stroke(myPath);
} else {
ctx.fillStyle = color;
ctx.fill(myPath);
}
return myPath;
}
This function populates the hexagon array
function populateLeftHex(canvasWidth, canvasHeight, hexProps) {
const startX = canvasWidth / 2;
const startY = canvasHeight / 2;
const baseLeft = canvasWidth * 0.05;
for(let i = 0; i < 5; i++){
let hexNumber = (i % 4 == 0)? 2: 1;
for(let j = 0; j < hexNumber; j++){
hexagons.push({
startX: startX,
startY: startY,
endX: baseLeft + (2 * j) + ((i % 2 == 0)? (hexProps.width * j) : (hexProps.width/2)),
endY: ((i + 1) * hexProps.height) - ((i) * hexProps.height * hexProps.facShort) + (i* 2),
stroke: true,
color: ( i % 2 == 0 && j % 2 == 0)? COLOR_DARK : COLOR_LIGHT,
type: TYPE_PRIMARY
});
}
}
}
And here is where Im calling the isPointInPath function.
window.onload = function (){
const c = document.getElementById('canvas');
const canvasWidth = c.width = window.innerWidth,
canvasHeight = c.height = window.innerHeight,
ctx = c.getContext('2d');
window.requestAnimFrame = (function (callback) {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
console.log(canvasWidth);
let hexProps = {
width: canvasWidth * 0.075,
get height () {
return this.width/Math.sqrt(3) + (1.5)*(this.width/Math.sqrt(2)/2);
} ,
facShort: 0.225,
get facLong () {
return 1 - this.facShort;
}
};
populateLeftHex(canvasWidth, canvasHeight, hexProps);
let pct = 0;
const fps = 200;
animate();
function animate () {
setTimeout(function () {
// increment pct towards 100%
pct += .03;
// if we're not done, request another animation frame
if (pct < 1.00) {
requestAnimFrame(animate);
} else { //if pct is no longer less than 1.00, then the movement animation is over.
hexagons.forEach(function (hex) {
if(hex.type === TYPE_PRIMARY) {
console.info(hex.path);
c.onclick = function(e) {
let x = e.clientX - c.offsetLeft,
y = e.clientY - c.offsetTop;
console.info(ctx.isPointInPath(hex.path, (e.clientX - c.offsetLeft), (e.clientY - c.offsetTop) ));
};
}
})
}
ctx.clearRect(0, 0, c.width, c.height);
// draw all hexagons
for ( let i = 0; i < hexagons.length; i++) {
// get reference to next shape
let hex = hexagons[i];
// note: dx/dy are fixed values
// they could be put in the shape object for efficiency
let dx = hex.endX - hex.startX;
let dy = hex.endY - hex.startY;
let nextX = hex.startX + dx * pct;
let nextY = hex.startY + dy * pct;
hex = hexagons[i];
ctx.fillStyle = hex.color;
hex.path = drawHex(ctx, nextX, nextY, hexProps, hex.stroke, hex.color);
}
}, 1000 / fps);
}
Can you help me figure out what I'm doing wrong? Maybe I misunderstood how Path2D works? Thanks in advance.
Had to do a bit of work to build a test page as your example is incomplete, but this is working for me - though my hexagon is concave...
var myCanvas = document.getElementById("myCanvas");
var ctx = myCanvas.getContext("2d");
var hexProps = {width:100, height:100, facShort:-2, facLong:10};
var hexagons = [];
function drawHex(ctx, x, y, hexProps, stroke, color) {
let myPath = new Path2D();
myPath.moveTo(x + hexProps.width*0.5, y);
myPath.lineTo(x, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width*0.5, y + hexProps.height);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x + hexProps.width*0.5, y);
myPath.closePath();
if (stroke){
ctx.strokeStyle = color;
ctx.stroke(myPath);
} else {
ctx.fillStyle = color;
ctx.fill(myPath);
}
return myPath;
}
hexagons.push({type:0, path:drawHex(ctx,100,100,hexProps,false,"#0f0")});
hexagons.forEach(function (hex) {
if(hex.type === 0) {
console.info(hex.path);
myCanvas.onclick = function(e) {
let x = e.clientX - myCanvas.offsetLeft,
y = e.clientY - myCanvas.offsetTop;
console.info(x,y);
console.info(ctx.isPointInPath(hex.path, (e.clientX -
myCanvas.offsetLeft), (e.clientY - myCanvas.offsetTop) ));
};
}
})
<canvas width=500 height=500 id=myCanvas style='border:1px solid red'></canvas>
Test clicks give true and false where expected:
test.htm:48 165 168
test.htm:49 true
test.htm:48 151 336
test.htm:49 false
test.htm:48 124 314
test.htm:49 true
test.htm:48 87 311
test.htm:49 false
I'm trying to create a hyperdrive effect, like from Star Wars, where the stars have a motion trail. I've gotten as far as creating the motion trail on a single circle, it still looks like the trail is going down in the y direction and not forwards or positive in the z direction.
Also, how could I do this with (many) randomly placed circles as if they were stars?
My code is on jsfiddle (https://jsfiddle.net/5m7x5zxu/) and below:
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var xPos = 180;
var yPos = 100;
var motionTrailLength = 16;
var positions = [];
function storeLastPosition(xPos, yPos) {
// push an item
positions.push({
x: xPos,
y: yPos
});
//get rid of first item
if (positions.length > motionTrailLength) {
positions.pop();
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = positions.length-1; i > 0; i--) {
var ratio = (i - 1) / positions.length;
drawCircle(positions[i].x, positions[i].y, ratio);
}
drawCircle(xPos, yPos, "source");
var k=2;
storeLastPosition(xPos, yPos);
// update position
if (yPos > 125) {
positions.pop();
}
else{
yPos += k*1.1;
}
requestAnimationFrame(update);
}
update();
function drawCircle(x, y, r) {
if (r == "source") {
r = 1;
} else {
r*=1.1;
}
context.beginPath();
context.arc(x, y, 3, 0, 2 * Math.PI, true);
context.fillStyle = "rgba(255, 255, 255, " + parseFloat(1-r) + ")";
context.fill();
}
Canvas feedback and particles.
This type of FX can be done many ways.
You could just use a particle systems and draw stars (as lines) moving away from a central point, as the speed increase you increase the line length. When at low speed the line becomes a circle if you set ctx.lineWidth > 1 and ctx.lineCap = "round"
To add to the FX you can use render feedback as I think you have done by rendering the canvas over its self. If you render it slightly larger you get a zoom FX. If you use ctx.globalCompositeOperation = "lighter" you can increase the stars intensity as you speed up to make up for the overall loss of brightness as stars move faster.
Example
I got carried away so you will have to sift through the code to find what you need.
The particle system uses the Point object and a special array called bubbleArray to stop GC hits from janking the animation.
You can use just an ordinary array if you want. The particles are independent of the bubble array. When they have moved outside the screen they are move to a pool and used again when a new particle is needed. The update function moves them and the draw Function draws them I guess LOL
The function loop is the main loop and adds and draws particles (I have set the particle count to 400 but should handle many more)
The hyper drive is operated via the mouse button. Press for on, let go for off. (It will distort the text if it's being displayed)
The canvas feedback is set via that hyperSpeed variable, the math is a little complex. The sCurce function just limits the value to 0,1 in this case to stop alpha from going over or under 1,0. The hyperZero is just the sCurve return for 1 which is the hyper drives slowest speed.
I have pushed the feedback very close to the limit. In the first few lines of the loop function you can set the top speed if(mouse.button){ if(hyperSpeed < 1.75){ Over this value 1.75 and you will start to get bad FX, at about 2 the whole screen will just go white (I think that was where)
Just play with it and if you have questions ask in the comments.
const ctx = canvas.getContext("2d");
// very simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// High performance array pool using buubleArray to separate pool objects and active object.
// This is designed to eliminate GC hits involved with particle systems and
// objects that have short lifetimes but used often.
// Warning this code is not well tested.
const bubbleArray = () => {
const items = [];
var count = 0;
return {
clear(){ // warning this dereferences all locally held references and can incur Big GC hit. Use it wisely.
this.items.length = 0;
count = 0;
},
update() {
var head, tail;
head = tail = 0;
while(head < count){
if(items[head].update() === false) {head += 1 }
else{
if(tail < head){
const temp = items[head];
items[head] = items[tail];
items[tail] = temp;
}
head += 1;
tail += 1;
}
}
return count = tail;
},
createCallFunction(name, earlyExit = false){
name = name.split(" ")[0];
const keys = Object.keys(this);
if(Object.keys(this).indexOf(name) > -1){ throw new Error(`Can not create function name '${name}' as it already exists.`) }
if(!/\W/g.test(name)){
let func;
if(earlyExit){
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ if (items[i++].${name}() === true) { break } }`;
}else{
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ items[i++].${name}() }`;
}
!this.items && (this.items = items);
this[name] = new Function(func);
}else{ throw new Error(`Function name '${name}' contains illegal characters. Use alpha numeric characters.`) }
},
callEach(name){var i = 0; while(i < count){ if (items[i++][name]() === true) { break } } },
each(cb) { var i = 0; while(i < count){ if (cb(items[i], i++) === true) { break } } },
next() { if (count < items.length) { return items[count ++] } },
add(item) {
if(count === items.length){
items.push(item);
count ++;
}else{
items.push(items[count]);
items[count++] = item;
}
return item;
},
getCount() { return count },
}
}
// Helpers rand float, randI random Int
// doFor iterator
// sCurve curve input -Infinity to Infinity out -1 to 1
// randHSLA creates random colour
// CImage, CImageCtx create image and image with context attached
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const sCurve = (v,p) => (2 / (1 + Math.pow(p,-v))) -1;
const randHSLA = (h, h1, s = 100, s1 = 100, l = 50, l1 = 50, a = 1, a1 = 1) => { return `hsla(${randI(h,h1) % 360},${randI(s,s1)}%,${randI(l,l1)}%,${rand(a,a1)})` }
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// create image to hold text
var textImage = CImageCtx(1024, 1024);
var c = textImage.ctx;
c.fillStyle = "#FF0";
c.font = "64px arial black";
c.textAlign = "center";
c.textBaseline = "middle";
const text = "HYPER,SPEED FX,VII,,Battle of Jank,,Hold the mouse,button to increase,speed.".split(",");
text.forEach((line,i) => { c.fillText(line,512,i * 68 + 68) });
const maxLines = text.length * 68 + 68;
function starWarIntro(image,x1,y1,x2,y2,pos){
var iw = image.width;
var ih = image.height;
var hh = (x2 - x1) / (y2 - y1); // Slope of left edge
var w2 = iw / 2; // half width
var z1 = w2 - x1; // Distance (z) to first line
var z2 = (z1 / (w2 - x2)) * z1 - z1; // distance (z) between first and last line
var sk,t3,t3a,z3a,lines, z3, dd = 0, a = 0, as = 2 / (y2 - y1);
for (var y = y1; y < y2 && dd < maxLines; y++) { // for each line
t3 = ((y - y1) * hh) + x1; // get scan line top left edge
t3a = (((y+1) - y1) * hh) + x1; // get scan line bottom left edge
z3 = (z1 / (w2 - t3)) * z1; // get Z distance to top of this line
z3a = (z1 / (w2 - t3a)) * z1; // get Z distance to bottom of this line
dd = ((z3 - z1) / z2) * ih; // get y bitmap coord
a += as;
ctx.globalAlpha = a < 1 ? a : 1;
dd += pos; // kludge for this answer to make text move
// does not move text correctly
lines = ((z3a - z1) / z2) * ih-dd; // get number of lines to copy
ctx.drawImage(image, 0, dd , iw, lines, t3, y, w - t3 * 2, 1.5);
}
}
// canvas settings
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// diagonal distance used to set point alpha (see point update)
var diag = Math.sqrt(w * w + h * h);
// If window size is changed this is called to resize the canvas
// It is not called via the resize event as that can fire to often and
// debounce makes it feel sluggish so is called from main loop.
function resizeCanvas(){
points.clear();
canvas.width = innerWidth;
canvas.height = innerHeight;
w = canvas.width;
h = canvas.height;
cw = w / 2; // center
ch = h / 2;
diag = Math.sqrt(w * w + h * h);
}
// create array of points
const points = bubbleArray();
// create optimised draw function itterator
points.createCallFunction("draw",false);
// spawns a new star
function spawnPoint(pos){
var p = points.next();
p = points.add(new Point())
if (p === undefined) { p = points.add(new Point()) }
p.reset(pos);
}
// point object represents a single star
function Point(pos){ // this function is duplicated as reset
if(pos){
this.x = pos.x;
this.y = pos.y;
this.dead = false;
}else{
this.x = 0;
this.y = 0;
this.dead = true;
}
this.alpha = 0;
var x = this.x - cw;
var y = this.y - ch;
this.dir = Math.atan2(y,x);
this.distStart = Math.sqrt(x * x + y * y);
this.speed = rand(0.01,1);
this.col = randHSLA(220,280,100,100,50,100);
this.dx = Math.cos(this.dir) * this.speed;
this.dy = Math.sin(this.dir) * this.speed;
}
Point.prototype = {
reset : Point, // resets the point
update(){ // moves point and returns false when outside
this.speed *= hyperSpeed; // increase speed the more it has moved
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
var x = this.x - cw;
var y = this.y - ch;
this.alpha = (Math.sqrt(x * x + y * y) - this.distStart) / (diag * 0.5 - this.distStart);
if(this.alpha > 1 || this.x < 0 || this.y < 0 || this.x > w || this.h > h){
this.dead = true;
}
return !this.dead;
},
draw(){ // draws the point
ctx.strokeStyle = this.col;
ctx.globalAlpha = 0.25 + this.alpha *0.75;
ctx.beginPath();
ctx.lineTo(this.x - this.dx * this.speed, this.y - this.dy * this.speed);
ctx.lineTo(this.x, this.y);
ctx.stroke();
}
}
const maxStarCount = 400;
const p = {x : 0, y : 0};
var hyperSpeed = 1.001;
const alphaZero = sCurve(1,2);
var startTime;
function loop(time){
if(startTime === undefined){
startTime = time;
}
if(w !== innerWidth || h !== innerHeight){
resizeCanvas();
}
// if mouse down then go to hyper speed
if(mouse.button){
if(hyperSpeed < 1.75){
hyperSpeed += 0.01;
}
}else{
if(hyperSpeed > 1.01){
hyperSpeed -= 0.01;
}else if(hyperSpeed > 1.001){
hyperSpeed -= 0.001;
}
}
var hs = sCurve(hyperSpeed,2);
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,0,0); // reset transform
//==============================================================
// UPDATE the line below could be the problem. Remove it and try
// what is under that
//==============================================================
//ctx.fillStyle = `rgba(0,0,0,${1-(hs-alphaZero)*2})`;
// next two lines are the replacement
ctx.fillStyle = "Black";
ctx.globalAlpha = 1-(hs-alphaZero) * 2;
//==============================================================
ctx.fillRect(0,0,w,h);
// the amount to expand canvas feedback
var sx = (hyperSpeed-1) * cw * 0.1;
var sy = (hyperSpeed-1) * ch * 0.1;
// increase alpha as speed increases
ctx.globalAlpha = (hs-alphaZero)*2;
ctx.globalCompositeOperation = "lighter";
// draws feedback twice
ctx.drawImage(canvas,-sx, -sy, w + sx*2 , h + sy*2)
ctx.drawImage(canvas,-sx/2, -sy/2, w + sx , h + sy)
ctx.globalCompositeOperation = "source-over";
// add stars if count < maxStarCount
if(points.getCount() < maxStarCount){
var cent = (hyperSpeed - 1) *0.5; // pulls stars to center as speed increases
doFor(10,()=>{
p.x = rand(cw * cent ,w - cw * cent); // random screen position
p.y = rand(ch * cent,h - ch * cent);
spawnPoint(p)
})
}
// as speed increases make lines thicker
ctx.lineWidth = 2 + hs*2;
ctx.lineCap = "round";
points.update(); // update points
points.draw(); // draw points
ctx.globalAlpha = 1;
// scroll the perspective star wars text FX
var scrollTime = (time - startTime) / 5 - 2312;
if(scrollTime < 1024){
starWarIntro(textImage,cw - h * 0.5, h * 0.2, cw - h * 3, h , scrollTime );
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Here's another simple example, based mainly on the same idea as Blindman67, concetric lines moving away from center at different velocities (the farther from center, the faster it moves..) also no recycling pool here.
"use strict"
var c = document.createElement("canvas");
document.body.append(c);
var ctx = c.getContext("2d");
var w = window.innerWidth;
var h = window.innerHeight;
var ox = w / 2;
var oy = h / 2;
c.width = w; c.height = h;
const stars = 120;
const speed = 0.5;
const trailLength = 90;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#fff"
ctx.fillRect(ox, oy, 1, 1);
init();
function init() {
var X = [];
var Y = [];
for(var i = 0; i < stars; i++) {
var x = Math.random() * w;
var y = Math.random() * h;
X.push( translateX(x) );
Y.push( translateY(y) );
}
drawTrails(X, Y)
}
function translateX(x) {
return x - ox;
}
function translateY(y) {
return oy - y;
}
function getDistance(x, y) {
return Math.sqrt(x * x + y * y);
}
function getLineEquation(x, y) {
return function(n) {
return y / x * n;
}
}
function drawTrails(X, Y) {
var count = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
function anim() {
for(var i = 0; i < X.length; i++) {
var x = X[i];
var y = Y[i];
drawNextPoint(x, y, count);
}
count+= speed;
if(count < trailLength) {
window.requestAnimationFrame(anim);
}
else {
init();
}
}
anim();
}
function drawNextPoint(x, y, step) {
ctx.fillStyle = "#fff";
var f = getLineEquation(x, y);
var coef = Math.abs(x) / 100;
var dist = getDistance( x, y);
var sp = speed * dist / 100;
for(var i = 0; i < sp; i++) {
var newX = x + Math.sign(x) * (step + i) * coef;
var newY = translateY( f(newX) );
ctx.fillRect(newX + ox, newY, 1, 1);
}
}
body {
overflow: hidden;
}
canvas {
position: absolute;
left: 0;
top: 0;
}
I am new to game development and I have build a car game where the automatically moves and when it hits a monster.Now I want to make the car move towards the monster.So I looked into the path finding algorithms and for now I thought to implement A-Star path finding algorithm in my game.So the function for finding path is like below:
function findPath(world, pathStart, pathEnd)
{
// shortcuts for speed
var abs = Math.abs;
var max = Math.max;
var pow = Math.pow;
var sqrt = Math.sqrt;
// the world data are integers:
// anything higher than this number is considered blocked
// this is handy is you use numbered sprites, more than one
// of which is walkable road, grass, mud, etc
var maxWalkableTileNum = 0;
// keep track of the world dimensions
// Note that this A-star implementation expects the world array to be square:
// it must have equal height and width. If your game world is rectangular,
// just fill the array with dummy values to pad the empty space.
var worldWidth = world[0].length;
var worldHeight = world.length;
var worldSize = worldWidth * worldHeight;
// which heuristic should we use?
// default: no diagonals (Manhattan)
var distanceFunction = ManhattanDistance;
var findNeighbours = function(){}; // empty
/*
// alternate heuristics, depending on your game:
// diagonals allowed but no sqeezing through cracks:
var distanceFunction = DiagonalDistance;
var findNeighbours = DiagonalNeighbours;
// diagonals and squeezing through cracks allowed:
var distanceFunction = DiagonalDistance;
var findNeighbours = DiagonalNeighboursFree;
// euclidean but no squeezing through cracks:
var distanceFunction = EuclideanDistance;
var findNeighbours = DiagonalNeighbours;
// euclidean and squeezing through cracks allowed:
var distanceFunction = EuclideanDistance;
var findNeighbours = DiagonalNeighboursFree;
*/
// distanceFunction functions
// these return how far away a point is to another
function ManhattanDistance(Point, Goal)
{ // linear movement - no diagonals - just cardinal directions (NSEW)
return abs(Point.x - Goal.x) + abs(Point.y - Goal.y);
}
function DiagonalDistance(Point, Goal)
{ // diagonal movement - assumes diag dist is 1, same as cardinals
return max(abs(Point.x - Goal.x), abs(Point.y - Goal.y));
}
function EuclideanDistance(Point, Goal)
{ // diagonals are considered a little farther than cardinal directions
// diagonal movement using Euclide (AC = sqrt(AB^2 + BC^2))
// where AB = x2 - x1 and BC = y2 - y1 and AC will be [x3, y3]
return sqrt(pow(Point.x - Goal.x, 2) + pow(Point.y - Goal.y, 2));
}
// Neighbours functions, used by findNeighbours function
// to locate adjacent available cells that aren't blocked
// Returns every available North, South, East or West
// cell that is empty. No diagonals,
// unless distanceFunction function is not Manhattan
function Neighbours(x, y)
{
var N = y - 1,
S = y + 1,
E = x + 1,
W = x - 1,
myN = N > -1 && canWalkHere(x, N),
myS = S < worldHeight && canWalkHere(x, S),
myE = E < worldWidth && canWalkHere(E, y),
myW = W > -1 && canWalkHere(W, y),
result = [];
if(myN)
result.push({x:x, y:N});
if(myE)
result.push({x:E, y:y});
if(myS)
result.push({x:x, y:S});
if(myW)
result.push({x:W, y:y});
findNeighbours(myN, myS, myE, myW, N, S, E, W, result);
return result;
}
// returns every available North East, South East,
// South West or North West cell - no squeezing through
// "cracks" between two diagonals
function DiagonalNeighbours(myN, myS, myE, myW, N, S, E, W, result)
{
if(myN)
{
if(myE && canWalkHere(E, N))
result.push({x:E, y:N});
if(myW && canWalkHere(W, N))
result.push({x:W, y:N});
}
if(myS)
{
if(myE && canWalkHere(E, S))
result.push({x:E, y:S});
if(myW && canWalkHere(W, S))
result.push({x:W, y:S});
}
}
// returns every available North East, South East,
// South West or North West cell including the times that
// you would be squeezing through a "crack"
function DiagonalNeighboursFree(myN, myS, myE, myW, N, S, E, W, result)
{
myN = N > -1;
myS = S < worldHeight;
myE = E < worldWidth;
myW = W > -1;
if(myE)
{
if(myN && canWalkHere(E, N))
result.push({x:E, y:N});
if(myS && canWalkHere(E, S))
result.push({x:E, y:S});
}
if(myW)
{
if(myN && canWalkHere(W, N))
result.push({x:W, y:N});
if(myS && canWalkHere(W, S))
result.push({x:W, y:S});
}
}
// returns boolean value (world cell is available and open)
function canWalkHere(x, y)
{
return ((world[x] != null) &&
(world[x][y] != null) &&
(world[x][y] <= maxWalkableTileNum));
};
// Node function, returns a new object with Node properties
// Used in the calculatePath function to store route costs, etc.
function Node(Parent, Point)
{
var newNode = {
// pointer to another Node object
Parent:Parent,
// array index of this Node in the world linear array
value:Point.x + (Point.y * worldWidth),
// the location coordinates of this Node
x:Point.x,
y:Point.y,
// the heuristic estimated cost
// of an entire path using this node
f:0,
// the distanceFunction cost to get
// from the starting point to this node
g:0
};
return newNode;
}
// Path function, executes AStar algorithm operations
function calculatePath()
{
// create Nodes from the Start and End x,y coordinates
var mypathStart = Node(null, {x:pathStart[0], y:pathStart[1]});
var mypathEnd = Node(null, {x:pathEnd[0], y:pathEnd[1]});
// create an array that will contain all world cells
var AStar = new Array(worldSize);
// list of currently open Nodes
var Open = [mypathStart];
// list of closed Nodes
var Closed = [];
// list of the final output array
var result = [];
// reference to a Node (that is nearby)
var myNeighbours;
// reference to a Node (that we are considering now)
var myNode;
// reference to a Node (that starts a path in question)
var myPath;
// temp integer variables used in the calculations
var length, max, min, i, j;
// iterate through the open list until none are left
while(length = Open.length)
{
max = worldSize;
min = -1;
for(i = 0; i < length; i++)
{
if(Open[i].f < max)
{
max = Open[i].f;
min = i;
}
}
// grab the next node and remove it from Open array
myNode = Open.splice(min, 1)[0];
// is it the destination node?
if(myNode.value === mypathEnd.value)
{
myPath = Closed[Closed.push(myNode) - 1];
do
{
result.push([myPath.x, myPath.y]);
}
while (myPath = myPath.Parent);
// clear the working arrays
AStar = Closed = Open = [];
// we want to return start to finish
result.reverse();
}
else // not the destination
{
// find which nearby nodes are walkable
myNeighbours = Neighbours(myNode.x, myNode.y);
// test each one that hasn't been tried already
for(i = 0, j = myNeighbours.length; i < j; i++)
{
myPath = Node(myNode, myNeighbours[i]);
if (!AStar[myPath.value])
{
// estimated cost of this particular route so far
myPath.g = myNode.g + distanceFunction(myNeighbours[i], myNode);
// estimated cost of entire guessed route to the destination
myPath.f = myPath.g + distanceFunction(myNeighbours[i], mypathEnd);
// remember this new path for testing above
Open.push(myPath);
// mark this node in the world graph as visited
AStar[myPath.value] = true;
}
}
// remember this route as having no more untested options
Closed.push(myNode);
}
} // keep iterating until the Open list is empty
return result;
}
// actually calculate the a-star path!
// this returns an array of coordinates
// that is empty if no path is possible
return calculatePath();
} // end of findPath() function
and then call the function by
currentPath = findPath(world,pathStart,pathEnd);
But not working.My working pen
Any help is appreciated.
Here is a simple path finding script to start from.
Once you have a path calculated, it should be trivial to move the car along it.
This script has two stages:
World generation
Where the map is scanned for hindrances and monsters
Path generation
Where a monster is found and a path is being calculated.
//HTML elements
var canvas = document.body.appendChild(document.createElement("canvas"));
canvas.height = 500;
canvas.width = canvas.height;
var ctx = canvas.getContext("2d");
//Logic elements
var tileSize = 16;
var monster = {
x: Math.floor(Math.random() * Math.ceil(canvas.width / tileSize) / 2) * 2,
y: Math.floor(Math.random() * Math.ceil(canvas.height / tileSize) / 2) * 2
};
var player = {
x: 9,
y: 9
};
var aStar = {
path: [],
opened: [],
closed: [],
done: false
};
//Simple distance formular
function distance(a, b) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
//Tested Tiles
ctx.fillStyle = "cyan";
for (var pi = 0; pi < aStar.closed.length; pi++) {
var p = aStar.closed[pi];
ctx.fillRect(p.x * tileSize, p.y * tileSize, tileSize, tileSize);
}
//Path
ctx.fillStyle = "blue";
for (var pi = 0; pi < aStar.path.length; pi++) {
var p = aStar.path[pi];
ctx.fillRect(p.x * tileSize, p.y * tileSize, tileSize, tileSize);
}
//Monster
ctx.fillStyle = "red";
ctx.fillRect(monster.x * tileSize, monster.y * tileSize, tileSize, tileSize);
//Player
ctx.fillStyle = "green";
ctx.fillRect(player.x * tileSize, player.y * tileSize, tileSize, tileSize);
//Tiles
for (var x = 0; x < Math.ceil(canvas.width / tileSize); x++) {
for (var y = 0; y < Math.ceil(canvas.height / tileSize); y++) {
ctx.strokeRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
}
}
function main() {
//If no steps, open "player"
if (aStar.opened.length == 0) {
aStar.opened.push({
x: player.x,
y: player.y,
step: 0
});
}
//Check for monster
if ((aStar.opened.some(function(c) {
return c.x === monster.x && c.y === monster.y;
})) == true) {
//if monster found
if (aStar.path.length < 1) {
//If no steps in path, add monster as first
aStar.path.push(aStar.opened.find(function(c) {
return c.x === monster.x && c.y === monster.y;
}));
} else if ((aStar.path.length > 0 ? aStar.path[aStar.path.length - 1].step == 0 : false) === false) {
//If last step of path isn't player, compute a step to path
var lastTile = aStar.path[aStar.path.length - 1];
var bestTile = {
x: lastTile.x,
y: lastTile.y,
step: lastTile.step
};
//Loop through tiles adjacent to the last path tile and pick the "best"
for (var x = lastTile.x - 1; x < lastTile.x + 2; x++) {
for (var y = lastTile.y - 1; y < lastTile.y + 2; y++) {
var suspect = aStar.closed.find(function(c) {
return c.x === x && c.y === y;
});
if (suspect !== void 0) {
if (suspect.step + distance(suspect, player) < bestTile.step + distance(bestTile, player)) {
bestTile = suspect;
}
}
}
}
//Add best tile to path
aStar.path.push(bestTile);
}
} else {
//If monster isn't found, continue world mapping
//"newOpen" will hold the next "opened" list
var newOpen = [];
//For each opened, check neighbours
for (var oi = 0; oi < aStar.opened.length; oi++) {
var o = aStar.opened[oi];
for (var x = o.x - 1; x < o.x + 2; x++) {
for (var y = o.y - 1; y < o.y + 2; y++) {
if (x === o.x && y === o.y ||
aStar.closed.some(function(c) {
return c.x === x && c.y === y;
}) ||
aStar.opened.some(function(c) {
return c.x === x && c.y === y;
}) ||
newOpen.some(function(c) {
return c.x === x && c.y === y;
})) {
continue;
}
//If neighbours isn't in any list, add it to the newOpen list
newOpen.push({
x: x,
y: y,
step: o.step + 1
});
}
}
}
//Close the previously opened list
aStar.closed = aStar.closed.concat(aStar.opened);
//Add new opened list
aStar.opened = newOpen;
}
//Draw progress
draw();
requestAnimationFrame(main);
}
//Start process
requestAnimationFrame(main);
EDIT 1 - No pathfinding
I am not even sure you need pathfinding for this.
In the example below the cars are simply pushed towards a target relative to their angle to it:
var __extends = (this && this.__extends) || (function() {
var extendStatics = Object.setPrototypeOf ||
({
__proto__: []
}
instanceof Array && function(d, b) {
d.__proto__ = b;
}) ||
function(d, b) {
for (var p in b)
if (b.hasOwnProperty(p)) d[p] = b[p];
};
return function(d, b) {
extendStatics(d, b);
function __() {
this.constructor = d;
}
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Game;
(function(Game) {
var GameImage = (function() {
function GameImage(name, src) {
this.name = name;
this.src = src;
this.node = document.createElement("img");
GameImage._pending++;
this.node.onload = GameImage._loading;
this.node.src = this.src;
GameImage.all.push(this);
}
GameImage.loaded = function() {
return this._loaded === this._pending;
};
GameImage._loading = function() {
this._loaded++;
};
GameImage.getImage = function(id) {
return this.all.find(function(img) {
return img.name === id;
});
};
return GameImage;
}());
GameImage.all = [];
GameImage._loaded = 0;
GameImage._pending = 0;
new GameImage("background", "http://res.cloudinary.com/dfhppjli0/image/upload/c_scale,w_2048/v1492045665/road_dwsmux.png");
new GameImage("hero", "http://res.cloudinary.com/dfhppjli0/image/upload/c_scale,w_32/v1491958999/car_p1k2hw.png");
new GameImage("monster", "http://res.cloudinary.com/dfhppjli0/image/upload/v1491958478/monster_rsm0po.png");
new GameImage("hero_other", "http://res.cloudinary.com/dfhppjli0/image/upload/v1492579967/car_03_ilt08o.png");
function distance(a, b) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
function degreeToRadian(degrees) {
return degrees * (Math.PI / 180);
}
function radianToDegree(radians) {
return radians * (180 / Math.PI);
}
function angleBetweenTwoPoints(p1, p2) {
return Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
}
var Actor = (function() {
function Actor() {
this.angle = 0;
}
Actor.prototype.main = function() {};
Actor.prototype.render = function(ctx) {
if (this.angle != 0) {
var rads = degreeToRadian(this.angle - 90);
ctx.translate(this.position.x + 0.5 * this.image.node.naturalWidth, this.position.y + 0.5 * this.image.node.naturalHeight);
ctx.rotate(rads);
ctx.drawImage(this.image.node, 0, 0);
ctx.rotate(-rads);
ctx.translate(-(this.position.x + 0.5 * this.image.node.naturalWidth), -(this.position.y + 0.5 * this.image.node.naturalHeight));
} else {
ctx.drawImage(this.image.node, this.position.x, this.position.y);
}
};
return Actor;
}());
var Monster = (function(_super) {
__extends(Monster, _super);
function Monster(position) {
var _this = _super.call(this) || this;
_this.position = position;
_this.image = GameImage.getImage("monster");
Monster.all.push(_this);
return _this;
}
return Monster;
}(Actor));
Monster.all = [];
var Car = (function(_super) {
__extends(Car, _super);
function Car(position, target) {
if (target === void 0) {
target = null;
}
var _this = _super.call(this) || this;
_this.position = position;
_this.target = target;
_this.hitCount = 0;
_this.image = GameImage.getImage("hero");
_this.speed = 10;
Car.all.push(_this);
return _this;
}
Car.prototype.main = function() {
var angle = angleBetweenTwoPoints(this.target.position, this.position);
var cos = Math.cos(degreeToRadian(angle)) * -1;
var sin = Math.sin(degreeToRadian(angle));
this.angle = angle;
this.position.x += cos * this.speed;
this.position.y -= sin * this.speed;
if (distance(this.position, this.target.position) < 10) {
this.target.position.x = Math.random() * mainCanvas.width;
this.target.position.y = Math.random() * mainCanvas.height;
this.hitCount++;
console.log("Hit!");
}
};
return Car;
}(Actor));
Car.all = [];
var background = GameImage.getImage("background");
var mainCanvas = document.body.appendChild(document.createElement("canvas"));
mainCanvas.width = background.node.naturalWidth;
mainCanvas.height = background.node.naturalHeight;
var ctx = mainCanvas.getContext("2d");
var monster1 = new Monster({
x: Math.random() * mainCanvas.width,
y: Math.random() * mainCanvas.height
});
var monster2 = new Monster({
x: Math.random() * mainCanvas.width,
y: Math.random() * mainCanvas.height
});
new Car({
x: Math.random() * mainCanvas.width,
y: Math.random() * mainCanvas.height
}, monster1);
new Car({
x: Math.random() * mainCanvas.width,
y: Math.random() * mainCanvas.height
}, monster2);
function main() {
ctx.drawImage(background.node, 0, 0);
for (var ci = 0; ci < Car.all.length; ci++) {
var c = Car.all[ci];
c.main();
c.render(ctx);
}
for (var mi = 0; mi < Monster.all.length; mi++) {
var m = Monster.all[mi];
m.main();
m.render(ctx);
}
requestAnimationFrame(main);
}
requestAnimationFrame(main);
})(Game || (Game = {}));
As long as there are not obstacles, this works fine.