Good day,
I am generating some circles with colors, sizes and positions. All of this things randomly.
But, my problem is that I do not want them to collide, so that no circle is inside another, not even a little bit.
The logic explained in detail within the code, I would like to know why the failure and why the infinite loop.
The important functions are:
checkSeparation and setPositions
window.addEventListener("load", draw);
function draw() {
var canvas = document.getElementById("balls"), // Get canvas
ctx = canvas.getContext("2d"); // Context
canvas.width = document.body.clientWidth; // Set canvas width
canvas.height = document.documentElement.scrollHeight; // Height
var cW = canvas.width, cH = canvas.height; // Save in vars
ctx.fillStyle = "#fff022"; // Paint background
ctx.fillRect(0, 0, cW, cH); // Coordinates to paint
var arrayOfBalls = createBalls(); // create all balls
setPositions(arrayOfBalls, cW, cH);
arrayOfBalls.forEach(ball => { // iterate balls to draw
ctx.beginPath(); // start the paint
ctx.fillStyle = ball.color;
ctx.arc(ball.x, ball.y, ball.radius, 0, (Math.PI/180) * 360, false); // draw the circle
ctx.fill(); // fill
ctx.closePath(); // end the paint
});
}
function Ball() {
this.x = 0; // x position of Ball
this.y = 0; // y position of Ball
this.radius = Math.floor(Math.random() * ( 30 - 10 + 1) + 10);
this.color = "";
}
Ball.prototype.setColor = function(){
for(var j = 0, hex = "0123456789ABCDEF", max = hex.length,
random, str = ""; j <= 6; j++, random = Math.floor(Math.random() * max), str += hex[random])
this.color = "#" + str;
};
function random(val, min) {
return Math.floor(Math.random() * val + min); // Random number
}
function checkSeparation(value, radius, toCompare) {
var min = value - radius, // Min border of circle
max = value + radius; // Max border of circle
// Why ? e.g => x position of circle + this radius it will be its right edge
for(; min <= max; min++) {
if(toCompare.includes(min)) return false;
/*
Since all the positions previously obtained, I add them to the array, in order to have a reference when verifying the other positions and that they do NOT collide.
Here I check if they collide.
In the range of:
[pos x - its radius, pos x + its radius]
*/
}
return true; // If they never collided, it returns true
}
function createBalls() {
var maxBalls = 50, // number of balls
balls = []; // array of balls
for(var j = 0; j < maxBalls; j++) { // create 50 balls
var newBall = new Ball(); // create ball
newBall.setColor(); // set the ball color
balls.push(newBall); //push the ball to the array of balls
}
return balls; // return all balls to draw later
}
function setPositions(balls, canvasW, canvasH) {
var savedPosX = [], // to save x pos of balls
savedPosY = []; // to save y pos of balls
for(var start = 0, max = balls.length; start < max; start++) {
var current = balls[start], // current ball
randomX = random(canvasW, current.radius), // get random value for x pos
randomY = random(canvasH, current.radius); // get random value for y pos
if(checkSeparation(randomX, current.radius, savedPosX)) {
current.x = randomX; // If it position, along with your radio does not touch another circle, I add the position
} else {
// start--; continue;
console.log("X: The above code causes an infinite loop");
}
if(checkSeparation(randomY, current.radius, savedPosY)) {
current.y = randomY;
} else {
// start--; continue;
console.log("Y: The above code causes an infinite loop");
}
}
}
body,html {
margin: 0; border: 0; padding: 0; overflow: hidden;
}
<canvas id="balls"></canvas>
In your code, you test possible collisions by means of arrays of already used x and y positions, but you never add new positions to these arrays. You also check the x and y coordinates separately, which means you are really testing a collision of a bounding box.
Two circles collide when the distance between their centres is smaller than the sum of their radii, so you could use:
function collides(balls, n, x, y, r) {
for (let i = 0; i < n; i++) {
let ball = balls[i];
let dx = ball.x - x;
let dy = ball.y - y;
let dd = dx*dx + dy*dy;
let rr = r + ball.radius;
if (dd < rr * rr) return true;
}
return false;
}
function setPositions(balls, canvasW, canvasH) {
for (let i = 0, max = balls.length; i < max; i++) {
let ball = balls[i],
r = ball.radius,
maxTries = 20;
ball.x = -canvasW;
ball.y = -canvasH;
for (let tries = 0; tries = maxTries; tries++) {
let x = random(canvasW - 2*r, r),
y = random(canvasH - 2*r, r);
if (!collides(balls, i, x, y, r)) {
ball.x = x;
ball.y = y;
break;
}
}
}
}
This is reasonably fast for 50 balls, but will be slow if you have more balls. In that case, some spatial data structures can speed up the collision search.
You must also guard against the case that no good place can be found. The code above gives up after 20 tries and moves the ball outside the visible canvas. You can improve the chances of placing balls by sorting the balls by radius and plaing the large balls first.
Finally, you add one hex digit too many to your random colour. (That for loop, where everything happens in the loop control is horrible, by the way.)
Related
I'm trying to achieve that, everytime you type a different letter key, the lines of the letters 'merge' into eachother instead of just 'jumping' to the next letter like it's doing now. I'm looking into the lerp() function but i'm not sure how to apply this to my code. Can someone help me into the right direction? This is what i have untill now:
var redtown;
var fontSize = 500;
var myArray;
var r = 3;
function preload(){
redtown = loadFont('redtown.otf');
}
function setup(){
createCanvas(windowWidth,windowHeight);
textFont(redtown);
textSize(fontSize);
}
function draw(){
background(0);
myArray = redtown.textToPoints(key, width/2, 500, fontSize, {
sampleFactor:0.5
});
// text(key, width/2, height/2 );
for (var i = 0; i < myArray.length; i++) {
// ellipse(myArray[i].x, myArray[i].y, 10, 10)
push();
translate(myArray[i].x, myArray[i].y);
rotate(r);
r++;
stroke(255);
strokeWeight(1);
line(-10,-10,10,10,10);
frameRate(17);
pop();
}
}
Here is a snippet that transitions from one character to another by using textToPoints to get the points from the last two keys that have been pressed and then slides each point in the old character to its position in the new character.
It uses this formula to get the x and y positions of points along a line from the point in the old character to the point in the new character.
x = (1-t)*x+t*nextX;
y = (1-t)*y+t*nextY;
It also uses the spinning lines idea from the question to give the points some motion although it pins the line size to a constant.
rotate(r+=0.1);
line(-1,-1,1,1);
You can see it in action here Fonts Transition
var myFont;
var fontSize = 160;
var fontPoints =[];
var previousFontPoints = [];
var r = 0;
var oldKey = ' ';
function preload(){
myFont = loadFont('inconsolata.otf');
}
function setup(){
createCanvas(500, 500);
textFont(myFont);
textSize(fontSize);
frameRate(30);
stroke(255);
strokeWeight(1);
background(0);
}
function draw(){
if (oldKey != key){
previousFontPoints =
myFont.textToPoints(oldKey, width/10, height/2, fontSize, {
sampleFactor:1
});
oldKey = key;
fontPoints = myFont.textToPoints(key, width/10, height/2, fontSize, {
sampleFactor:1
});
t = 0.025;
}
t += .01;
if (fontPoints.length > 0 && t< 1.0){
background(0);
for (i = 0; i < fontPoints.length; i++){
let x = 0;
let y = 0;
// if we don't have enought points we will just float in from 0,0
let nextX = 0;
let nextY = 0;
push();
if (previousFontPoints.length > i){
x = previousFontPoints[i].x;
y = previousFontPoints[i].y;
// in case the new array does not have enough points
nextX = x;
nextY = y;
}
if (fontPoints.length > i){
nextX = fontPoints[i].x;
nextY = fontPoints[i].y;
}
x = (1-t)*x+t*nextX;
y = (1-t)*y+t*nextY;
translate(x, y);
rotate(r+=0.1);
line(-1,-1,1,1);
pop();
}
}
}
I want to randomly generate a color for each pixel in the canvas however my loop seems to crash the browser and I cannot figure out why?
I have tried reducing the loop to a static number (X = 10 x Y=100) and it seems to work. However it takes a few seconds for the result to be shown on the screen (despite my tests showing a run time of 10ms)
I am new to javascript/html so this could be an obvious solution, however any help is greatly appreciated.
//"use strict";
// SELECT THE CANVAS ELEMENT FROM THE HTML PAGE AND NAME IT 'WORLD'
let world = document.querySelector("canvas");
// SET THE CANVAS HEIGHT/WIDTH TO THE WINDOW INNER X/Y
world.width = window.innerWidth;
world.height = window.innerHeight;
let context = world.getContext("2d");
// GET PERFORMANCE TEST VALUE BEFORE LOOP
let t0 = performance.now();
let x=0;
let y=0;
// LOOP THROUGH THE CANVAS STARTING AT FIRST PIXEL OF TOP ROW MOVING TO THE LAST PIXEL OF THE TOP ROW AND FILL, THEN MOVE TO THE NEXT LINE AND REPEAT FILL FOR EACH ROW UNTIL CANVAS IS COLORED
for (y=0; y < world.height; y++)
{
//TODO: ADD RANDOM RGB COLOR TO STROKE
context.lineTo(x,y);
context.stroke();
for (x=0; x < 10; x++){
//TODO: ADD RANDOM RGB COLOR TO STROKE
context.lineTo(x,y);
context.stroke();
}
}
// GET PERFORMANCE TEST VALUE AFTER LOOP
let t1 = performance.now();
// LOG THE TOTAL MILLISECONDS OF THE LOOP
console.log("Total Time" + (t1-t0) + " milliseconds");
// GENERATE A RANDOM NUMBER BASED ON THE WINDOW INNER WIDTH
function getRandomX(){
return Math.random() * window.innerWidth;
}
// GENERATE A RANDOM NUMBER BASED ON THE WINDOW INNER HEIGHT
function getRandomY(){
return Math.random() * window.innerHeight;
}
// GENERATE A RANDOM NUMBER BETWEEN 0 - 255
function getRandomRGB(){
return Math.Random() * 255;
}
<canvas></canvas>
Does not load, crashes browser window
You’re redrawing the path every time you add a line to it. That’s quadratic and not necessary. (Quadratic is a problem when you have 20,000+ lines.)
Draw once:
let x = 0;
for (let y = 0; y < world.height; y++) {
context.lineTo(x, y);
for (x = 0; x < 10; x++) {
context.lineTo(x, y);
}
}
context.stroke();
And when you want to draw in multiple colours, start a new path every for each line:
let x = 0;
for (let y = 0; y < world.height; y++) {
//TODO: ADD RANDOM RGB COLOR TO STROKE
context.lineTo(x, y);
context.stroke();
context.beginPath();
context.moveTo(x, y);
for (x = 0; x < 10; x++) {
//TODO: ADD RANDOM RGB COLOR TO STROKE
context.lineTo(x, y);
context.stroke();
context.beginPath();
context.moveTo(x, y);
}
}
fillRect seems like a better choice for a canvas method to draw a pixel, though:
const getRandomColor = () =>
'#' + (Math.random() * 0x1000000 >>> 0).toString(16);
const world = document.getElementById('canvas');
const context = world.getContext('2d');
const start = performance.now();
for (let y = 0; y < world.height; y++) {
for (let x = 0; x < world.width; x++) {
context.fillStyle = getRandomColor();
context.fillRect(x, y, 1, 1);
}
}
console.log(performance.now() - start);
<canvas id="canvas"></canvas>
And finally, putting image data probably gives the best performance.
const world = document.getElementById('canvas');
const context = world.getContext('2d');
const start = performance.now();
const {width, height} = world;
const random32 = new Uint32Array(width * height);
for (let i = 0; i < random32.length; i++) {
random32[i] = Math.random() * 0x100000000 >>> 0;
}
const randomRGBA = new Uint8ClampedArray(random32.buffer);
for (let i = 3; i < randomRGBA.length; i += 4) {
randomRGBA[i] = 255;
}
const imageData = new ImageData(randomRGBA, width, height);
context.putImageData(imageData, 0, 0);
console.log(performance.now() - start);
<canvas id="canvas"></canvas>
(Fun fact: crypto.getRandomValues is faster than this in practice, but not a good choice.)
So I am trying to implement a concept of shooting star over an already drawn canvas of slowly moving stars. But I haven't found a way to do so. I tried implementing an array to make it look so but the trail isn't as efficient.
This code is as follows:
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var mouse = {
x : innerWidth/2,
y : innerHeight/2
};
var colors = [
'#3399CC',
'#67B8DE',
'#91C9E8',
'#B4DCED',
'#E8F8FF'
];
addEventListener('resize', function () {
canvas.width = innerWidth;
canvas.height = innerHeight;
init();
});
var isClicked = false;
addEventListener('click', function () {
mouse.x = event.clientX;
mouse.y = event.clientY;
isClicked = true;
});
function randomIntFromRange (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomColor (colors) {
return colors[Math.floor(Math.random() * colors.length)];
}
function Stars (x, y, radius, dy, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.dy = dy;
this.color = color;
this.draw = function () {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
c.shadowColor = this.color;
c.shadowBlur = 15;
c.shadowOffsetX = 0;
c.shadowOffsetY = 0;
c.fillStyle = this.color;
c.fill();
c.closePath();
}
this.update = function () {
if (this.y < -10) {
this.y = canvas.height + 10;
this.x = randomIntFromRange(this.radius, canvas.width);
}
this.y -= this.dy;
this.draw();
}
}
function ShootingStar (x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.draw = function () {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
c.shadowColor = "red";
c.shadowBlur = 15;
c.shadowOffsetX = 0;
c.shadowOffsetY = 0;
c.fillStyle = "red";
c.fill();
c.closePath();
}
this.update = function () {
this.x += 10;
this.y += 10;
this.draw();
}
}
let stars = [];
let shooting_star = [];
function init () {
stars = [];
for (var i = 0; i < 300; i++) {
var stars_radius = randomIntFromRange(2, 3);
var stars_x = randomIntFromRange(stars_radius, canvas.width);
var stars_y = randomIntFromRange(stars_radius, canvas.height);
var stars_dy = Math.random() / 6;
var color = randomColor(colors);
stars.push(new Stars(stars_x, stars_y, stars_radius, stars_dy, color));
}
}
function Explode () {
shooting_star = [];
var shooting_star_radius = 3;
var shooting_star_x = mouse.x;
var shooting_star_y = mouse.y;
for (var i = 0; i < 50; i++) {
shooting_star.push(new ShootingStar(shooting_star_x, shooting_star_y, shooting_star_radius));
if (shooting_star_radius > 0.2) {
shooting_star_radius -= .2;
}
var initiator = randomIntFromRange(-1, 1);
console.log(initiator);
shooting_star_x -= 3;
shooting_star_y -= 3;
}
}
function animate () {
requestAnimationFrame(animate);
c.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < stars.length; i++)
stars[i].update();
for (var i = 0; i < shooting_star.length; i++)
shooting_star[i].update();
if (isClicked == true) {
Explode();
isClicked = false;
}
}
init();
animate();
Here is the jsfiddle to it
https://jsfiddle.net/qjug4qdz/
I basically want the shooting star to come from a random location to the point where my mouse is clicked, but the trail is difficult to work with using an array.
Basic particles
For the particular effect you are looking for you can use a basic particle system.
As the shooting star moves you will drop a particle that starts with the star velocity and then slows and fades out.
The particle
You first start with the particle. I like to use Object.assign when creating objects but you can use any method you like, class, new, factory...
// This defines a particle and is copied to create new particles
const starDust = {
x : 0, // the current position
y : 0,
dx : 0, // delta x,y (velocity)
dy : 0,
drag : 0, // the rate that the particle slows down.
life : 0, // count down till particle is removed
age : 0, // the starting value of life
draw(){ // function to update and draw the particle
this.x += this.dx; // move it
this.y += this.dy;
this.dx *= this.drag; // slow it down
this.dy *= this.drag;
const unitLife = (this.life / this.age); // get the life left as a value
// from 0 to 1 where 0 is end
ctx.globalAlpha = unitLife; // set the alpha
ctx.beginPath();
ctx.arc(this.x,this.y,4,0,Math.PI); // draw the particle
this.life -= 1; // count down
return this.life > 0; // return true if still alive
}
Be memory aware.
A common mistake when creating particle systems is that people forget that creating and destroying objects will add a lot of work to javascripts memory management. The worst of which is GC (Garbage Collection). GC is a major source of lag and if you are wasteful with memory it will impact the quality of the animation. For simple particles it may not be noticeable, but you may want hundreds of complex particles spawning each frame. This is when GC realy hurts the animation.
Most Game engines reduce the GC impact by reusing objects rather than dereferencing and recreating. A common method is an object pool, where a second array holds objects that are no longer used. When a new object is needed then the pool is first checked, if there is an unused object, it is used, else a new object is created.
This way you never delete any particles, greatly reducing the GC workload, and preventing your animation from dropping frames (if you use a lot of particles)
Particle needs initializer
But you need to provide a way to re-initialize the object. Thus add the function init to the particle that will set it up to be used again
init(x,y,vx,vy){ // where x,y and velocity vx,vy of shooting star
this.x = x;
this.y = y;
this.dx = vx;
this.dy = vy;
// give a random age
this.age = (Math.random() * 100 + 60) | 0; // in frames and | 0 floors the value
this.life = this.age; // and set the life countdown
this.drag = Math.random() * 0.01 + 0.99; // the drag that slows the particle down
}
} // end of starDust object.
The arrays
To manage all the particles we create object that has arrays and methods for adding, creating and rendering the particles. In this case I will call it dust
const dust = {
particles : [], // array of active particles
pool : [], // array of unused particels
createParticle(particleDesc){ // creates a new particle from particleDesc
return Object.assign({},particleDesc);
},
add(x,y,vx,vy){ // where x,y and velocity vx,vy
var dust;
if(this.pool.length){ // are there any particles in the pool
dust = this.pool.pop(); // get one
}else{ // else there are no spare particles so create a new one
dust = this.createParticle(starDust);
}
dust.init(x,y,vx,vy); // init the particle
this.items.push(dust); // put it in the active particle array
return dust; // return it (sometimes you want to do something with it)
},
draw(){ // updates and draws all active particles
var i = 0;
while(i < this.items.length){ // iterate each particle in items
if(this.items[i].draw() === false){ // is it dead??
this.pool.push(this.items.splice(i,1)[0]); // if dead put in the pool for later
}else{ i ++ } // if not dead get index of next particle
}
}
}//end of dust object
Using the particle system
The simplest way to create a particle is to use a random number and set the chance of a particle being created every frame.
In your main loop
// assuming that the falling star is called star and has an x,y and dx,dy (delta)
if(star) { // only if there is a start to spawn from
// add a particle once every 10 frame (on average
if(Math.random() < 0.1) {
dust.add(star.x, star.y, star.dx, star.dy); // add some dust at the shooting starts position and speed
}
}
dust.draw(); // draw all particles
And that is it.
I have two questions, the first being how do I access the indexes within my array separately, because my console.log of [n][0] results in two values - x and y. Secondly, for the butterfly curve, https://en.wikipedia.org/wiki/Butterfly_curve_%28transcendental%29, how would I determine the values of t? and reiterate through a certain minimum and maximum. In need of logic support.
Here's my progress so far.
/*function drawButterFly(n){
c.beginPath();
console.log(n[2])
for (var i = 0; i < n.length; i++){
if (i === 0) {
c.moveTo();
} else {
c.lineTo();
}
c.stroke();
}
}*/
function butterFly() {
var r = 5;
var N = 3;
var value = [];
for (var a = 0.2; a < 2*Math.PI; a = a + 0.1){
value.push(a);
}
var t = value[Math.floor(Math.random()*value.length)];
var cos = r*Math.cos(t)*( (Math.exp(Math.cos(t))) - (2*Math.cos(4*t)) - (Math.sin(t/12)^5) );
var sin = r*Math.sin(t)*( (Math.exp(Math.cos(t))) - (2*Math.cos(4*t)) - (Math.sin(t/12)^5) );
var n = [];
for (var u = 0; u < N; u++){
var x = sin * -u;
var y = cos * -u;
n.push([x,y]);
}
drawButterFly(n);
}
Since you're pushing an array here: n.push([x,y]) you can access the x component of the first element with n[0][0] and the y component of the same element with n[0][1]
Example:
var n = [];
n.push( ["x", "y"] );
console.log( n[0][0] );
console.log( n[0][1] );
As for the useful values of t - in the image you've shown, you'll notice that the same butterfly is drawn several times at different sizes. To draw a complete butterfly, you need to use the range for t of [0..2pi]. If you want to draw two butterflies, you need to use the range [0..4pi]. That is it's cyclic over the same period that a circle is. Unlike a circle however, each cycle doesn't draw over the previous one.
Here's a quick and nasty example:
function byId(id) {
return document.getElementById(id);
}
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt) {
butterFly();
}
function butterFly() {
var pointArray = [];
var stepSize = 0.05; // ~125 steps for every 360°
var upperLimit = 4 * Math.PI;
var scale = 20;
for (var t = 0.0; t < upperLimit; t += stepSize) {
var xVal = Math.sin(t) * ((Math.exp(Math.cos(t))) - (2 * Math.cos(4 * t)) - (Math.pow(Math.sin(t / 12), 5)));
var yVal = Math.cos(t) * ((Math.exp(Math.cos(t))) - (2 * Math.cos(4 * t)) - (Math.pow(Math.sin(t / 12), 5)));
pointArray.push([scale * xVal, -scale * yVal]); // -1 value since screen-y direction is opposite direction to cartesian coords y
}
drawButterFly(pointArray);
}
function drawButterFly(pointArray) {
var can = byId('myCan');
var ctx = can.getContext('2d');
var originX, originY;
originX = can.width / 2;
originY = can.height / 2;
ctx.beginPath();
for (var i = 0; i < pointArray.length; i++) {
if (i === 0) {
ctx.moveTo(originX + pointArray[i][0], originX + pointArray[i][1]);
} else {
ctx.lineTo(originX + pointArray[i][0], originY + pointArray[i][1]);
}
}
ctx.closePath();
ctx.stroke();
}
canvas {
border: solid 1px red;
}
<canvas id='myCan' width='256' height='256' />
If I'm not mistaken, the Butterfly curve is given as a pair of parametric equations, meaning you increment t to get the next (x, y) points on your curve. In other words, your t is what you should be using in place of u in your code, and the range of values for t should be 0 .. 24*pi as that's the range in which sin(t / 12) has its unique values).
Here's a version that demonstrates the drawing of the curve to a canvas:
function getPoint(t, S, O) {
var cos_t = Math.cos(t);
var factor = Math.exp(cos_t) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5);
return {
x: S * Math.sin(t) * factor + O.x,
y: S * cos_t * factor + O.y
};
}
var canvas = document.getElementById("c");
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext("2d");
// First path
ctx.beginPath();
ctx.strokeStyle = 'blue';
var offset = {x:150, y:120};
var scale = 40;
var maxT = 24 * Math.PI;
var p = getPoint(0, scale, offset);
ctx.moveTo(p.x, canvas.height - p.y);
for (var t = 0.01; t <= maxT; t += 0.01) {
p = getPoint(t, scale, offset);
ctx.lineTo(p.x, canvas.height - p.y);
}
ctx.stroke();
#c {
border: solid 1px black;
}
<canvas id="c"></canvas>
One thing to note: canvases have y = 0 start at the top, so you need to inverse your y (i.e. canvas.height - y) to have your curve orient correctly.
UPDATE: Added animated version
As requested by royhowie, here's an animated version, using requestAnimationFrame:
function getPoint(t, S, O) {
var cos_t = Math.cos(t);
var factor = Math.exp(cos_t) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5);
return {
x: S * Math.sin(t) * factor + O.x,
y: S * cos_t * factor + O.y
};
}
var canvas = document.getElementById("c");
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext("2d");
var offset = {x:150, y:120};
var scale = 40;
var maxT = 24 * Math.PI;
var animationID;
var started = false;
var t = 0;
document.getElementById('start').addEventListener('click', function(e) {
e.preventDefault();
if (!started) {
animationID = requestAnimationFrame(animate);
started = true;
}
});
document.getElementById('pause').addEventListener('click', function(e) {
e.preventDefault();
if (started) {
cancelAnimationFrame(animationID);
started = false;
}
});
function animate() {
animationID = requestAnimationFrame(animate);
var p = getPoint(t, scale, offset);
if (t === 0) {
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.moveTo(p.x, canvas.height - p.y);
t += 0.01;
} else if (t < maxT) {
ctx.lineTo(p.x, canvas.height - p.y);
ctx.stroke();
t += 0.01;
} else {
cancelAnimationFrame(animationID);
}
}
#c {
border: solid 1px black;
}
<div>
<button id="start">Start</button>
<button id="pause">Pause</button>
</div>
<canvas id="c"></canvas>
Question 1
For an arbitrary integer i, let n[i] = [xi, yi]. xi can then be accessed via n[i][0] and yi via n[i][1]
Question 2
For values of t, I am sure you're gonna want to use sub-integer values, so I recommend using a constant increment value representing the "resolution" of your graph.
Let's call it dt. Also, I'd advise changing your variable names from single letters to something more descriptive, like min_t and max_t, and instead of n I'm going to call your array points.
function drawButterFly(points){
for (var i = 0, n = points.count; i < n; ++i) {
var x = points[i][0];
var y = points[i][1];
...
}
}
function butterFly(min_t, max_t, dt, r) {
var points = [];
for (var t = min_t; t < max_t; t+=dt){
var x = r*Math.sin(t)*...
var y = r*Math.cos(t)*...
points.push([x,y]);
}
drawButterFly(points, dt);
}
I'm not sure what the other loop inside of that function was for, but if you need it, you can adapt from the pattern above.
Usage example: butterFly(0, 10, 0.01, 3) -> t goes from 0 to 10 with an increment of 0.01, and r=3
Regarding your first question it's a better option to replace the multidimensional array containing the x and y coordinates with an object. Then when iterating over the array you can check for the object values.
So instead of:
n.push([x,y]);
you should do:
m.push({
'xPos' : x,
'yPos' : y
})
Later you can access this by m.xPos or m.yPos
Then you can access the x and y values by object literal names.
Regarding the second question: for a good pseudo code implementation of butterfly curves you might check Paul Burke site: http://paulbourke.net/geometry/butterfly/. So t in your case is:
t = i * 24.0 * PI / N;
As you see t is a parametric value which got incremented on each step when iterating over the array.
I'm trying to create a game in canvas with javascript where you control a spaceship and have it so that the canvas will translate and rotate to make it appear like the spaceship is staying stationary and not rotating.
Any help would be greatly appreciated.
window.addEventListener("load",eventWindowLoaded, false);
function eventWindowLoaded() {
canvasApp();
}
function canvasSupport() {
return Modernizr.canvas;
}
function canvasApp() {
if (!canvasSupport()) {
return;
}
var theCanvas = document.getElementById("myCanvas");
var height = theCanvas.height; //get the heigth of the canvas
var width = theCanvas.width; //get the width of the canvas
var context = theCanvas.getContext("2d"); //get the context
var then = Date.now();
var bgImage = new Image();
var stars = new Array;
bgImage.onload = function() {
context.translate(width/2,height/2);
main();
}
var rocket = {
xLoc: 0,
yLoc: 0,
score : 0,
damage : 0,
speed : 20,
angle : 0,
rotSpeed : 1,
rotChange: 0,
pointX: 0,
pointY: 0,
setScore : function(newScore){
this.score = newScore;
}
}
function Star(){
var dLoc = 100;
this.xLoc = rocket.pointX+ dLoc - Math.random()*2*dLoc;
this.yLoc = rocket.pointY + dLoc - Math.random()*2*dLoc;
//console.log(rocket.xLoc+" "+rocket.yLoc);
this.draw = function(){
drawStar(this.xLoc,this.yLoc,20,5,.5);
}
}
//var stars = new Array;
var drawStars = function(){
context.fillStyle = "yellow";
if (typeof stars !== 'undefined'){
//console.log("working");
for(var i=0;i< stars.length ;i++){
stars[i].draw();
}
}
}
var getDistance = function(x1,y1,x2,y2){
var distance = Math.sqrt(Math.pow((x2-x1),2)+Math.pow((y2-y1),2));
return distance;
}
var updateStars = function(){
var numStars = 10;
while(stars.length<numStars){
stars[stars.length] = new Star();
}
for(var i=0; i<stars.length; i++){
var tempDist = getDistance(rocket.pointX,rocket.pointY,stars[i].xLoc,stars[i].yLoc);
if(i == 0){
//console.log(tempDist);
}
if(tempDist > 100){
stars[i] = new Star();
}
}
}
function drawRocket(xLoc,yLoc, rWidth, rHeight){
var angle = rocket.angle;
var xVals = [xLoc,xLoc+(rWidth/2),xLoc+(rWidth/2),xLoc-(rWidth/2),xLoc-(rWidth/2),xLoc];
var yVals = [yLoc,yLoc+(rHeight/3),yLoc+rHeight,yLoc+rHeight,yLoc+(rHeight/3),yLoc];
for(var i = 0; i < xVals.length; i++){
xVals[i] -= xLoc;
yVals[i] -= yLoc+rHeight;
if(i == 0){
console.log(yVals[i]);
}
var tempXVal = xVals[i]*Math.cos(angle) - yVals[i]*Math.sin(angle);
var tempYVal = xVals[i]*Math.sin(angle) + yVals[i]*Math.cos(angle);
xVals[i] = tempXVal + xLoc;
yVals[i] = tempYVal+(yLoc+rHeight);
}
rocket.pointX = xVals[0];
rocket.pointY = yVals[0];
//rocket.yLoc = yVals[0];
//next rotate
context.beginPath();
context.moveTo(xVals[0],yVals[0])
for(var i = 1; i < xVals.length; i++){
context.lineTo(xVals[i],yVals[i]);
}
context.closePath();
context.lineWidth = 5;
context.strokeStyle = 'blue';
context.stroke();
}
var world = {
//pixels per second
startTime: Date.now(),
speed: 50,
startX:width/2,
startY:height/2,
originX: 0,
originY: 0,
xDist: 0,
yDist: 0,
rotationSpeed: 20,
angle: 0,
distance: 0,
calcOrigins : function(){
world.originX = -world.distance*Math.sin(world.angle*Math.PI/180);
world.originY = -world.distance*Math.cos(world.angle*Math.PI/180);
}
};
var keysDown = {};
addEventListener("keydown", function (e) {
keysDown[e.keyCode] = true;
}, false);
addEventListener("keyup", function (e) {
delete keysDown[e.keyCode];
}, false);
var update = function(modifier) {
if (37 in keysDown) { // Player holding left
rocket.angle -= rocket.rotSpeed* modifier;
rocket.rotChange = - rocket.rotSpeed* modifier;
//console.log("left");
}
if (39 in keysDown) { // Player holding right
rocket.angle += rocket.rotSpeed* modifier;
rocket.rotChange = rocket.rotSpeed* modifier;
//console.log("right");
}
};
var render = function (modifier) {
context.clearRect(-width*10,-height*10,width*20,height*20);
var dX = (rocket.speed*modifier)*Math.sin(rocket.angle);
var dY = (rocket.speed*modifier)*Math.cos(rocket.angle);
rocket.xLoc += dX;
rocket.yLoc -= dY;
updateStars();
drawStars();
context.translate(-dX,dY);
context.save();
context.translate(-rocket.pointX,-rocket.pointY);
context.translate(rocket.pointX,rocket.pointY);
drawRocket(rocket.xLoc,rocket.yLoc,50,200);
context.fillStyle = "red";
context.fillRect(rocket.pointX,rocket.pointY,15,5);
//context.restore(); // restores the coordinate system back to (0,0)
context.fillStyle = "green";
context.fillRect(0,0,10,10);
context.rotate(rocket.angle);
context.restore();
};
function drawStar(x, y, r, p, m)
{
context.save();
context.beginPath();
context.translate(x, y);
context.moveTo(0,0-r);
for (var i = 0; i < p; i++)
{
context.rotate(Math.PI / p);
context.lineTo(0, 0 - (r*m));
context.rotate(Math.PI / p);
context.lineTo(0, 0 - r);
}
context.fill();
context.restore();
}
// the game loop
function main(){
requestAnimationFrame(main);
var now = Date.now();
var delta = now - then;
update(delta / 1000);
//now = Date.now();
//delta = now - then;
render(delta / 1000);
then = now;
// Request to do this again ASAP
}
var w = window;
var requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame;
//start the game loop
//gameLoop();
//event listenters
bgImage.src = "images/background.jpg";
} //canvasApp()
Origin
When you need to rotate something in canvas it will always rotate around origin, or center for the grid if you like where the x and y axis crosses.
You may find my answer here useful as well
By default the origin is in the top left corner at (0, 0) in the bitmap.
So in order to rotate content around a (x,y) point the origin must first be translated to that point, then rotated and finally (and usually) translated back. Now things can be drawn in the normal order and they will all be drawn rotated relative to that rotation point:
ctx.translate(rotateCenterX, rotateCenterY);
ctx.rotate(angleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
Absolute angles and positions
Sometimes it's easier to keep track if an absolute angle is used rather than using an angle that you accumulate over time.
translate(), transform(), rotate() etc. are accumulative methods; they add to the previous transform. We can set absolute transforms using setTransform() (the last two arguments are for translation):
ctx.setTransform(1, 0, 0, 1, rotateCenterX, rotateCenterY); // absolute
ctx.rotate(absoluteAngleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
The rotateCenterX/Y will represent the position of the ship which is drawn untransformed. Also here absolute transforms can be a better choice as you can do the rotation using absolute angles, draw background, reset transformations and then draw in the ship at rotateCenterX/Y:
ctx.setTransform(1, 0, 0, 1, rotateCenterX, rotateCenterY);
ctx.rotate(absoluteAngleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
// update scene/background etc.
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transforms
ctx.drawImage(ship, rotateCenterX, rotateCenterY);
(Depending on orders of things you could replace the first line here with just translate() as the transforms are reset later, see demo for example).
This allows you to move the ship around without worrying about current transforms, when a rotation is needed use the ship's current position as center for translation and rotation.
And a final note: the angle you would use for rotation would of course be the counter-angle that should be represented (ie. ctx.rotate(-angle);).
Space demo ("random" movements and rotations)
The red "meteors" are dropping in one direction (from top), but as the ship "navigates" around they will change direction relative to our top view angle. Camera will be fixed on the ship's position.
(ignore the messy part - it's just for the demo setup, and I hate scrollbars... focus on the center part :) )
var img = new Image();
img.onload = function() {
var ctx = document.querySelector("canvas").getContext("2d"),
w = 600, h = 400, meteors = [], count = 35, i = 0, x = w * 0.5, y, a = 0, a2 = 0;
ctx.canvas.width = w; ctx.canvas.height = h; ctx.fillStyle = "#555";
while(i++ < count) meteors.push(new Meteor());
(function loop() {
ctx.clearRect(0, 0, w, h);
y = h * 0.5 + 30 + Math.sin((a+=0.01) % Math.PI*2) * 60; // ship's y and origin's y
// translate to center of ship, rotate, translate back, render bg, reset, draw ship
ctx.translate(x, y); // translate to origin
ctx.rotate(Math.sin((a2+=0.005) % Math.PI) - Math.PI*0.25); // rotate some angle
ctx.translate(-x, -y); // translate back
ctx.beginPath(); // render some moving meteors for the demo
for(var i = 0; i < count; i++) meteors[i].update(ctx); ctx.fill();
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transforms
ctx.drawImage(img, x - 32, y); // draw ship as normal
requestAnimationFrame(loop); // loop animation
})();
};
function Meteor() { // just some moving object..
var size = 5 + 35 * Math.random(), x = Math.random() * 600, y = -200;
this.update = function(ctx) {
ctx.moveTo(x + size, y); ctx.arc(x, y, size, 0, 6.28);
y += size * 0.5; if (y > 600) y = -200;
};
}
img.src = "http://i.imgur.com/67KQykW.png?1";
body {background:#333} canvas {background:#000}
<canvas></canvas>