How to animate multiple objects inside canvas? - javascript

I have made a canvas with multiple hollow circles. And I am trying to animate it. I tried to use the method requestAnimationFrame, but it was unsuccessful. And if possible, I would prefer trying to stick with this method.
To avoid confusion in my code, I have added as much as reasonable comments which explains how the codes works. Note, that my knowledge regarding Javascript is far from good hence the amount of comments.
Also: Outside of the ending script tag, I am showing the lines of codes I tried to use for animation but was unsuccessful in doing so.
See the code below:
body {
background-color: #000000;
}
canvas {
padding: 0;
margin: auto;
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #111416;
border: 10px solid blue;
border-style: double;
box-shadow: 0 0 20px 5px blue;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<link rel="stylesheet" href="canvas.css">
<canvas id="myCanvas" width="350" height="350"></canvas>
<script>
// Get id from the canvas element
var canvas = document.getElementById("myCanvas");
// Provide 2D rendering context for the drawing surface of canvas
var context = canvas.getContext("2d");
// Get width and height of the canvas element
var canvW = document.getElementById("myCanvas").width;
var canvH = document.getElementById("myCanvas").height;
// Create a random range of numbers
const randomRange = (min, max) => {
return Math.random () * (max - min) + min;
}
// Class = Create multiple objects with the same properties
class Point {
// Build the new object and pass parameters
constructor(x, y){
// Define properties
// "this" refers to the scope of the class
// class point contains propertie x and y
this.x = x;
this.y = y;
}
}
// Separate point from canvas
// Make dot a new entity
// Create another class
class Agent {
constructor(x, y){
// First property is position using x and y parameters
this.pos = new Point(x, y);
// Create points with random radius
this.radius = randomRange(4, 12);
}
// Create new method called draw
// Pass paramter as reference to the context
draw (context) {
// Draw the points
context.save();
context.translate(this.pos.x, this.pos.y);
context.beginPath();
context.arc(0, 0, this.radius, 0, Math.PI * 2);
context.lineWidth = 4;
context.strokeStyle = "blue";
context.stroke();
context.restore();
}
}
// Create empty array called
// Note to myself: Cannot be written before the initialization of class agent
const agents = [];
// Create 40 random points using a for loop
for (let i =0; i<40; i++){
const x = randomRange(0, canvW);
const y = randomRange(0, canvH);
// push adds new items to the end of an array
// push changes the length of an array
// push returns the new length
agents.push(new Agent (x, y));
// Call function for each element in the array
agents.forEach(agent => {
agent.draw(context);
});
}
</script>
<!--
// Define velocity of point
this.vel = new Point( randomRange(-1, 1), randomRange(-1, 1));
// Add velocity to the points
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
}
agent.update();
const animate = () => {}
requestAnimationFrame(animate);
};
animate();
-->
</body>
</html>

I changed a few lines and commented them with 'Info:'.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<link rel="stylesheet" href="canvas.css">
<canvas id="myCanvas" width="350" height="350"></canvas>
<script>
// Get id from the canvas element
var canvas = document.getElementById("myCanvas");
// Provide 2D rendering context for the drawing surface of canvas
var context = canvas.getContext("2d");
// Get width and height of the canvas element
var canvW = document.getElementById("myCanvas").width;
var canvH = document.getElementById("myCanvas").height;
// Create a random range of numbers
const randomRange = (min, max) => {
return Math.random () * (max - min) + min;
}
// Create empty array called
// Note to myself: Cannot be written before the initialization of class agent
// Info: It can, maybe something else was wrong? Although, you might want to keep it together.
const agents = [];
// Class = Create multiple objects with the same properties
class Point {
// Build the new object and pass parameters
constructor(x, y){
// Define properties
// "this" refers to the scope of the class
// class point contains propertie x and y
this.x = x;
this.y = y;
}
}
// Separate point from canvas
// Make dot a new entity
// Create another class
class Agent {
constructor(x, y){
// First property is position using x and y parameters
this.pos = new Point(x, y);
// Define velocity of point.
// Info: Moved velocity in class.
// Todo: Decide where to handle velocity. Set it once? Update it later?
this.vel = new Point(randomRange(-1, 1), randomRange(-1, 1));
// Create points with random radius
this.radius = randomRange(4, 12);
}
// Create new method called draw
// Pass paramter as reference to the context
draw (context) {
// Draw the points
// Info: Added fill to make individual elements visible
context.save();
context.translate(this.pos.x, this.pos.y);
context.beginPath();
context.arc(0, 0, this.radius, 0, Math.PI * 2);
context.lineWidth = 4;
context.strokeStyle = "blue";
context.fillStyle = "white";
context.fill();
context.stroke();
context.restore();
}
// Info: Moved update in class
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
}
}
// Create 40 random points using a for loop
for (let i =0; i<40; i++){
const x = randomRange(0, canvW);
const y = randomRange(0, canvH);
// push adds new items to the end of an array
// push changes the length of an array
// push returns the new length
agents.push(new Agent (x, y));
}
// Info: Whole drawing and update process can be handled here
const animate = () => {
agents.forEach(agent => {
agent.draw(context);
agent.update();
});
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>

Related

How to fit multiple rotating rectangle inside the canvas (Javascript)

as mentioned I am trying to fit a whole circle of rectangle inside a canvas, but as seen, I can only show a fourth of the circle. I made a basic html document with a canvas element. Relative to the size of the canvas element I made a single rectangel and centered it in the middle of the canvas. With that, I tried to make a for loop which should rotate the rectangle while making a full circle. But it didn't work.
body{
background-color: #000000;
}
canvas {
padding: 0;
margin: auto;
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: white;
border:1px solid
}
<html>
<body>
<link rel="stylesheet" href="canvas.css">
<canvas id="myCanvas" width="900" height="900" ></canvas>
<script>
// Get id from canvas element
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
// Change size of rectangle
var recWidth = 40
var recHeight = 40
// Position rectangle in the middle of the canvas
var xPos = (document.getElementById("myCanvas").width/2) - (recWidth/2);
var yPos = (document.getElementById("myCanvas").height/2) - (recHeight/2);
// Convert degree to radian
const degToRad = (degrees) => {
return degrees / 180 * Math.PI;
}
// Number of rectangles
const num = 36;
for (let i = 0; i<num; i++){
const slice = degToRad (360 / num);
const angle = slice * i;
context.fillStyle = 'green';
context.save();
context.rotate (angle);
context.beginPath();
context.rect(xPos,yPos,recWidth,recHeight);
context.fill();
context.restore();
}
</script>
</body>
</html>
I hope I'm not violating SO protocols by creating a separate answer,
but I have done so to avoid some clutter with the new code.
Here is a cleaned up version of your code.
Explanation:
Move the center of rotation to the center of the canvas:
context.translate(width/2, height/2)
Then specify the radius for drawing as a quarter of canvas dimension.
rad = xPos/2
Do the rotation one slice at a time, leaving it in place (no save/restore).
(incorrect statement removed)
<script>
// Get id from canvas element
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
context.font = "14pt Arial";
context.fillStyle = "green";
// Change size of rectangle
var recWidth = 30
var recHeight = 30
// Position rectangle in the middle of the canvas
var canvW = document.getElementById("myCanvas").width;
var canvH = document.getElementById("myCanvas").height;
var xPos = canvW/2;
var yPos = canvH/2;
var rad = xPos/2; // radius = 1/4 of canvas dimensions
context.translate(xPos,yPos); // rotate around the center
// Convert degree to radian
const degToRad = (degrees) => {
return degrees / 180 * Math.PI;
}
// Number of rectangles
const num = 36;
const slice = degToRad (360 / num);
const angle = slice;
// animates the drawing to help see what is going on
// by introducing 1/2 second delay between individual draws
function draw1Rect(i)
{
i++;
if (i < num) setTimeout("draw1Rect("+i+")", 500);
context.rotate (angle);
context.fillRect(rad,rad,recWidth,recHeight);
context.fillText(i, rad-30, rad-30);
}
draw1Rect(0);
</script>
The problem is that most of your squares are being drawn outside the boundaries of the canvas.
It appears the canvas rotates around (0, 0) rather than around the center.
Outside the loop:
// Use a different center and a smaller radius
var xPos = (document.getElementById("myCanvas").width/4) - (recWidth/4);
var yPos = (document.getElementById("myCanvas").height/4) - (recHeight/4);
// Move the center
context.translate(xPos * 2, yPos * 9/4);
context.font = "14pt Arial";
Inside the loop:
context.fill(); // existing line of code
context.fillText(i, xPos-30, yPos-30); // added
context.restore(); // existing
Also: instead of context.beginPath(), context.rect() and context.fill() you can use context.fillRect().

Splice (index,1) is removing more than one element from array

I am making a simple game in HTML Canvas. As a part of it, in the background i want to make falling stars which create the illusion of travelling. After the star reaches the end of the canvas i want to remove it. Each star is an instance of Star class, and depending on it's radius it has a different velocity. This is where problems start. When using constant velocity for every star, they disappear one by one like they should be. When velocity is changed, stars that "overtake" slower stars, when reached the end of the canvas, do not only dissapear themselves but also remove every star that was in array before them.
I have tried many solutions described below:
let canvas = document.querySelector("canvas");
let c = canvas.getContext('2d');
Star Class declaration:
class Star{
constructor(x, y, radius, color, velocity){
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.velocity = velocity
}
draw(){
c.globalCompositeOperation='destination-over'
c.beginPath()
c.arc(this.x, this.y, this.radius, 0, Math.PI*2, false);
c.fillStyle = this.color;
c.shadowColor= "white"
c.shadowBlur=12
c.fill();
c.shadowBlur=0
}
update(){
this.draw();
this.y = this.y + this.radius/2;
}
}
Creating stars and adding it to array
let stars = [];
function createStar(){
setInterval(()=>{
//Create random radius and X position
let randomRadius = Math.floor((Math.random()*5))
let randomXPosition = Math.floor((Math.random()*780)+15)
let velocity = 1;
stars.push(new Star(randomXPosition, -randomRadius, randomRadius, "white",velocity));
console.log("stars:"+ stars.length);
},300)
}
Below here I use a function calling itself to clear and refresh the star drawing. I have tried looping through stars array with forEach method, reversed loop (like in example below), I tried putting the if statement with splice function in seperate setTimeout(()=>{},0) or setTimeout(()=>{},10). I tried using the condition like
(forEach method removes stars, however the number of active stars do not remain more or less the same. It constantly slowly increases)
function animate(){
c.clearRect(0, 0, canvas.width, canvas.height);
for(let i = stars.length-1; i>=0; i--){
stars[i].update()
if (stars[i].y > canvas.height +stars[i].radius ){
stars.splice(stars[i], 1)
}
}
requestAnimationFrame(animate);
}
animate();
createStar();
I tried using the condition like:
if (stars[i].y > 5000 ){
stars.splice(stars[i], 1)
}
But it's not how it's supposed to be solved, because stars live for 5000 pixels longer,what makes game laggy.
Just to be clear on the problem.
If i generate 5 stars every 300ms and push the into the array called "stars" I get e.g.
[star1, star2, star3, star4, star5]
Lets say star1 and star 2 have small radius and they move slowly. Star3 is bigger therefore it moves faster and overtakes first two. When star3 reaches canvas.height + star[i].radius it disappear just over the canvas, but it also makes every star in array that was before star3 disappear (in this case it's star1 and star2).
This is my first post on stackoverflow. I apologise for all understatements in advance.
HTML Canvas
<body>
<div class="container">
<button class="logOutButton">Log Out</button>
<button class="topScores">Top 10</button>
<header>
<p class="hello">Hello <span id="spanName"></span></p>
<p class="topScore">Your TOP score: <span id="score"></span></p>
</header>
<div class="gameTitle">
<h2 class="title">Space Warrior</h2>
</div>
<canvas class="canvas" width="800" height="500"></canvas>
</div>
<script src="User.js"></script>
</body>
EDIT
I changed stars.splice(stars[i], 1) to stars.splice(i, 1) - not working
I tried adding another removal array like below but array stars just slowly gets bigger (even though some elements get removed)
var removeStar = [];
// stars.forEach((star,starIndex)=>{
let total = stars.length-1;
for(let i = total; i>=0; i--){
stars[i].update();
if (stars[i].y > canvas.height + stars[i].radius){
removeStar.push(i);
}
};
for(let i = removeStar.length; i>0; i--){
stars.splice(removeStar[i-1],1)
}
you wrote:
stars.splice(stars[i], 1)
first argument should be index which you want to remove ...just index stars.splice(i, 1)
..another problem is that you changing the array while looping within it, which is bad idea.
see this answer to very similar question:
https://stackoverflow.com/a/65725703/3054380
After fixing the bug mentioned above and adding minVelocity and star limiting condition (because you adding with interval and removing when star goes off the canvas - we need to limit in case interval goes faster) ...now everything looks good - working snippet below (added maxStars minVelocity)
let canvas = document.querySelector(".mycanvas");
let c = canvas.getContext('2d');
canvas.width = 300;
canvas.height = 150;
let maxStars = 60;
let minVelocity = 0.5;
let stars = [];
class Star {
constructor(x, y, radius, color, velocity) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.velocity = velocity
}
draw() {
c.globalCompositeOperation = 'destination-over'
c.beginPath()
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
c.fillStyle = this.color;
c.shadowColor = "white"
c.shadowBlur = 12
c.fill();
c.shadowBlur = 0
}
update() {
this.draw();
this.y = this.y + Math.max(minVelocity, this.velocity * this.radius / 2);
}
}
function createStar() {
if (stars.length < maxStars) {
//Create random radius and X position
let randomRadius = Math.floor((Math.random() * 5));
let randomXPosition = Math.floor((Math.random() * (canvas.width - 20)) + 10);
let velocity = 1;
stars.push(new Star(randomXPosition, -randomRadius, randomRadius, "white", velocity));
console.log("stars:" + stars.length);
}
}
function animate() {
c.clearRect(0, 0, canvas.width, canvas.height);
for (let i = stars.length - 1; i >= 0; i--) {
stars[i].update()
if (stars[i].y > canvas.height) {
stars.splice(i, 1)
}
}
requestAnimationFrame(animate);
}
setInterval(createStar, 50);
animate();
.mycanvas {
width: 300px;
height: 150px;
border: 1px solid #f00;
}
<body style="background-color:#000;color:#fff;">
<div class="container">
<canvas class="mycanvas"></canvas>
</div>
<script src="User.js"></script>
</body>
I think I found the issue causing your everexpanding array. It has nothing to do with your splice function, which is implemented incorrectly as webdev-dan mentioned in his answer.
follow this logic
The radius has been set with Math.floor((Math.random()*5)).This means the radius can be 0.
In your update method you are increasing y based on the radius in this.y = this.y + this.radius/2. So this is possibly changing y with 0.
In if (stars[i].y > 5000 ) you are removing values of y over 5000.
So if the radius is 0, y doesn't change, stars are never removed. Nor can you see them visually.
solution
You could guarantee a minimal speed of 1. this.y += Math.max(1, this.radius/2).
PS: I had to do quite a bit of refactoring to figure this out. You are writing your code in a too complex way. Several types of logic are mixed up.
You really want to separate out your rendering logic from management of the stars object.
It is also quite hard to mentally keep track of the stars array because you are modifying it from anywhere; inside for loops, with async code ( the interval ).
This is what I ended up while trying to clean up your code a bit: https://jsfiddle.net/5hk0vscg/1/ Hope it's useful. Note that it is not a full cleanup, but it's an improvement over your current code.

Javascript: can't represent sinusoidal graph

I want to make a box to move as a sinusoidal graph.
At the point where i am now i simply can't represent the box into the canvas. At the beginning I was able to, but after working out the trigonometry part the box disappeared and a get no error...
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<canvas id="canvas" width="600" height="300" style="background-color:red"></canvas>
<script type="text/javascript">
var canvas = document.querySelector("canvas");//isoute me document.getElementsByTagName()
var ctx = canvas.getContext("2d");
var can_width = canvas.width;
var can_height = canvas.height;
var x,y;
function PVector(x_,y_){
var y = [];
var x = [0, Math.PI/6, Math.PI/4, Math.PI/3, Math.PI/2, 2/3*Math.PI, 3/4*Math.PI,
5/6*Math.PI, Math.PI, 7/6*Math.PI, 5/4*Math.PI, 4/3*Math.PI, 3/2*Math.PI,
5/3*Math.PI, 7/4*Math.PI, 11/6*Math.PI, 2*Math.PI];
for (var i=0, len=x["length"]; i<len; i++){
var A;
A = Math.sin(x[i]);
y.push(A);
}console.log(y);console.log(x);
return{
x:x,
y:y
};
}
var Point = {
location : {x:0, y: can_height/2},//initial location
velocity : new PVector(x,y),
display : ctx.fillRect(can_width/2,can_height/2 , 25, 25),//initial position of the box
step : function(){
this.location.x += this.velocity.x;
this.location.y += this.velocity.y;
},
display : function(){
ctx.fillRect(this.location.x, this.location.y, 25, 25);
}
};
function update(){
Point["step"]();
ctx.clearRect(0,0, can_width, can_height);
Point["display"]();
window.setTimeout(update, 1000/30);
}
function init(){
update();
}
init();
</script>
</body>
Problem
In your PVector object you are returning Arrays for x and y, while you use them as values in the step() method. This will cause the entire array to be added as a string.
Solution
You need something that traverse that array. Here is an example, it may not be the result you're after, but it shows the principle which you need to apply:
// first note the return type here:
function PVector(x_,y_){
var y = [];
var x = [0, Math.PI/6, Math.PI/4, Math.PI/3, Math.PI/2, 2/3*Math.PI,
...snipped---
return {
x:x, // x and y are arrays
y:y
};
}
var Point = {
location: {
x: 0,
y: can_height / 2,
step: 0 // some counter to keep track of array position
}, //initial location
velocity: new PVector(x, y),
step: function () {
this.location.step++; // increase step for arrays
// using modulo will keep the step as a valid value within the array length:
// if step = 7 and length = 5, index will become 2 (sort of "wrap around")
var indexX = this.location.step % this.velocity.x.length;
var indexY = this.location.step % this.velocity.y.length
this.location.x += this.velocity.x[indexX];
this.location.y += this.velocity.y[indexY];
},
...
Updated fiddle
Tip: I would as Robin in his answer, recommend to simplify the sinus calculation. Sinus-tables are good when performance is needed and the browser can't keep up (ie. will thousands of objects), but in simpler scenario, direct calculation will work too.
If your goal is just to have a box moving in a sinusoidal graph, it can be done simpler.
This jsfiddle shows a slightly simpler example of a box moving in a sinusoidal graph where I just removed parts of your code and calculate the path with Math.sin and use time instead of precalculated values for x.
function update(){
time += 0.1;
ctx.clearRect(0,0, can_width, can_height);
x = time;
y = (can_height/2)+(can_height/2)*Math.sin(time);
console.log(x, y);
ctx.fillRect(x*16, y, 25, 25);
window.setTimeout(update, 1000/30);
}
The variables are modified to make it look ok on the canvas. You can edit the addition to time, and the altitude and base line for y, to fit your needs.
If you need to follow the specification in your code, look at the answer by Ken.
Since you want a sinusoidal move, it makes sense to use... the sin function.
The formula for a sinusoidal move is :
y = maxValue * sin ( 2 * PI * frequency * time ) ;
where the frequency is in Herz (== 'number of times per second') and time is in second.
Most likely you'll use Date.now(), so you'll have a time in millisecond, that you need to convert. Since the value of PI should not change in the near future, you can compute once the magic number
k = 2 * PI / 1000 = 0.006283185307179587
and the formula becomes :
y = sin( k * frequency * Date.now() );
Here's a simple example on how to use the formula :
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
var can_width = canvas.width;
var can_height = canvas.height;
// magic number
var k = 0.006283185307179587;
// oscillations per second
var frequency = 1/5;
// ...
var amplitude = can_width / 8;
function animate() {
ctx.clearRect(0,0,can_width, can_height);
ctx.fillStyle= '#000';
var y = amplitude * Math.sin ( k * frequency * Date.now() );
ctx.fillRect(can_width/2, can_height/2 + y, 20, 20 );
}
setInterval(animate, 30);
<canvas id="canvas" width="400" height="200" style="background-color:red"></canvas>

Randomize drawing of canvas sections

New to javascript (and programming in general) and I am trying to have different characters spawn in my web-game I am making which is represented as different sections in my sprite.
I was wondering how I could achieve this?
I tried to at least change the coordinates as the score would go higher but i couldn't crack it.
function Enemy(nX, nY) {
this.srcX = nX; //i was trying to sub
this.srcY = nY;
this.width = 172;
this.height = 68;
this.speed = 2;
this.drawX = Math.floor(Math.random() * 1000) + gameWidth;
this.drawY = Math.floor(Math.random() * 360);
this.rewardPoints = 5;
}
This is the other half
Enemy.prototype.draw = function () {
this.drawX -= this.speed;
ctxEnemy.drawImage(imgSprite,this.srcX,this.srcY,this.width,this.height,this.drawX,this.drawY,this.width,this.height);
this.checkEscaped();
};
You can see the full thing here. http://dev.yeahnah.tv/gina2/ (fair warning: it's pretty bad.)
Cheers!
How to efficiently spawn random sprites from a spritesheet
Store the position and dimensions of each sprite on a spritesheet in an object:
{x:50, y:15, width:170, height:200}
Then push all sprite definition objects into an array
// store x/y/width/height of each sprite in an object
// add each object to the sprites array
var sprites=[]
sprites.push({x:50,y:15,width:170,height:200});
sprites.push({x:265,y:10,width:140,height:200});
sprites.push({x:460,y:10,width:180,height:200});
sprites.push({x:10,y:385,width:180,height:180});
sprites.push({x:225,y:395,width:200,height:200});
sprites.push({x:445,y:305,width:200,height:160});
Then when you want a random sprite, just use Math.random to pull one out of the sprites array:
function spawnRandomSprite(){
// choose a random sprite and draw it
var sprite=sprites[parseInt(Math.random()*5)];
// draw the sprite by using the spritesheet position and dimensions in the object
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(spritesheet,
sprite.x,sprite.y,sprite.width,sprite.height,
0,0,sprite.width,sprite.height
);
}
You could also store a speed and reward points in the sprite object and use those to drive your animation and your scoring:
{ x:50, y:15, width:170, height:200, speed:3, rewardPoints:5 }
Here’s code and a Fiddle: http://jsfiddle.net/m1erickson/YzUmv/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; padding:20px;}
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// store x/y/width/height of each sprite in an object
// add each object to the sprites array
var sprites=[]
sprites.push({x:50,y:15,width:170,height:200});
sprites.push({x:265,y:10,width:140,height:200});
sprites.push({x:460,y:10,width:180,height:200});
sprites.push({x:10,y:385,width:180,height:180});
sprites.push({x:225,y:395,width:200,height:200});
sprites.push({x:445,y:305,width:200,height:160});
// choose a random sprite and draw it
function spawnRandomSprite(){
var sprite=sprites[parseInt(Math.random()*5)];
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(spritesheet,
sprite.x,sprite.y,sprite.width,sprite.height,
0,0,sprite.width,sprite.height
);
}
ctx.font="18pt Verdana";
ctx.fillText("Loading spritesheet...",20,20);
var spritesheet=document.createElement("img");
spritesheet.onload=function(){
spawnRandomSprite();
}
spritesheet.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/angryBirds.png";
$("#canvas").click(function(){ spawnRandomSprite(); });
}); // end $(function(){});
</script>
</head>
<body>
<p>Click on the canvas for a random sprite</p>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>

Draw an arrow on HTML5 Canvas between two objects

I'm working on concept maps application, which has a set of nodes and links. I have connected the links to nodes using the center of the node as reference. Since I have nodes with different size and shapes, it is not advisable to draw arrow-head for the link by specifying height or width of the shape. My approach is to draw a link, starting from one node, pixel by pixel till the next node is reached(here the nodes are of different color from that of the background), then by accessing the pixel value, I want to be able to decide the point of intersection of link and the node, which is actually the co-ordinate for drawing the arrow-head.
It would be great, if I could get some help with this.
Sample Code:
http://jsfiddle.net/9tUQP/4/
Here the green squares are nodes and the line starting from left square and entering into the right square is the link. I want the arrow-head to be drawn at the point of intersection of link and the right square.
I've created an example that does this. I use Bresenham's Line Algorithm to walk the line of whole canvas pixels and check the alpha at each point; whenever it crosses a 'threshold' point I record that as a candidate. I then use the first and last such points to draw an arrow (with properly-rotated arrowhead).
Here's the example: http://phrogz.net/tmp/canvas_shape_edge_arrows.html
Refresh the example to see a new random test case. It 'fails' if you have another 'shape' already overlapping one of the end points. One way to solve this would be to draw your shapes first to a blank canvas and then copy the result (drawImage) to the final canvas.
For Stack Overflow posterity (in case my site is down) here's the relevant code:
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>HTML5 Canvas Shape Edge Detection (for Arrow)</title>
<style type="text/css">
body { background:#eee; margin:2em 4em; text-align:center; }
canvas { background:#fff; border:1px solid #666 }
</style>
</head><body>
<canvas width="800" height="600"></canvas>
<script type="text/javascript">
var ctx = document.querySelector('canvas').getContext('2d');
for (var i=0;i<20;++i) randomCircle(ctx,'#999');
var start = randomDiamond(ctx,'#060');
var end = randomDiamond(ctx,'#600');
ctx.lineWidth = 2;
ctx.fillStyle = ctx.strokeStyle = '#099';
arrow(ctx,start,end,10);
function arrow(ctx,p1,p2,size){
ctx.save();
var points = edges(ctx,p1,p2);
if (points.length < 2) return
p1 = points[0], p2=points[points.length-1];
// Rotate the context to point along the path
var dx = p2.x-p1.x, dy=p2.y-p1.y, len=Math.sqrt(dx*dx+dy*dy);
ctx.translate(p2.x,p2.y);
ctx.rotate(Math.atan2(dy,dx));
// line
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(-len,0);
ctx.closePath();
ctx.stroke();
// arrowhead
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(-size,-size);
ctx.lineTo(-size, size);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Find all transparent/opaque transitions between two points
// Uses http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
function edges(ctx,p1,p2,cutoff){
if (!cutoff) cutoff = 220; // alpha threshold
var dx = Math.abs(p2.x - p1.x), dy = Math.abs(p2.y - p1.y),
sx = p2.x > p1.x ? 1 : -1, sy = p2.y > p1.y ? 1 : -1;
var x0 = Math.min(p1.x,p2.x), y0=Math.min(p1.y,p2.y);
var pixels = ctx.getImageData(x0,y0,dx+1,dy+1).data;
var hits=[], over=null;
for (x=p1.x,y=p1.y,e=dx-dy; x!=p2.x||y!=p2.y;){
var alpha = pixels[((y-y0)*(dx+1)+x-x0)*4 + 3];
if (over!=null && (over ? alpha<cutoff : alpha>=cutoff)){
hits.push({x:x,y:y});
}
var e2 = 2*e;
if (e2 > -dy){ e-=dy; x+=sx }
if (e2 < dx){ e+=dx; y+=sy }
over = alpha>=cutoff;
}
return hits;
}
function randomDiamond(ctx,color){
var x = Math.round(Math.random()*(ctx.canvas.width - 100) + 50),
y = Math.round(Math.random()*(ctx.canvas.height - 100) + 50);
ctx.save();
ctx.fillStyle = color;
ctx.translate(x,y);
ctx.rotate(Math.random() * Math.PI);
var scale = Math.random()*0.8 + 0.4;
ctx.scale(scale,scale);
ctx.lineWidth = 5/scale;
ctx.fillRect(-50,-50,100,100);
ctx.strokeRect(-50,-50,100,100);
ctx.restore();
return {x:x,y:y};
}
function randomCircle(ctx,color){
ctx.save();
ctx.beginPath();
ctx.arc(
Math.round(Math.random()*(ctx.canvas.width - 100) + 50),
Math.round(Math.random()*(ctx.canvas.height - 100) + 50),
Math.random()*20 + 10,
0, Math.PI * 2, false
);
ctx.fillStyle = color;
ctx.fill();
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
</script>
</body></html>

Categories

Resources