Im currently in the process of animation some laser effects on the canvas for the purpose of making my game a bit more enjoyable.
For the purpose of that, i require the drawing and animation of "laser" weapons fire, something along the lines of Star Wars.
So far, im basicly only drawing a short line in red or blue and then drawing a thinner, white, line on top of it, so it gives the impression of a gradient.
I also use linecap = "round";
my current code:
function drawProjectile(weapon, ox, oy, x, y){
var trailEnd = getPointInDirection(weapon.projSize*-2, getAngleFromTo({x: ox, y: oy}, {x: x, y: y}), x, y);
fxCtx.lineCap = "round";
fxCtx.beginPath(); //the background, wider beam
fxCtx.moveTo(x+cam.o.x, y+cam.o.y);
fxCtx.lineTo(trailEnd.x+cam.o.x, trailEnd.y+cam.o.y);
fxCtx.closePath();
fxCtx.strokeStyle = weapon.animColor //blue or red
fxCtx.lineWidth = weapon.projSize;
fxCtx.stroke();
fxCtx.globalAlpha = 0.5; // the inner, thin, white beam
fxCtx.strokeStyle = "white";
fxCtx.lineWidth = 2;
fxCtx.stroke();
fxCtx.globalAlpha = 1;
fxCtx.lineCap = "butt";
}
Can someone advice how to improve my laser beam effect ?
Using canvas as images, glows, and more.
The demo below creates all the graphics it needs as offscreen canvases. The background is also drawn on to leave a burn mark when a laser hits the ground.
Lasers
Laser shots are 3 layered ctx.drawImage calls. The first 2 are glow with ctx.globalCompositeOperation = "lighter" . One has fixed alpha, the second has a random alpha. The last is drawn ctx.globalCompositeOperation = "source-over" and is just an image of a line.
There are 4 images (canvas) for the laaser called laserRed, laserGRed, laserGreen, and laserGGreen. the ones with the extra G are the laser glow.
When the laser shot is at the end I draw 4 frames of expanding glow from pre- rendered image. In the first of the 4 frames I draw to the background canvas leaving a burn mark.
Pre rendered images
All the graphics used are rendered in the function onResize which is called on resize from the boilerplate code.
The function display is called once every frame and handles all the animation.
There is an object called imageTools that has some helper functions to make the coding a little simplier. var image = imageTools.createImage(width,height) creates a canvas. The image has the context attached image.ctx so you can draw to it just like any canvas. You can then draw that image onto the global canvas with imageTools.drawImage(image,x,y,scale,rotation,alpha) The image is drawn at it center point with a scale, rotation, and alpha.
The bullets use an object pool so that the GC (Garbage collection) does not interfere to much.
I did not set any limits so those with high res devices or those with low end machines may see some slowdown. If its a problem OP you can reduce the bullet count by making them go a little faster, you can also reduce the rendering to just two or one layer. But this is a lot faster than if you were rendering with canvas vector calls, shadows, etc...
I will leave the rest for you to work out. Its a bit sparse in the comments but I did not have much time.
There is also some boilerplate at the bottom that is not very readable.
Left click to start the war.
/*************************************************************************************
* Called from boilerplate code and is debounced by 100ms
* Creates all the images used in the demo.
************************************************************************************/
var onResize = function(){
// create a background as drawable image
background = imageTools.createImage(canvas.width,canvas.height);
// create tile image
tile = imageTools.createImage(64,64);
tile.ctx.fillStyle = imageTools.createGradient(ctx,"linear",0,0,64,64,["#555","#666"]);
tile.ctx.fillRect(0,0,64,64);
tile.ctx.fillStyle = "#333"; // add colour
tile.ctx.globalCompositeOperation = "lighter";
tile.ctx.fillRect(0,0,62,2);
tile.ctx.fillRect(0,0,2,62);
tile.ctx.fillStyle = "#AAA"; // multiply colour to darken
tile.ctx.globalCompositeOperation = "multiply";
tile.ctx.fillRect(62,1,2,62);
tile.ctx.fillRect(1,62,62,2);
for(var y = -32; y < canvas.height; y += 64 ){
for(var x = -32; x < canvas.width; x += 64 ){
background.ctx.drawImage(tile,x,y);
}
}
background.ctx.globalCompositeOperation = "multiply"; // setup for rendering burn marks
burn = imageTools.createImage(flashSize/2,flashSize/2);
burn.ctx.fillStyle = imageTools.createGradient(ctx,"radial",flashSize/4,flashSize/4,0,flashSize/4,["#444","#444","#333","#000","#0000"]);
burn.ctx.fillRect(0,0,flashSize/2,flashSize/2);
glowRed = imageTools.createImage(flashSize,flashSize);
glowRed.ctx.fillStyle = imageTools.createGradient(ctx,"radial",flashSize/2,flashSize/2,0,flashSize/2,["#855F","#8000"]);
// #855F is non standard colour last digit is alpha
// 8,8 is ceneter 0 first radius 8 second
glowRed.ctx.fillRect(0,0,flashSize,flashSize);
glowGreen = imageTools.createImage(flashSize,flashSize);
glowGreen.ctx.fillStyle = imageTools.createGradient(ctx,"radial",flashSize/2,flashSize/2,0,flashSize/2,["#585F","#0600"]);
// #855F is non standard colour last digit is alpha
// 8,8 is ceneter 0 first radius 8 second
glowGreen.ctx.fillRect(0,0,flashSize,flashSize);
// draw the laser
laserLen = 32;
laserWidth = 4;
laserRed = imageTools.createImage(laserLen,laserWidth);
laserGreen = imageTools.createImage(laserLen,laserWidth);
laserRed.ctx.lineCap = laserGreen.ctx.lineCap = "round";
laserRed.ctx.lineWidth = laserGreen.ctx.lineWidth = laserWidth;
laserRed.ctx.strokeStyle = "#F33";
laserGreen.ctx.strokeStyle = "#3F3";
laserRed.ctx.beginPath();
laserGreen.ctx.beginPath();
laserRed.ctx.moveTo(laserWidth/2 + 1,laserWidth/2);
laserGreen.ctx.moveTo(laserWidth/2 + 1,laserWidth/2);
laserRed.ctx.lineTo(laserLen - (laserWidth/2 + 1),laserWidth/2);
laserGreen.ctx.lineTo(laserLen - (laserWidth/2 + 1),laserWidth/2);
laserRed.ctx.stroke();
laserGreen.ctx.stroke();
// draw the laser glow FX
var glowSize = 8;
laserGRed = imageTools.createImage(laserLen + glowSize * 2,laserWidth + glowSize * 2);
laserGGreen = imageTools.createImage(laserLen + glowSize * 2,laserWidth + glowSize * 2);
laserGRed.ctx.lineCap = laserGGreen.ctx.lineCap = "round";
laserGRed.ctx.shadowBlur = laserGGreen.ctx.shadowBlur = glowSize;
laserGRed.ctx.shadowColor = "#F33"
laserGGreen.ctx.shadowColor = "#3F3";
laserGRed.ctx.lineWidth = laserGGreen.ctx.lineWidth = laserWidth;
laserGRed.ctx.strokeStyle = "#F33";
laserGGreen.ctx.strokeStyle = "#3F3";
laserGRed.ctx.beginPath();
laserGGreen.ctx.beginPath();
laserGRed.ctx.moveTo(laserWidth/2 + 1 + glowSize,laserWidth/2 + glowSize);
laserGGreen.ctx.moveTo(laserWidth/2 + 1 + glowSize,laserWidth/2 + glowSize);
laserGRed.ctx.lineTo(laserLen + glowSize * 2 - (laserWidth/2 + 1 + glowSize),laserWidth/2 + glowSize);
laserGGreen.ctx.lineTo(laserLen + glowSize * 2 - (laserWidth/2 + 1 + glowSize),laserWidth/2 + glowSize);
laserGRed.ctx.stroke();
laserGGreen.ctx.stroke();
readyToRock = true;
}
var flashSize = 16;
const flashBrightNorm = 4 * (flashSize/2) * (flashSize/2) * Math.PI; // area of the flash
var background,tile,glowRed,glowGreen,grad, laserGreen,laserRed,laserGGreen,laserGRed,readyToRock,burn;
readyToRock = false;
/*************************************************************************************
* create or reset a bullet
************************************************************************************/
function createShot(x,y,xx,yy,speed,type,bullet){ // create a bullet object
if(bullet === undefined){
bullet = {};
}
var nx = xx-x; // normalise
var ny = yy-y;
var dist = Math.sqrt(nx*nx+ny*ny);
nx /= dist;
ny /= dist;
bullet.x = x;
bullet.y = y;
bullet.speed = speed;
bullet.type = type;
bullet.xx = xx;
bullet.yy = yy;
bullet.nx = nx; // normalised vector
bullet.ny = ny;
bullet.rot = Math.atan2(ny,nx); // will draw rotated so get the rotation
bullet.life = Math.ceil(dist/speed); // how long to keep alive
return bullet;
}
// semi static array with object pool.
var bullets=[]; // array of bullets
var bulletPool=[]; // array of used bullets. Use to create new bullets this stops GC messing with frame rate
const BULLET_TYPES = {
red : 0,
green : 1,
}
/*************************************************************************************
* Add a bullet to the bullet array
************************************************************************************/
function addBullet(xx,yy,type){
var bullet,x,y;
if(bulletPool.length > 0){
bullet = bulletPool.pop(); // get bullet from pool
}
if(type === BULLET_TYPES.red){
x = canvas.width + 16 + 32 * Math.random();
y = Math.random() * canvas.height;
}else if(type === BULLET_TYPES.green){
x = - 16 - 32 * Math.random();
y = Math.random() * canvas.height;
}
// randomise shoot to position
var r = Math.random() * Math.PI * 2;
var d = Math.random() * 128 + 16;
xx += Math.cos(r)* d;
yy += Math.sin(r)* d;
bullets[bullets.length] = createShot(x,y,xx,yy,16,type,bullet);
}
/*************************************************************************************
* update and draw bullets
************************************************************************************/
function updateDrawAllBullets(){
var i,img,imgGlow;
for(i = 0; i < bullets.length; i++){
var b = bullets[i];
b.life -= 1;
if(b.life <= 0){ // bullet end remove it and put it in the pool
bulletPool[bulletPool.length] = bullets.splice(i,1)[0];
i--; // to stop from skipping a bullet
}else{
if(b.life < 5){
if(b.life===4){
b.x += b.nx * b.speed * 0.5; // set to front of laser
b.y += b.ny * b.speed * 0.5;
var scale = 0.9 + Math.random() *1;
background.ctx.setTransform(scale,0,0,scale,b.x,b.y);
background.ctx.globalAlpha = 0.1 + Math.random() *0.2;;
background.ctx.drawImage(burn,-burn.width /2 ,-burn.height/2);
}
if(b.type === BULLET_TYPES.red){
img = glowRed;
}else{
img = glowGreen;
}
ctx.globalCompositeOperation = "lighter";
imageTools.drawImage(img,b.x,b.y,(4-b.life)*(4-b.life),b.rot,1);//b.life/4);
imageTools.drawImage(img,b.x,b.y,4,b.rot,b.life/4);
ctx.globalCompositeOperation = "source-over";
}else{
b.x += b.nx * b.speed;
b.y += b.ny * b.speed;
if(b.type === BULLET_TYPES.red){
img = laserRed;
imgGlow = laserGRed;
}else{
img = laserGreen;
imgGlow = laserGGreen;
}
ctx.globalCompositeOperation = "lighter";
imageTools.drawImage(imgGlow,b.x,b.y,1,b.rot,1);
imageTools.drawImage(imgGlow,b.x,b.y,2,b.rot,Math.random()/2);
ctx.globalCompositeOperation = "source-over";
imageTools.drawImage(img,b.x,b.y,1,b.rot,1);
}
}
}
}
/*************************************************************************************
* Main display loop
************************************************************************************/
function display() {
if(readyToRock){
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.drawImage(background,0,0);
ctx.globalCompositeOperation = "source-over";
if(mouse.buttonRaw & 1){
addBullet(mouse.x,mouse.y,BULLET_TYPES.red);
addBullet(mouse.x,mouse.y,BULLET_TYPES.green);
}
updateDrawAllBullets();
}
}
/*************************************************************************************
* Tools for creating canvas images and what not
************************************************************************************/
var imageTools = (function () {
// This interface is as is. No warenties no garenties, and NOT to be used comercialy
var workImg,workImg1,keep; // for internal use
var xdx,xdy,spr; // static vars for drawImage and drawSprite
keep = false;
var tools = {
canvas : function (width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage : function (width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
drawImage : function(image, x, y, scale, ang, alpha) {
ctx.globalAlpha = alpha;
xdx = Math.cos(ang) * scale;
xdy = Math.sin(ang) * scale;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y);
ctx.drawImage(image, -image.width/2,-image.height/2);
},
hex2RGBA : function(hex){ // Not CSS colour as can have extra 2 or 1 chars for alpha
// #FFFF & #FFFFFFFF last F and FF are the alpha range 0-F & 00-FF
if(typeof hex === "string"){
var str = "rgba(";
if(hex.length === 4 || hex.length === 5){
str += (parseInt(hex.substr(1,1),16) * 16) + ",";
str += (parseInt(hex.substr(2,1),16) * 16) + ",";
str += (parseInt(hex.substr(3,1),16) * 16) + ",";
if(hex.length === 5){
str += (parseInt(hex.substr(4,1),16) / 16);
}else{
str += "1";
}
return str + ")";
}
if(hex.length === 7 || hex.length === 8){
str += parseInt(hex.substr(1,2),16) + ",";
str += parseInt(hex.substr(3,2),16) + ",";
str += parseInt(hex.substr(5,2),16) + ",";
if(hex.length === 5){
str += (parseInt(hex.substr(7,2),16) / 255).toFixed(3);
}else{
str += "1";
}
return str + ")";
}
return "rgba(0,0,0,0)";
}
},
createGradient : function(ctx, type, x, y, xx, yy, colours){ // Colours MUST be array of hex colours NOT CSS colours
// See this.hex2RGBA for details of format
var i,g,c;
var len = colours.length;
if(type.toLowerCase() === "linear"){
g = ctx.createLinearGradient(x,y,xx,yy);
}else{
g = ctx.createRadialGradient(x,y,xx,x,y,yy);
}
for(i = 0; i < len; i++){
c = colours[i];
if(typeof c === "string"){
if(c[0] === "#"){
c = this.hex2RGBA(c);
}
g.addColorStop(Math.min(1,i / (len -1)),c); // need to clamp top to 1 due to floating point errors causes addColorStop to throw rangeError when number over 1
}
}
return g;
},
};
return tools;
})();
// CODE FROM HERE DOWN IS SUPPORT CODE AN HAS LITTLE TO DO WITH THE ANSWER
//==================================================================================================
// The following code is support code that provides me with a standard interface to various forums.
// It provides a mouse interface, a full screen canvas, and some global often used variable
// like canvas, ctx, mouse, w, h (width and height), globalTime
// This code is not intended to be part of the answer unless specified and has been formated to reduce
// display size. It should not be used as an example of how to write a canvas interface.
// By Blindman67
if(typeof onResize === "undefined"){
window["onResize"] = undefined; // create without the JS parser knowing it exists.
// this allows for it to be declared in an outside
// modal.
}
const RESIZE_DEBOUNCE_TIME = 100;
var w, h, cw, ch, canvas, ctx, mouse, createCanvas, resizeCanvas, setGlobals, globalTime = 0, resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = window.innerWidth-2;
canvas.height = window.innerHeight-2;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
mouse.updateBounds();
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.clientX - m.bounds.left;
m.y = e.clientY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
if ((m.buttonRaw & 2) && m.crashRecover !== null) {
if (typeof m.crashRecover === "function") {
setTimeout(m.crashRecover, 0);
}
}
e.preventDefault();
}
m.updateBounds = function () {
if (m.active) {
m.bounds = m.element.getBoundingClientRect();
}
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
} else {
throw new TypeError("mouse.addCallback argument must be a function");
}
}
m.start = function (element, blockContextMenu) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.blockContextMenu = blockContextMenu === undefined ? false : blockContextMenu;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
if (m.blockContextMenu === true) {
m.element.addEventListener("contextmenu", preventDefault, false);
}
m.active = true;
m.updateBounds();
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
if (m.contextMenuBlocked === true) {
m.element.removeEventListener("contextmenu", preventDefault);
}
m.element = m.callbacks = m.contextMenuBlocked = undefined;
m.active = false;
}
}
return mouse;
})();
// Clean up. Used where the IDE is on the same page.
var done = function () {
window.removeEventListener("resize", resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = undefined;
}
resizeCanvas();
mouse.start(canvas, true);
mouse.crashRecover = done;
window.addEventListener("resize", resizeCanvas);
function update(timer) { // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
You can add a shadow for a glowing effect
fxCtx.shadowBlur = 10;
fxCtx.shadowColor = '#FD0100';
but i think thats all, bacause your laser is so small that a real gradient would not make sense.
Use some nice colors like #FEF1BA insted of white for the red laser and thats it
I've been trying to put together a simple maze game using HTML5 and Javascript. I can successfully load the HTML and CSS content onto the page, but no matter what I try, I can't get the JS to load. It's definitely saved as a .html file and i've only been using Sublime text to put it together (but I wouldn't have thought that would have an affect anyway). Just a bit stumped really, so I thought it must be something I've missed in my code. I wasn't sure if I've missed something?
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title> Maze Game </title>
</head>
<style>
canvas {
border: 8px double navy;
background: white;
}
img {
display: none;
}
button {
padding: 3px;
}
</style>
<body>
<canvas id="canvas"> </canvas>
<img id="sprite" src="sprite.png">
<script>
//these define the global variables for the canvas and the drawing context
var canvas;
var context;
var x = 0;
var y = 0; //positioning of the sprite
var dx = 0;
var dy = 0; //momentum of the sprite at start
window.onload = function() {
//setting up the canvas
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
//Draws the maze background
drawMaze("maze.png", 268, 5);
//On key press, run the following function
window.onkeydown = processKey;
};
var x = 0;
var y = 0;
function drawMaze(mazeFile, Xstart, Ystart) {
//This loads the maze picture in
dx = 0;
dy = 0; //if the face is already moving, stop it
var imgMaze = new Image();
imgMaze.onLoad = function() {
canvas.width = imgMaze.width;
canvas.height = imgMaze.height;
//Draws the maze onto the canvas
context.drawImage(imgMaze, 0, 0);
//draws the sprite and positions
x = Xstart;
y = Ystart;
var imgSprite = document.getElementById("sprite");
context.drawImage(imgSprite, x, y);
context.stroke();
//sets a short timer for the next frame to be drawn in (10ms)
setTimeout("drawFrame()", 10);
};
imgMaze.src = mazeFile;
}
function processKey(e) { //e needs to be used for event handling
//stop the sprite if it's already moving - enables collision
var dx = 0;
var dy = 0;
//condition for the Up arrow being pressed
if (e.keyCode == 38) {
dy = -1;
}
//condition for the Left arrow being pressed
if (e.keyCode == 37) {
dx = -1;
}
//condition for the Down arrow being pressed
if (e.keyCode == 40) {
dy = 1;
}
//condition for the Right arrow being pressed
if (e.keyCode == 39) {
dx = 1;
}
}
function drawFrame() {
if (dx != 0 || dy != 0) {
context.beginPath();
context.fillStyle = "rgb(254,244,207)";
context.rect(x, y, 15, 15);
context.fill
x += dx;
y += dy;
if (checkForCollision()) {
(dx/y = 0)
x -= dx;
y -= dy;
dx = 0;
dy = 0;
}
//Now we can finally draw the sprite!
var imgSprite = document.getElementById("sprite");
context.drawImage(imgSprite, x, y);
if (y > (canvas.height - 17)) {
alert("Congratulations! You made it!");
return;
}
}
timer = setTimeout(drawFrame, 10);
}
var imageData = context.getImageData(0, 0, 100, 50);
var pixels = imageData.data;
for (var i = 0, n = pixels.length; i < n; i += 4) {
//This will get the data/values for one pixel
var red = pixels[i];
var green = pixels [i+1];
var blue = pixels [i+2];
var alpha = pixels [i+3];
//This will invert the colours
pixels[i] = 255 - red;
pixels[i+1] = 255 - green;
pixels[i+2] = 255 - blue;
}
context.putImageData(imageData, 0, 0);
function checkForCollision() {
var imgData = context.getImageData(x-1, y-1, 15+2, 15+2);
var pixels = imgData.data;
//Then we need to perform a check, same as above
for (var i = 0; n = pixels.length, i < n; i += 4) {
var red = pixels[i];
var green = pixels[i+1];
var blue = pixels[i+2];
var alpha = pixels[i+3];
//now check for the black pixels for a wall
if (red == 0 && green == 0 && blue == 0) {
return true;
} //checks for a greyish colour - possibly the edge of a wall
if (red == 169 && green == 169 && blue == 169) {
return true;
}
}
return false; //there was no collision
}
</script>
</body>
</html>
There are lots of errors in this code. For example, in this section alone:
(commented where some issues are)
function drawFrame() {
if (dx != 0 || dy != 0) {
context.beginPath();
context.fillStyle = "rgb(254,244,207)";
context.rect(x, y, 15, 15);
context.fill // Missing parentheses and semicolon
x += dx;
y += dy;
if (checkForCollision()) {
(dx/y = 0) // Equivalent to { dx / (y = 0) }
x -= dx; // which both serves no purpose and divides by zero
y -= dy;
dx = 0;
dy = 0;
}
//Now we can finally draw the sprite!
var imgSprite = document.getElementById("sprite");
context.drawImage(imgSprite, x, y);
if (y > (canvas.height - 17)) {
alert("Congratulations! You made it!");
return;
}
}
timer = setTimeout(drawFrame, 10); // timer is not defined anywhere
// also you are calling the function within itself
// with no end condition, so it's an infinite loop
}
And on line 48:
setTimeout("drawFrame()", 10);
You should be passing the function as just the function identifier, not as a string. As such:
setTimeout(drawFrame, 10);
These are just a few. There are also some logical errors, variables that are defined but never used, and more. In its current state this will not compile.
Also, if only just for clarity's sake, try to define a script type instead of just leaving the tag empty, like:
<script type="text/javascript">
// Some JS
</script>
It can be hard when you've been staring at code for hours, but it helps to give each section a slow read-through and think about exactly what the code is doing. You can avoid lots of syntactical and logical errors this way.
I also recommend using a text editor or online interface (JS Bin, JSfiddle, etc.) that has JShint/lint or some kind of code-checking functionality. You can even use http://www.javascriptlint.com/online_lint.php and paste your whole code in there.
I'm having a real issue trying to define a function for where I click on empty space. So far I have managed to define where I click on an object - of which there are 10 - but now I need a separate function for when I am not clicking on any of the objects. The general idea can be found at http://deciballs.co.uk/experience.html. The objects are the rings. My code is below... Any ideas?
var shapeObj = function (context, canvas, settingsBox, radius) {
this.ctx = context;
this.canvas = canvas;
this.sBox = settingsBox;
this.frequencies = new Array(220, 440, 1024, 2048);
this.cols = new Array(255, 225, 200, 175, 150);
this.strokes = new Array(1, 1.5, 2);
this.waves = new Array('sine', 'sawtooth', 'triangle', 'square');
this.properties = {
dur: Math.random()*0.5,
freq: this.frequencies[Math.floor(Math.random() * this.frequencies.length)],
radius: radius,
stroke: this.strokes[Math.floor(Math.random() * this.strokes.length)],
speed: Math.random()*6-3,
vol: Math.random()*10,
col1: this.cols[Math.floor(Math.random() * this.cols.length)],
col2: this.cols[Math.floor(Math.random() * this.cols.length)],
col3: this.cols[Math.floor(Math.random() * this.cols.length)],
alpha: 0,
wave: this.waves[Math.floor(Math.random() * this.waves.length)],
delay: 0
}
this.x = Math.random()*this.ctx.canvas.width;
this.y = Math.random()*this.ctx.canvas.height;
this.vx = 0.5;
this.vy = 1;
this.draw = function () {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.properties.radius, 0, Math.PI*2, false);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
}
this.clickTest = function (e) {
var canvasOffset = this.canvas.offset();
var canvasX = Math.floor(e.pageX-canvasOffset.left);
var canvasY = Math.floor(e.pageY-canvasOffset.top);
var dX = this.x-canvasX;
var dY = this.y-canvasY;
var distance = Math.sqrt((dX*dX)+(dY*dY));
if (distance < this.properties.radius) {
this.manageClick();
} else {
this.properties.alpha = 0;
}
};
this.manageClick = function () {
this.sBox.populate(this.properties, this);
var divs = document.getElementsByTagName('section');
for(var i = 0, e = divs[0], n = divs.length; i < n; e = divs[++i]){
e.className='class2';
}
this.properties.alpha = 0.5;
}
}
Getting perfect mouse clicks is slightly tricky, I'll share the most bulletproof mouse code that I have created thus far. It works on all browsers will all manner of padding, margin, border, and add-ons (like the stumbleupon top bar).
// Creates an object with x and y defined,
// set to the mouse position relative to the state's canvas
// If you wanna be super-correct this can be tricky,
// we have to worry about padding and borders
// takes an event and a reference to the canvas
function getMouse(e, canvas) {
var element = canvas, offsetX = 0, offsetY = 0, mx, my;
// Compute the total offset. It's possible to cache this if you want
if (element.offsetParent !== undefined) {
do {
offsetX += element.offsetLeft;
offsetY += element.offsetTop;
} while ((element = element.offsetParent));
}
// Add padding and border style widths to offset
// Also add the <html> offsets in case there's a position:fixed bar (like the stumbleupon bar)
// This part is not strictly necessary, it depends on your styling
offsetX += stylePaddingLeft + styleBorderLeft + htmlLeft;
offsetY += stylePaddingTop + styleBorderTop + htmlTop;
mx = e.pageX - offsetX;
my = e.pageY - offsetY;
// We return a simple javascript object with x and y defined
return {x: mx, y: my};
}
You'll notice that I use some (optional) variables that are undefined in the function. They are:
stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(canvas, null)['paddingLeft'], 10) || 0;
stylePaddingTop = parseInt(document.defaultView.getComputedStyle(canvas, null)['paddingTop'], 10) || 0;
styleBorderLeft = parseInt(document.defaultView.getComputedStyle(canvas, null)['borderLeftWidth'], 10) || 0;
styleBorderTop = parseInt(document.defaultView.getComputedStyle(canvas, null)['borderTopWidth'], 10) || 0;
// Some pages have fixed-position bars (like the stumbleupon bar) at the top or left of the page
// They will mess up mouse coordinates and this fixes that
var html = document.body.parentNode;
htmlTop = html.offsetTop;
htmlLeft = html.offsetLeft;
I'd recommend only computing those once, which is why they are not in the getMouse function.
You should really have a single function hat handles mouse clicks, calls getMouse once, and then goes though a list of objects, checking against each one with the x and y. Pseudocode:
function onMouseDown(e) {
var mouse = getMouse(e, canvas)
var l = myObjects.length;
var found = false;
// Maybe "deselect" them all right here
for (var i = 0; i < l; i++) {
if (distance sqrt to myObjects[i]) {
found = true;
myObjects[i].ManageClickOrWhateverYouWantHere()
}
break;
}
// And now we can know if we clicked on empty space or not!
if (!found) {
// No objects found at the click, so nothing has been clicked on
// do some relevant things here because of that
// I presume from your question this may be part of what you want
}
}
I have a simple HTML5 Canvas example that lets the user draw paths onto a canvas. Is there any way to determine the rectangular bounds of the path / shape that was drawn? (i.e., what is the width, height of the rectangular region surrounding the path).
I realize I could do the math while the shape is being drawn to figure out the bounds, but I wanted to see if there was an easier / built in way.
I assume you are using lineTos the only way I could think of would be to keep a min/max stored for the height and width as the user is drawing paths. Other than that the only way to pull back info from the canvas would be to use getImageData which will only give you raw pixel information.
Quick example showing this
var ctx = document.getElementById("canvas").getContext("2d");
var xMin, xMax, yMin, yMax;
// These are set to where the path starts, i start them at 10,10
xMin = xMax = 10;
yMin = yMax = 10;
ctx.beginPath();
ctx.moveTo(10,10);
for(var i = 0; i <10; i++){
var x = Math.floor(Math.random()*150),
y = Math.floor(Math.random()*150);
ctx.lineTo(x,y);
if(x < xMin){
xMin = x;
}
if(x > xMax){
xMax = x;
}
if(y < yMin){
yMin = y;
}
if(y > yMax){
yMax = y;
}
}
ctx.strokeStyle = "rgb(0,0,0)";
ctx.stroke();
ctx.closePath();
ctx.strokeStyle = "rgb(255,0,0)";
ctx.strokeRect(xMin,yMin,xMax - xMin,yMax - yMin);
#canvas{
width: 300px;
height: 300px;
}
<canvas id="canvas"></canvas>
note I just create a bunch of random points. The main thing to remember is set the min/max vals to the coords of the first path a user creates.
I guess you knew that though, so the real answer is no there is unfortunately no built in way currently to do it..
Although you have to track it yourself, I would suggest wrapping it up in reusable functionality. Here's a minimal example, tracking it only for moveTo and lineTo. See the live example here: http://phrogz.net/tmp/canvas_bounding_box.html
function trackBBox( ctx ){
var begin = ctx.beginPath;
ctx.beginPath = function(){
this.minX = this.minY = 99999999999;
this.maxX = this.maxY = -99999999999;
return begin.call(this);
};
ctx.updateMinMax = function(x,y){
if (x<this.minX) this.minX = x;
if (x>this.maxX) this.maxX = x;
if (y<this.minY) this.minY = y;
if (y>this.maxY) this.maxY = y;
};
var m2 = ctx.moveTo;
ctx.moveTo = function(x,y){
this.updateMinMax(x,y);
return m2.call(this,x,y);
};
var l2 = ctx.lineTo
ctx.lineTo = function(x,y){
this.updateMinMax(x,y);
return l2.call(this,x,y);
};
ctx.getBBox = function(){
return {
minX:this.minX,
maxX:this.maxX,
minY:this.minY,
maxY:this.maxY,
width:this.maxX-this.minX,
height:this.maxY-this.minY
};
};
}
...
var ctx = myCanvas.getContext("2d");
// Cause the canvas to track its own bounding box for each path
trackBBox(ctx);
ctx.beginPath();
ctx.moveTo(40,40);
for(var i=0; i<10; i++) ctx.lineTo(Math.random()*600,Math.random()*400);
// Find the bounding box of the current path
var bbox = ctx.getBBox();
ctx.strokeRect(bbox.minX,bbox.minY,bbox.width,bbox.height);
Inspired by #Phrogz's answer, the answer from Calculate bounding box of arbitrary pixel-based drawing, and his two slightly different demos http://phrogz.net/tmp/canvas_bounding_box.html and http://phrogz.net/tmp/canvas_bounding_box2.html, here is a version not using the alpha channel (it did not work in my case) but just using a comparison to white color.
function contextBoundingBox(ctx){
var w=ctx.canvas.width,h=ctx.canvas.height;
var data = ctx.getImageData(0,0,w,h).data;
var x,y,minX,minY,maxY,maxY;
o1: for (y=h;y--;) for (x=w;x--;) if ((data[(w*y+x)*4] != 255) && (data[(w*y+x)*4+1] != 255) && (data[(w*y+x)*4+2] != 255)) { maxY=y; break o1 }
o2: for (x=w;x--;) for (y=maxY+1;y--;) if ((data[(w*y+x)*4] != 255) && (data[(w*y+x)*4+1] != 255) && (data[(w*y+x)*4+2] != 255)) { maxX=x; break o2 }
o3: for (x=0;x<=maxX;++x) for (y=maxY+1;y--;) if ((data[(w*y+x)*4] != 255) && (data[(w*y+x)*4+1] != 255) && (data[(w*y+x)*4+2] != 255)) { minX=x; break o3 }
o4: for (y=0;y<=maxY;++y) for (x=minX;x<=maxX;++x) if ((data[(w*y+x)*4] != 255) && (data[(w*y+x)*4+1] != 255) && (data[(w*y+x)*4+2] != 255)) { minY=y; break o4 }
return {x:minX,y:minY,maxX:maxX,maxY:maxY,w:maxX-minX,h:maxY-minY};
}