I am working a building a simple Javascript Whack-a-mole game. I believe the issue is when by the time the "co-ordinates" for the mouse are read, the picture's X and Y values change.
When playing the game, you click on the picture, and the text that should come up when you successful click does not appear. This will not allow me to change to the picture to the "hit" picture, to let the players know they hit the object.
This is the code:
$(document).ready(function() {
document.body.onmousedown = function() {
return false;
} //so page is unselectable
//Canvas stuff
var canvas = $("#canvas")[0];
var ctx = canvas.getContext("2d");
var w = $("#canvas").width();
var h = $("#canvas").height();
var mx, my;
var player;
var mC;
var mR;
var smackSound = new Audio("audio/boing.wav");
var smackSound2 = new Audio("audio/boing2.wav");
var smackSound3 = new Audio("audio/boing3.wav");
var mel = new Image();
var melHit = new Image();
var melX;
var melY;
var melXref;
var melYref;
/////////////////////////////////
////////////////////////////////
//////// GAME INIT
/////// Runs this code right away, as soon as the page loads.
////// Use this code to get everything in order before the game starts
//////////////////////////////
/////////////////////////////
function init() {
//////////
///STATE VARIABLES
mel.src = "images/mel.jpg";
melHit.src = "images/melCrazy.jpg";
//////////////////////
///GAME ENGINE START
// This starts the game/program
// "paint is the piece of code that runs over and over again.
// "60" sets how fast things should go
if (typeof game_loop != "undefined") clearInterval(game_loop);
game_loop = setInterval(paint, 1000);
}
init();
function generate() {
var random;
random = Math.floor(Math.random() * 4);
while (random == 3) {
random = Math.floor(Math.random() * 4);
}
return random;
}
function posDisplay() {
ctx.fillStyle = "black"
ctx.fillText("Mouse Column: " + mC, 10, 10);
ctx.fillText("Mouse Row: " + mR, 10, 20);
}
///////////////////////////////////////////////////////
//////////////////////////////////////////////////////
//////// Main Game Engine
////////////////////////////////////////////////////
///////////////////////////////////////////////////
function paint() {
ctx.clearRect(0, 0, w, h);
melX = generate() * w / 3;
melY = generate() * h / 3;
//melXref = generate() / w / 3;
//melYref = generate() / h / 3;
//ctx.fillStyle = 'white';
posDisplay()
ctx.drawImage(mel, melX, melY, 200, 200);
if (melXref == mR && melYref == mC && clicker = true) {
ctx.fillStyle = "black";
ctx.fillText("It works!!!!!", 200, 200);
}
if (melX < w / 3 && clicker = true) { // First Column (Mel)
if (melY < h / 3) {
melXref = 1
melYref = 1
// clicker = true;
} else if (melY > h / 3 && melY < h / 1.5) {
melXref = 1
melYref = 2
// clicker = true;
} else if (melY > h / 1.5) {
melXref = 1
melYref = 3
// clicker = true;
}
} else if (melX > w / 3 && melX < w / 1.5 && clicker = true) { // Second Column (Mel)
if (melY < h / 3) {
melXref = 2
melYref = 1
// clicker = true;
} else if (melY > h / 3 && melY < h / 1.5) {
melXref = 2
melYref = 2
// clicker = true;
} else if (melY > h / 1.5) {
melXref = 2
melYref = 3
// clicker = true;
}
} else if (melX > w / 1.5 && clicker = true) { // Third Column (Mel)
if (melY < h / 3) {
melXref = 3
melYref = 1
// clicker = true;
} else if (melY > h / 3 && melY < h / 1.5) {
melXref = 3
melYref = 2
// clicker = true;
} else if (melY > h / 1.5) {
melXref = 3
melYref = 3
// clicker = true;
}
if (melXref == mR && melYref == mC) {
ctx.fillStyle = "black";
ctx.fillText("IT WORKS", 200, 200);
}
ctx.drawImage(mel, melX, melY, 200, 200);
if (melX == mx && melY == my) {
ctx.fillStyle = "black";
ctx.fillText("YESSSSSSSSS", 250, 250);
//ctx.drawImage(melHit,(generate()*200),(generate()*200),200,200);
}
} ////////////////////////////////////////////////////////////////////////////////END PAINT/ GAME ENGINE
}
////////////////////////////////////////////////////////
///////////////////////////////////////////////////////
///// MOUSE LISTENER
//////////////////////////////////////////////////////
/////////////////////////////////////////////////////
/////////////////
// Mouse Click
///////////////
canvas.addEventListener('click', function(evt) {
if (mx < w && my < h) {
clicker = true;
} else {
clicker = false;
}
if (clicker = true && mx < w && my < h) { // Randomizes the sound && only allows audio to be played if it is within the canvas
if (generate() == 1) {
smackSound.play();
} else if (generate() == 2) {
smackSound2.play();
} else if (generate() == 3) {
smackSound3.play();
}
}
}, false);
canvas.addEventListener('mouseout', function() {
pause = true;
}, false);
canvas.addEventListener('mouseover', function() {
pause = false;
}, false);
canvas.addEventListener('mousemove', function(evt) {
var mousePos = getMousePos(canvas, evt);
mx = mousePos.x;
my = mousePos.y;
if (mx < w / 3) { // First Column
if (my < h / 3) {
mC = 1
mR = 1
// clicker = true;
} else if (my > h / 3 && my < h / 1.5) {
mC = 1
mR = 2
//clicker = true;
} else if (my > h / 1.5) {
mC = 1
mR = 3
// clicker = true;
}
} else if (mx > w / 3 && mx < w / 1.5) { // Second Column
if (my < h / 3) {
mC = 2
mR = 1
// clicker = true;
} else if (my > h / 3 && my < h / 1.5) {
mC = 2
mR = 2
// clicker = true;
} else if (my > h / 1.5) {
mC = 2
mR = 3
// clicker = true;
}
} else if (mx > w / 1.5) { // Third Column
if (my < h / 3) {
mC = 3
mR = 1
// clicker = true;
} else if (my > h / 3 && my < h / 1.5) {
mC = 3
mR = 2
// clicker = true;
} else if (my > h / 1.5) {
mC = 3
mR = 3
// clicker = true;
}
}
}, false);
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
///////////////////////////////////
//////////////////////////////////
//////// KEY BOARD INPUT
////////////////////////////////
window.addEventListener('keydown', function(evt) {
var key = evt.keyCode;
//p 80
//r 82
//1 49
//2 50
//3 51
}, false);
})
This is a bit of a shot in the dark (and its too much to put in a comment), but from what I can tell, while you are recording the mouse position in the mousemove event, you are not doing so in the click event. I believe its not so much that the picture's X and Y have changed, it is probably that the last recorded mouse's X and Y have changed due to the mousemove event firing after the click.
A general solution (because it can be solved many different ways) might be to record the mouse's row and column in the click event, and then suppress any mouse position updates in the mousemove event if your clicker variable is true. So maybe something like the following:
canvas.addEventListener('mousemove', function(evt) {
if(!clicker){
// happily record the mouse position, because user hasn't clicked anything
In the paint method, you can check if clicker is true and do the text thing using the last known mouse position previously recorded in the click event, and then reset it back to false when you're done processing. Your paint method is fairly large, and clicker seems to be used throughout so I won't post a full blown code block in the interests of brevity, but I think you get the idea.
ALSO: I notice in your if statements, you're checking the clicker variable using && clicker = true. Notice the single equal sign? That's extremely bad! Instead of checking its value, you're assigned its value. Remember single equals is for assignment, double equals is for equivalence.
You can solve that error a couple of ways; the simplest is to simply make sure you change it to == instead, and anywhere else you might have made that error. Or, since its a boolean, you can simple do && clicker or && !clicker, depending on the condition.
Alternatively, from the looks of your logic, since every if and if else is dependent on whether clicker is true, you can get rid of the redundant checking in each condition and simply wrap the entire thing in oneif`, like so:
if(clicker){
if (melXref == mR && melYref == mC) {
ctx.fillStyle = "black";
ctx.fillText("It works!!!!!", 200, 200);
}
if (melX < w / 3) { // First Column (Mel)
... other conditions
}
Related
I'm a beginner using p5js and I'm trying to work with classes. I'm making a game where you have to find and click a 'wanted man', from a crowd.
So basically, a randomizer picks between 7 different types of 'civilians', and it's supposed to remove one of the types from the 'civilians' that have been spawned. After removing the 'wanted man', I want to add one wanted man so that there is only one 'wanted man'.
So the code spawns a bunch of random 'civilians', then it will delete all 'wanted man' types in the array, and add only one of them. I think there is a better way to do this though.
My basic desire is to have a crowd of 'civilians' that run around, - one of which is a 'wanted man' - and you would have to find and click that 'wanted man' (kind of like a hunting/assassination game).
This is the code for the sketch.js file:
var civilians = [];
var page = 0;
var man1img;
var man2img;
var man3img;
var man4img;
var man5img;
var man6img;
var aliemanimg;
var w;
var h;
var spawnCount = 14;
var wantedMan;
var randCiv;
function preload() {
man1img = loadImage("man1.png");
man2img = loadImage("man2.png");
man3img = loadImage("man3.png");
man4img = loadImage("man4.png");
man5img = loadImage("man5.png");
man6img = loadImage("man6.png");
aliemanimg = loadImage("alieman.png");
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
function setup() {
createCanvas(windowWidth, windowHeight);
imageMode(CENTER);
// wantedMan = round(random(0, 6));
wantedMan = 0;
for (var i = 0; i < spawnCount; i++) {
randCiv = round(random(0, 6));
w = random(windowWidth);
h = random(windowHeight);
civilians.push(new Civilian(w, h, wantedMan, randCiv));
console.log(wantedMan);
if (civilians[i].isWantedMan()) {
//OVER HERE \/
civilians.splice(i, 1);
}
}
civilians.push(new Civilian(w, h, wantedMan, wantedMan));
}
// page setup
// page 1 : main screen (play, settings, and those stuff)
// page 2 : show chosen civilian
// page 3 : playing
// page 4 : lose
// page 5 : options
function draw() {
background(220, 80, 80);
for (var i = civilians.length - 1; i >= 0; i--) {
civilians[i].update();
civilians[i].show(mouseX, mouseY);
if (civilians[i].clickedOn(mouseX, mouseY)) {
// detect if is right person
console.log("clicked on boi");
if (civilians[i].isWantedMan()) {
console.log("HES WANTED");
} else {
console.log("HES NOT WANTED");
}
}
}
text(round(frameRate()), 20, 20);
//show wanted man
var tempImg = man1img;
if (wantedMan == 1) {
tempImg = man2img;
} else if (wantedMan == 2) {
tempImg = man3img;
} else if (wantedMan == 3) {
tempImg = man4img;
}
if (wantedMan == 4) {
tempImg = man5img;
} else if (wantedMan == 5) {
tempImg = man6img;
} else if (wantedMan == 6) {
tempImg = aliemanimg;
}
image(tempImg, 50, 70, 70, 90);
}
This is the code for the class:
class Civilian {
constructor(x, y, wantedMan, type) {
this.x = x;
this.y = y;
this.w = 47;
this.h = 60;
this.t = {
x: x,
y: y,
};
this.size = 47;
this.moveSpeed = 0.01;
this.moveDist = 20;
this.wantedMan = wantedMan;
this.civilian = type
this.civilianImg = man1img
this.wantedMan = wantedMan
}
update() {
//move target to random position
this.t.x = random(this.t.x - this.moveDist, this.t.x + this.moveDist);
this.t.y = random(this.t.y - this.moveDist, this.t.y + this.moveDist);
//edge detect
if (this.t.x < 0) {
this.t.x += 5;
}
if (this.t.x > width) {
this.t.x -= 5;
}
if (this.t.y < 0) {
this.t.y += 5;
}
if (this.t.y > height) {
this.t.y -= 5;
}
//images position follows target but with easing
this.x += (this.t.x - this.x) * this.moveSpeed;
this.y += (this.t.y - this.y) * this.moveSpeed;
}
show(ex, ey) {
var d = dist(ex, ey, this.x, this.y);
if (d > this.size / 2) {
tint(255, 255, 255);
} else {
tint(0, 255, 0);
}
if(this.civilian == 1) {
this.civilianImg = man2img
} else if(this.civilian == 2) {
this.civilianImg = man3img
} else if(this.civilian ==3) {
this.civilianImg = man4img
} if(this.civilian == 4) {
this.civilianImg = man5img
} else if(this.civilian == 5) {
this.civilianImg = man6img
} else if(this.civilian == 6) {
this.civilianImg = aliemanimg
}
image(this.civilianImg, this.x, this.y, 47, 60);
}
clickedOn(ex, ey) {
var d = dist(ex, ey, this.x, this.y);
return d < this.size / 2 && mouseIsPressed;
}
isWantedMan() {
return this.civilian == this.wantedMan;
}
}
However, whenever I add a .splice(i,1) under the 'for' loop in setup function - to remove the 'wanted man', it shows this error:
"TypeError: Cannot read properties of undefined (reading
'isWantedMan') at /sketch.js:41:22".
isWantedMan() is a function in the Civilian Class, that returns true if the current 'civilian' is wanted. The .splice is supposed to remove a object from the array, when it is a 'wanted man'.
I don't know why this happens. When I replace the .splice code with a console.log() code, then there is no error.
Also there were probably a lot of things that I could have done better in the code.
When I test or publish my project, which is a jigsaw puzzle from this tutorial: https://www.youtube.com/watch?v=uCQuUZs3UGE
Nothing gets drawn except the grey background. No puzzle pices nor any puzzle shape is drawn at all. I also get a warning:
WARNINGS:
Frame numbers in EaselJS start at 0 instead of 1. For example, this affects gotoAndStop and gotoAndPlay calls. (17)
But that should just be a warning, not an error. So might there be an error in my javascript code? This is the code, its fairly simple:
//************
// Initialize;
var numPieces = 16;
for (var i = 0; i = < numPieces; i++)
{
var pieceName = "p" + (i + 1);
var piece = this[pieceName];
if (piece)
{
piece.name = pieceName;
piece.on("mousedown", function(evt)
{
this.scaleX = 1;
this.scaleY = 1;
this.shadow = null;
this.parent.addChild(this);
this.offset = (x:this.x - evt.stageX, y:this.y - evt.stageY);
});
piece.on("pressmove", function (evt)
{
this.x = evt.stageX + this.offset.x;
this.y = evt.stageY + this.offset.y;
});
piece.on("pressup", function(evt)
{
var target = this.parent["t"+this.name.substr(1)];
if (target && hitTestInRange(target, 30) )
{
this.x = target.x;
this.y = target.y;
}
});
}
}
function hitTestInRange( target, range )
{
if (target.x > stage.mouseX - range &&
target.x < stage.mouseX + range &&
target.y > stage.mouseY - range &&
target.y < stage.mouseY + range)
{
return true;
}
return false;
}
I also included a screenshot of the project. screenshot
Thanks in advance
Im having a few issues resetting my canvas when the window re-sizes or after a given amount of time. I want to complete reset and have it fresh. The problem is if your wait a few minutes (Because the refresh runs) or you re-size your window, everything starts to go haywire. I believe its because its drawing the canvas over top of an existing one and that's whats bleeding through. Any ideas on how to resolve this?
// Constellations
function constellations() {
var pr = (function () {
var ctx = document.createElement("canvas").getContext("2d"),
dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
})();
function t() {
this.x = Math.random() * o.width, this.y = Math.random() * o.height, this.vx = -.5 + Math.random(), this.vy = -.5 + Math.random(), this.radius = Math.random()
}
function d() {
for (a.clearRect(0, 0, o.width, o.height), i = 0; i < r.nb; i++) r.array.push(new t), dot = r.array[i], dot.create();
dot.line(), dot.animate()
}
var o = document.querySelector("#constellations");
var ratio = pr;
o.width = $(window).width() * ratio;
o.height = $(window).height() * ratio;
o.style.width = $(window).width() + "px";
o.style.height = $(window).height() + "px";
o.style.display = "block";
var n = "#1A2732";
var linecolor = "#FF535A";
a = o.getContext("2d");
a.setTransform(ratio, 0, 0, ratio, 0, 0);
a.clearRect(0, 0, o.width, o.height);
a.fillStyle = n;
a.lineWidth = .1;
a.strokeStyle = linecolor;
var e = {
x: 30 * o.width / 100,
y: 30 * o.height / 100
},
r = {
nb: o.width / 10,
distance: 80,
d_radius: 150,
array: []
};
t.prototype = {
create: function() {
a.beginPath(), a.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, !1), a.fill()
},
animate: function() {
for (i = 0; i < r.nb; i++) {
var t = r.array[i];
t.y < 0 || t.y > o.height ? (t.vx = t.vx, t.vy = -t.vy) : (t.x < 0 || t.x > o.width) && (t.vx = -t.vx, t.vy = t.vy), t.x += t.vx, t.y += t.vy
}
},
line: function() {
for (i = 0; i < r.nb; i++)
for (j = 0; j < r.nb; j++) i_dot = r.array[i], j_dot = r.array[j], i_dot.x - j_dot.x < r.distance && i_dot.y - j_dot.y < r.distance && i_dot.x - j_dot.x > -r.distance && i_dot.y - j_dot.y > -r.distance && i_dot.x - e.x < r.d_radius && i_dot.y - e.y < r.d_radius && i_dot.x - e.x > -r.d_radius && i_dot.y - e.y > -r.d_radius && (a.beginPath(), a.moveTo(i_dot.x, i_dot.y), a.lineTo(j_dot.x, j_dot.y), a.stroke(), a.closePath())
}
};
var refresh = setInterval(d, 1e3 / 30);
$(window).resize(function() {
window.clearInterval(refresh);
constellations();
});
}constellations();
<canvas id="constellations"></canvas>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
To see it working in action. Just re-size the divider and watch. It may even be a case of reseting the function. Im not sure what should be done to resolve this issue. Its a weird one.
https://jsfiddle.net/v4dqgazr/
The problem is, as the browser is being resized, $(window).resize() is triggered continuously as the resizing is in progress. You can use David Walsh's debounce method to solve this issue.
Here is an update demo with event logs
The debounce method looks like this
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
Just wanted to give some insight on what's happening:
Every instant that the window is being resized, constellations() is being called. Apparently different browsers handle this differently, Dhiraj's answer should help you.
I added code like this, which shows you that many instances are being created and are running:
function constellations(instanceID) {
...
function d() {
console.log("Draw called from instance id: " + instanceID;
...
}
$(window).resize(function() {
window.clearInterval(refresh);
constellations(Math.random());
});
You'll see a lot of log calls being made once you resize.
We're working with the HTML5 canvas, displaying lots of images at one time.
This is working pretty well but recently we've had a problem with chrome.
When drawing images on to a canvas you seem to reach a certain point where the performance degrades very quickly.
It's not a slow effect, it seems that you go right from 60fps to 2-4fps.
Here's some reproduction code:
// Helpers
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
// http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimFrame = (function () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; })();
// https://github.com/mrdoob/stats.js
var Stats = function () { var e = Date.now(), t = e; var n = 0, r = Infinity, i = 0; var s = 0, o = Infinity, u = 0; var a = 0, f = 0; var l = document.createElement("div"); l.id = "stats"; l.addEventListener("mousedown", function (e) { e.preventDefault(); y(++f % 2) }, false); l.style.cssText = "width:80px;opacity:0.9;cursor:pointer"; var c = document.createElement("div"); c.id = "fps"; c.style.cssText = "padding:0 0 3px 3px;text-align:left;background-color:#002"; l.appendChild(c); var h = document.createElement("div"); h.id = "fpsText"; h.style.cssText = "color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; h.innerHTML = "FPS"; c.appendChild(h); var p = document.createElement("div"); p.id = "fpsGraph"; p.style.cssText = "position:relative;width:74px;height:30px;background-color:#0ff"; c.appendChild(p); while (p.children.length < 74) { var d = document.createElement("span"); d.style.cssText = "width:1px;height:30px;float:left;background-color:#113"; p.appendChild(d) } var v = document.createElement("div"); v.id = "ms"; v.style.cssText = "padding:0 0 3px 3px;text-align:left;background-color:#020;display:none"; l.appendChild(v); var m = document.createElement("div"); m.id = "msText"; m.style.cssText = "color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; m.innerHTML = "MS"; v.appendChild(m); var g = document.createElement("div"); g.id = "msGraph"; g.style.cssText = "position:relative;width:74px;height:30px;background-color:#0f0"; v.appendChild(g); while (g.children.length < 74) { var d = document.createElement("span"); d.style.cssText = "width:1px;height:30px;float:left;background-color:#131"; g.appendChild(d) } var y = function (e) { f = e; switch (f) { case 0: c.style.display = "block"; v.style.display = "none"; break; case 1: c.style.display = "none"; v.style.display = "block"; break } }; var b = function (e, t) { var n = e.appendChild(e.firstChild); n.style.height = t + "px" }; return { REVISION: 11, domElement: l, setMode: y, begin: function () { e = Date.now() }, end: function () { var f = Date.now(); n = f - e; r = Math.min(r, n); i = Math.max(i, n); m.textContent = n + " MS (" + r + "-" + i + ")"; b(g, Math.min(30, 30 - n / 200 * 30)); a++; if (f > t + 1e3) { s = Math.round(a * 1e3 / (f - t)); o = Math.min(o, s); u = Math.max(u, s); h.textContent = s + " FPS (" + o + "-" + u + ")"; b(p, Math.min(30, 30 - s / 100 * 30)); t = f; a = 0 } return f }, update: function () { e = this.end() } } }
// Firefox events suck
function getOffsetXY(eventArgs) { return { X: eventArgs.offsetX == undefined ? eventArgs.layerX : eventArgs.offsetX, Y: eventArgs.offsetY == undefined ? eventArgs.layerY : eventArgs.offsetY }; }
function getWheelDelta(eventArgs) { if (!eventArgs) eventArgs = event; var w = eventArgs.wheelDelta; var d = eventArgs.detail; if (d) { if (w) { return w / d / 40 * d > 0 ? 1 : -1; } else { return -d / 3; } } else { return w / 120; } }
// Reproduction Code
var stats = new Stats();
document.body.appendChild(stats.domElement);
var masterCanvas = document.getElementById('canvas');
var masterContext = masterCanvas.getContext('2d');
var viewOffsetX = 0;
var viewOffsetY = 0;
var viewScaleFactor = 1;
var viewMinScaleFactor = 0.1;
var viewMaxScaleFactor = 10;
var mouseWheelSensitivity = 10; //Fudge Factor
var isMouseDown = false;
var lastMouseCoords = null;
var imageDimensionPixelCount = 25;
var paddingPixelCount = 2;
var canvasDimensionImageCount = 50;
var totalImageCount = Math.pow(canvasDimensionImageCount, 2);
var images = null;
function init() {
images = createLocalImages(totalImageCount, imageDimensionPixelCount);
initInteraction();
renderLoop();
}
function initInteraction() {
var handleMouseDown = function (eventArgs) {
isMouseDown = true;
var offsetXY = getOffsetXY(eventArgs);
lastMouseCoords = [
offsetXY.X,
offsetXY.Y
];
};
var handleMouseUp = function (eventArgs) {
isMouseDown = false;
lastMouseCoords = null;
}
var handleMouseMove = function (eventArgs) {
if (isMouseDown) {
var offsetXY = getOffsetXY(eventArgs);
var panX = offsetXY.X - lastMouseCoords[0];
var panY = offsetXY.Y - lastMouseCoords[1];
pan(panX, panY);
lastMouseCoords = [
offsetXY.X,
offsetXY.Y
];
}
};
var handleMouseWheel = function (eventArgs) {
var mouseX = eventArgs.pageX - masterCanvas.offsetLeft;
var mouseY = eventArgs.pageY - masterCanvas.offsetTop;
var zoom = 1 + (getWheelDelta(eventArgs) / mouseWheelSensitivity);
zoomAboutPoint(mouseX, mouseY, zoom);
if (eventArgs.preventDefault !== undefined) {
eventArgs.preventDefault();
} else {
return false;
}
}
masterCanvas.addEventListener("mousedown", handleMouseDown, false);
masterCanvas.addEventListener("mouseup", handleMouseUp, false);
masterCanvas.addEventListener("mousemove", handleMouseMove, false);
masterCanvas.addEventListener("mousewheel", handleMouseWheel, false);
masterCanvas.addEventListener("DOMMouseScroll", handleMouseWheel, false);
}
function pan(panX, panY) {
masterContext.translate(panX / viewScaleFactor, panY / viewScaleFactor);
viewOffsetX -= panX / viewScaleFactor;
viewOffsetY -= panY / viewScaleFactor;
}
function zoomAboutPoint(zoomX, zoomY, zoomFactor) {
var newCanvasScale = viewScaleFactor * zoomFactor;
if (newCanvasScale < viewMinScaleFactor) {
zoomFactor = viewMinScaleFactor / viewScaleFactor;
} else if (newCanvasScale > viewMaxScaleFactor) {
zoomFactor = viewMaxScaleFactor / viewScaleFactor;
}
masterContext.translate(viewOffsetX, viewOffsetY);
masterContext.scale(zoomFactor, zoomFactor);
viewOffsetX = ((zoomX / viewScaleFactor) + viewOffsetX) - (zoomX / (viewScaleFactor * zoomFactor));
viewOffsetY = ((zoomY / viewScaleFactor) + viewOffsetY) - (zoomY / (viewScaleFactor * zoomFactor));
viewScaleFactor *= zoomFactor;
masterContext.translate(-viewOffsetX, -viewOffsetY);
}
function renderLoop() {
clearCanvas();
renderCanvas();
stats.update();
requestAnimFrame(renderLoop);
}
function clearCanvas() {
masterContext.clearRect(viewOffsetX, viewOffsetY, masterCanvas.width / viewScaleFactor, masterCanvas.height / viewScaleFactor);
}
function renderCanvas() {
for (var imageY = 0; imageY < canvasDimensionImageCount; imageY++) {
for (var imageX = 0; imageX < canvasDimensionImageCount; imageX++) {
var x = imageX * (imageDimensionPixelCount + paddingPixelCount);
var y = imageY * (imageDimensionPixelCount + paddingPixelCount);
var imageIndex = (imageY * canvasDimensionImageCount) + imageX;
var image = images[imageIndex];
masterContext.drawImage(image, x, y, imageDimensionPixelCount, imageDimensionPixelCount);
}
}
}
function createLocalImages(imageCount, imageDimension) {
var tempCanvas = document.createElement('canvas');
tempCanvas.width = imageDimension;
tempCanvas.height = imageDimension;
var tempContext = tempCanvas.getContext('2d');
var images = new Array();
for (var imageIndex = 0; imageIndex < imageCount; imageIndex++) {
tempContext.clearRect(0, 0, imageDimension, imageDimension);
tempContext.fillStyle = "rgb(" + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ")";
tempContext.fillRect(0, 0, imageDimension, imageDimension);
var image = new Image();
image.src = tempCanvas.toDataURL('image/png');
images.push(image);
}
return images;
}
// Get this party started
init();
And a jsfiddle link for your interactive pleasure:
http://jsfiddle.net/BtyL6/14/
This is drawing 50px x 50px images in a 50 x 50 (2500) grid on the canvas. I've also quickly tried with 25px x 25px and 50 x 50 (2500) images.
We have other local examples that deal with bigger images and larger numbers of images and the other browser start to struggle with these at higher values.
As a quick test I jacked up the code in the js fiddle to 100px x 100px and 100 x 100 (10000) images and that was still running at 16fps when fully zoomed out. (Note: I had to lower the viewMinScaleFactor to 0.01 to fit it all in when zoomed out.)
Chrome on the other hand seems to hit some kind of limit and the FPS drops from 60 to 2-4.
Here's some info about what we've tried and the results:
We've tried using setinterval rather than requestAnimationFrame.
If you load 10 images and draw them 250 times each rather than 2500 images drawn once each then the problem goes away. This seems to indicate that chrome is hitting some kind of limit/trigger as to how much data it's storing about the rendering.
We have culling (not rendering images outside of the visual range) in our more complex examples and while this helps it's not a solution as we need to be able to show all the images at once.
We have the images only being rendered if there have been changes in our local code, against this helps (when nothing changes, obviously) but it isn't a full solution because the canvas should be interactive.
In the example code we're creating the images using a canvas, but the code can also be run hitting a web service to provide the images and the same behaviour (slowness) will be seen.
We've found it very hard to even search for this issue, most results are from a couple of years ago and woefully out of date.
If any more information would be useful then please ask!
EDIT: Changed js fiddle URL to reflect the same code as in the question. The code itself didn't actually change, just the formatting. But I want to be consistent.
EDIT: Updated jsfiddle and and code with css to prevent selection and call requestAnim after the render loop is done.
In Canary this code freezes it on my computer. As to why this happens in Chrome the simple answer is that it uses a different implementation than f.ex. FF. In-depth detail I don't know, but there is obviously room for optimizing the implementation in this area.
I can give some tip however on how you can optimize the given code to make it run in Chrome as well :-)
There are several things here:
You are storing each block of colors as images. This seem to have a huge performance impact on Canary / Chrome.
You are calling requestAnimationFrame at the beginning of the loop
You are clearing and rendering even if there are no changes
Try to (addressing the points):
If you only need solid blocks of colors, draw them directly using fillRect() instead and keep the color indexes in an array (instead of images). Even if you draw them to an off-screen canvas you will only have to do one draw to main canvas instead of multiple image draw operations.
Move requestAnimationFrame to the end of the code block to avoid stacking.
Use dirty flag to prevent unnecessary rendering:
I modified the code a bit - I modified it to use solid colors to demonstrate where the performance impact is in Chrome / Canary.
I set a dirty flag in global scope as true (to render the initial scene) which is set to true each time the mouse move occur:
//global
var isDirty = true;
//mouse move handler
var handleMouseMove = function (eventArgs) {
// other code
isDirty = true;
// other code
};
//render loop
function renderLoop() {
if (isDirty) {
clearCanvas();
renderCanvas();
}
stats.update();
requestAnimFrame(renderLoop);
}
//in renderCanvas at the end:
function renderCanvas() {
// other code
isDirty = false;
}
You will of course need to check for caveats for the isDirty flag elsewhere and also introduce more criteria if it's cleared at the wrong moment. I would store the old position of the mouse and only (in the mouse move) if it changed set the dirty flag - I didn't modify this part though.
As you can see you will be able to run this in Chrome and in FF at a higher FPS.
I also assume (I didn't test) that you can optimize the clearCanvas() function by only drawing the padding/gaps instead of clearing the whole canvas. But that need to be tested.
Added a CSS-rule to prevent the canvas to be selected when using the mouse:
For further optimizing in cases such as this, which is event driven, you don't actually need an animation loop at all. You can just call the redraw when the coords or mouse-wheel changes.
Modification:
http://jsfiddle.net/BtyL6/10/
This was a legitimate bug in chrome.
https://code.google.com/p/chromium/issues/detail?id=247912
It has now been fixed and should be in a chrome mainline release soon.
I am reading and trying to do this tutorial:
http://www.sitepoint.com/creating-a-simple-windows-8-game-with-javascript-input-and-sound/
Yesterday, I wrote in this forum with one error, and solved it, but today, I'm in the final one and getting another error.
My default.html file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>CatapultGame</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-light.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<!-- CatapultGame references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
<script src="js/CreateJS/easeljs-0.6.0.min.js"></script>
<script src="js/CreateJS/preloadjs-0.2.0.min.js"></script>
</head>
<body>
<canvas id="gameCanvas"></canvas>
</body>
</html>
and default.js file :
// For an introduction to the Blank template, see the following documentation:
// http://go.microsoft.com/fwlink/?LinkId=232509
(function () {
"use strict";
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
WinJS.strictProcessing();
var canvas, context, stage;
var bgImage, p1Image, p2Image, ammoImage, p1Lives, p2Lives, title, endGameImage;
var bgBitmap, p1Bitmap, p2Bitmap, ammoBitmap;
var preload;
// calculate display scale factor - original game assets assume 800x480
var SCALE_X = window.innerWidth / 800;
var SCALE_Y = window.innerHeight / 480;
var MARGIN = 25;
var GROUND_Y = 390 * SCALE_Y;
var LIVES_PER_PLAYER = 3;
var player1Lives = LIVES_PER_PLAYER;
var player2Lives = LIVES_PER_PLAYER;
var isShotFlying = false;
var playerTurn = 1;
var playerFire = false;
var shotVelocity;
var MAX_SHOT_POWER = 10;
var GRAVITY = 0.07;
var isAiming = false;
var aimPower = 1;
var aimStart, aimVector;
var FIRE_SOUND_FILE = "/sounds/CatapultFire.wav";
var HIT_SOUND_FILE = "/sounds/BoulderHit.wav";
var EXPLODE_SOUND_FILE = "/sounds/CatapultExplosion.wav";
var LOSE_SOUND_FILE = "/sounds/Lose.wav";
var AIM_SOUND_FILE = "/sounds/RopeStretch.wav";
var WIN_SOUND_FILE = "/sounds/Win.wav";
app.onactivated = function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
// TODO: This application has been newly launched. Initialize
// your application here.
} else {
// TODO: This application has been reactivated from suspension.
// Restore application state here.
}
args.setPromise(WinJS.UI.processAll());
}
};
function initialize() {
canvas = document.getElementById("gameCanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
context = canvas.getContext("2d");
canvas.addEventListener("MSPointerUp", endAim, false);
canvas.addEventListener("MSPointerMove", adjustAim, false);
canvas.addEventListener("MSPointerDown", beginAim, false)
**var stage = new createjs.Stage(canvas);** <<========== HERE IS THE ERROR LINE !!!!
// use preloadJS to get sounds and images loaded before starting
preload = new createjs.PreloadJS();
preload.onComplete = prepareGame;
var manifest = [
{ id: "screenImage", src: "images/Backgrounds/gameplay_screen.png" },
{ id: "redImage", src: "images/Catapults/Red/redIdle/redIdle.png" },
{ id: "blueImage", src: "images/Catapults/Blue/blueIdle/blueIdle.png" },
{ id: "ammoImage", src: "images/Ammo/rock_ammo.png" },
{ id: "winImage", src: "images/Backgrounds/victory.png" },
{ id: "loseImage", src: "images/Backgrounds/defeat.png" },
{ id: "blueFire", src: "images/Catapults/Blue/blueFire/blueCatapultFire.png" },
{ id: "redFire", src: "images/Catapults/Red/redFire/redCatapultFire.png" },
{ id: "hitSound", src: HIT_SOUND_FILE },
{ id: "explodeSound", src: EXPLODE_SOUND_FILE },
{ id: "fireSound", src: FIRE_SOUND_FILE },
{ id: "loseSound", src: LOSE_SOUND_FILE },
{ id: "aimSound", src: AIM_SOUND_FILE },
{ id: "winSound", src: WIN_SOUND_FILE }
];
preload.loadManifest(manifest);
}
function prepareGame()
{
// draw Bg first, others appear on top
bgImage = preload.getResult("screenImage").result;
bgBitmap = new createjs.Bitmap(bgImage);
bgBitmap.scaleX = SCALE_X;
bgBitmap.scaleY = SCALE_Y;
stage.addChild(bgBitmap);
// draw p1 catapult
p1Image = preload.getResult("redImage").result;
p1Bitmap = new createjs.Bitmap(p1Image);
p1Bitmap.scaleX = SCALE_X;
p1Bitmap.scaleY = SCALE_Y;
p1Bitmap.x = MARGIN;
p1Bitmap.y = GROUND_Y - p1Image.height * SCALE_Y;
stage.addChild(p1Bitmap);
// draw p2 catapult and flip
p2Image = preload.getResult("blueImage").result;
p2Bitmap = new createjs.Bitmap(p2Image);
p2Bitmap.regX = p2Image.width;
p2Bitmap.scaleX = -SCALE_X; // flip from right edge
p2Bitmap.scaleY = SCALE_Y;
p2Bitmap.x = canvas.width - MARGIN - (p2Image.width * SCALE_X);
p2Bitmap.y = GROUND_Y - (p2Image.height * SCALE_Y);
stage.addChild(p2Bitmap);
// draw the boulder, and hide for the moment
ammoImage = preload.getResult("ammoImage").result;
ammoBitmap = new createjs.Bitmap(ammoImage);
ammoBitmap.scaleX = SCALE_X;
ammoBitmap.scaleY = SCALE_Y;
ammoBitmap.visible = false; // hide until fired
stage.addChild(ammoBitmap);
// player 1 lives
p1Lives = new createjs.Text("Lives Left : " + player1Lives, "20px sans-serif", "red");
p1Lives.scaleX = SCALE_X;
p1Lives.scaleY = SCALE_Y;
p1Lives.x = MARGIN;
p1Lives.y = MARGIN * SCALE_Y;
stage.addChild(p1Lives);
//player 2 lives
p2Lives = new createjs.Text("Lives Left : " + player2Lives, "20px sans-serif", "red");
p2Lives.scaleX = SCALE_X;
p2Lives.scaleY = SCALE_Y;
p2Lives.x = canvas.width - p2Lives.getMeasuredWidth() * SCALE_X - MARGIN;
p2Lives.y = MARGIN * SCALE_Y;
stage.addChild(p2Lives);
// game title
title = new createjs.Text("Catapult Wars", "30px sans-serif", "black");
title.scaleX = SCALE_X;
title.scaleY = SCALE_Y;
title.x = canvas.width / 2 - (title.getMeasuredWidth() * SCALE_X) / 2
title.y = 30 * SCALE_Y;
stage.addChild(title);
stage.update();
startGame();
}
function startGame()
{
Ticker.setInterval(window.requestAnimationFrame);
Ticker.addListener(gameLoop);
}
function gameLoop()
{
update();
draw();
}
function update() {
if (isShotFlying)
{
// shot in the air
ammoBitmap.x += shotVelocity.x;
ammoBitmap.y += shotVelocity.y;
shotVelocity.y += GRAVITY; //apply gravity to the y(height) values only, obviously
if (ammoBitmap.y + ammoBitmap.image.height >= GROUND_Y ||
ammoBitmap.x <= 0 ||
ammoBitmap.x + ammoBitmap.image.width >= canvas.width)
{
// missed
isShotFlying = false; //stop shot
ammoBitmap.visible = false;
playerTurn = playerTurn % 2 + 1; // invert player ( switch between 1 and 2)
}
else if (playerTurn == 1)
{
if (checkHit(p2Bitmap)) {
// Hit
p2Lives.text = "Lives Left : " + --player2Lives;
processHit();
}
}
else if (playerTurn == 2)
{
if (checkHit(p1Bitmap))
{
// Hit
p1Lives.text = "Lives Left : " + --player1Lives;
processHit();
}
}
}
// No current shot, should either player fire ?
else if (playerTurn == 1)
{
// does the player want to fire ?
if (playerFire)
{
playerFire = false;
ammoBitmap.x = p1Bitmap.x + (p1Bitmap.image.width * SCALE_X / 2);
ammoBitmap.y = p1Bitmap.y;
shotVelocity = aimVector;
fireShot();
}
}
else if (playerTurn == 2)
{
// AI automatically fires (randomly on it's turn)
ammoBitmap.x = p2Bitmap.x + (p2Bitmap.image.width * SCALE_X / 2);
ammoBitmap.y = p2Bitmap.y;
shotVelocity = new createjs.Point(
Math.random() * (-4 * SCALE_X) - 3,
Math.random() * (-3 * SCALE_Y) - 1);
fireShot();
}
}
// triggered by MSPointerDown event
function beginAim(event)
{
if (playerTurn == 1)
{
if (!isAiming)
{
aimStart = new createjs.Point(event.x, event.y);
isAiming = true;
}
}
}
// triggered by MSPointerMove event
function adjustAim(event)
{
if (isAiming)
{
var aimCurrent = new createjs.Point(event.x, event.y);
aimVector = calculateAim(aimStart, aimCurrent);
// TODO write text and/or show aiming arrow on screen
Debug.writeln("Aiming..." + aimVector.x + "/" + aimVector.y);
}
}
// triggered by MSPointerUp event
function endAim(event)
{
if (isAiming) {
isAiming = false;
var aimCurrent = new createjs.Point(event.x, event.y);
aimVector = calculateAim(aimStart, aimCurrent);
playerFire = true;
}
}
function calculateAim(start, end)
{
// this only works for player 1
var aim = new createjs.Point(
(end.x - start.x) / 80,
(end.y - start.y) / 80);
aim.x = Math.min(MAX_SHOT_POWER, aim.x); // cap velocity
aim.x = Math.max(0, aim.x); // fire forward only
aim.y = Math.max(-MAX_SHOT_POWER, aim.y);/// cap velocity
aim.y = Math.min(0, aim.y); // fire up only
return aim;
}
function checkHit(target)
{
// EaselJS hit test doesn't factor in scaling
// so use simple bounding box vs center of rock
// get centre of rock
var shotX = ammoBitmap.x + ammoBitmap.image.width / 2;
var shotY = ammoBitmap.y + ammoBitmap.image.height / 2;
// return wether center of rock is in rectangle bounding target player
return (((shotX > target.x) &&
(shotX <= target.x + (target.image.width * SCALE_X)))
&&
((shotY >= target.y) &&
(shotY <= target.y + (target.image.height * SCALE_Y))));
}
function fireShot()
{
playSound(FIRE_SOUND_FILE);
ammoBitmap.visible = true;
isShotFlying = true;
}
function processHit()
{
playSound(EXPLODE_SOUND_FILE);
isShotFlying = false; // stop shot
ammoBitmap.visible = false; // hide shot
playerTurn = playerTurn % 2 + 1; // change player
if ((player1Lives <= 0) || (player2Lives <= 0)) {
endGame();
}
}
function endGame()
{
Ticker.setPaused(true); // stop game loop
// show win/lose graphic
var endGameImage;
if (player1Lives <= 0)
{
playSound(LOSE_SOUND_FILE);
endGameImage = preload.getResult("loseImage").result;
}
else if (player2Lives <= 0)
{
endGameImage = preload.getResult("winImage").result;
playSound(WIN_SOUND_FILE);
}
var endGameBitmap = new createjs.Bitmap(endGameImage);
stage.addChild(endGameBitmap);
endGameBitmap.x = (canvas.width / 2) - (endGameImage.width * SCALE_X / 2);
endGameBitmap.y = (canvas.height / 2) - (endGameImage.height * SCALE_Y / 2);
endGameBitmap.scaleX = SCALE_X;
endGameBitmap.scaleY = SCALE_Y;
stage.update();
}
function draw() {
// EaselJS allows for easy updates
stage.update();
}
function playSound(path)
{
var sound = document.createElement("audio");
sound.src = path;
sound.autoplay = true;
}
app.oncheckpoint = function (args) {
// TODO: This application is about to be suspended. Save any state
// that needs to persist across suspensions here. You might use the
// WinJS.Application.sessionState object, which is automatically
// saved and restored across suspension. If you need to complete an
// asynchronous operation before your application is suspended, call
// args.setPromise().
};
document.addEventListener("DOMContentLoaded", initialize, false);
app.start();
})();
And there is the problem: when I'm trying to build this game, I'm getting error like this:
0x800a01bd - JavaScript runtime error: Object doesn't support this action
in the marked place in my code
Thanks for any help :)
Three problems, I report them from previous comments:
easeljs not present in the correct folder
mages/Catapults/Red/redFire/redCatapultFire.png missing
change var stage = new createjs.Stage(canvas); to stage = new createjs.Stage(canvas); you don't have to instantiate that variable as reported in the tutorial (http://www.sitepoint.com/creating-a-simple-windows-8-game-with-javascript-game-basics-createjseaseljs/)
And close this other related question (https://stackoverflow.com/a/16101960/975520), I think is solved by posting your solution as answer.