I'm creating a HTML5 minigame that uses collision-detection and I've recently discovered that it has a speed problem:
I think the reason of this problem is that...
Inside the 60fps ticker there are two forEach loops, one for the rects array and other for the lasers array. So, when there's 5 rects and 5 lasers in the canvas, it'll loop 5 times in the first forEach and five times in the second at each frame, and each forEach function has lots of ifs in it, making the game slow. How can I change that to something less CPU-intensive?
If you know a bigger speed problem in this minigame, feel free to help me to solve it too.
Here's my entire code:
I highly recommend you to see the JSFiddle instead of the code below, since there's more than 400 lines.
<!DOCTYPE html>
<html>
<head>
<title>VelJS α</title>
<!-- This app was coded by Tiago Marinho -->
<!-- Do not leech it! -->
<link rel="shortcut icon" href="http://i.imgur.com/Jja8mvg.png">
<!-- EaselJS: -->
<script src="http://static.tumblr.com/uzcr0ts/uzIn1l1v2/easeljs-0.7.1.min.js"></script>
<script src="http://pastebin.com/raw.php?i=W4S2mtCp"></script>
<!-- jQuery: -->
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<script>
(function () {
// Primary vars (stage, circle, rects):
var stage,
circle, // Hero!
rects = [], // Platforms
lasers = [];
// Velocity vars:
var xvel = 0, // X Velocity
yvel = 0, // Y Velocity
xvelpast = 0,
yvelpast = 0;
// Keyvars (up, left, right, down):
var up = false, // W or arrow up
left = false, // A or arrow left
right = false, // D or arrow right
down = false; // S or arrow down
// Other vars (maxvel, col, pause):
var maxvel = 256, // Maximum velocity
col = false, // Collision detection helper (returns true if collided side-to-side)
pause = false;
// Volatility vars (rmdir, pastrmdir):
var rmdir = 0,
pastrmdir = 0;
// Main part (aka creating stage, creating circle, etc):
function init() {
stage = new createjs.Stage("canvas");
// Creating circle:
var circle = new createjs.Shape();
circle.radius = 11;
circle.graphics.beginFill("#fff").beginStroke("white").drawCircle(circle.radius - 0.5, circle.radius - 0.5, circle.radius);
circle.width = circle.radius * 2;
circle.height = circle.radius * 2;
stage.addChild(circle);
setTimeout(function () {
// newobj(W, H, X, Y)
newobj("laser", 3, 244, stage.canvas.width / 2 - 125, stage.canvas.height / 4 * 3 - 247);
newobj("rect", 125, 3, stage.canvas.width / 2 - 125, stage.canvas.height / 4 * 3 - 250);
}, 250); // Wait until first tick finishes and stage is resized to 100%, then calculate the middle of canvas.
// User Input (Redirect input to Input Handler):
// Keydown:
document.addEventListener("keydown", function (evt) {
if (evt.keyCode == 87 || evt.keyCode == 38) { // up
up = true;
}
if (evt.keyCode == 65 || evt.keyCode == 37) { // left
left = true;
}
if (evt.keyCode == 68 || evt.keyCode == 39) { // right
right = true;
}
if (evt.keyCode == 83 || evt.keyCode == 40) { // down
down = true;
}
if (evt.keyCode == 8 || evt.keyCode == 80) { // del/p
if (pause == false) {
xvelpast = xvel;
yvelpast = yvel;
pause = true;
var fadestep = 0;
for (var i = 1; i > 0; i -= 0.1) {
i = parseFloat(i.toFixed(1));
fadestep++;
fadeFill("circle", i, fadestep);
rects.forEach(function (rect) {
fadeFill("rect", i, fadestep);
});
}
} else {
pause = false;
xvel = xvelpast;
yvel = yvelpast;
var fadestep = 0;
for (var i = 0; i <= 1; i += 0.1) {
i = parseFloat(i.toFixed(1));
fadestep++;
fadeFill("circle", i, fadestep);
rects.forEach(function (rect) {
fadeFill("rect", i, fadestep);
});
}
}
}
});
// Keyup:
document.addEventListener("keyup", function (evt) {
if (evt.keyCode == 87 || evt.keyCode == 38) { // up
up = false;
}
if (evt.keyCode == 65 || evt.keyCode == 37) { // left
left = false;
}
if (evt.keyCode == 68 || evt.keyCode == 39) { // right
right = false;
}
if (evt.keyCode == 83 || evt.keyCode == 40) { // down
down = false;
}
});
// Functions:
// Fade beginFill to a lower alpha:
function fadeFill(obj, i, t) {
setTimeout(function () {
if (obj == "circle") {
circle.graphics.clear().beginFill("rgba(255,255,255," + i + ")").beginStroke("white").drawCircle(circle.radius, circle.radius, circle.radius).endFill();
}
if (obj == "rect") {
for (var r = 0; r < rects.length; r++) {
rects[r].graphics.clear().beginFill("rgba(255,255,255," + i + ")").beginStroke("white").drawRect(0, 0, rects[r].width, rects[r].height).endFill();
}
}
}, t * 20);
};
// To create new rects:
function newobj(type, w, h, x, y) {
if (type == "rect") {
var rect = new createjs.Shape();
rect.graphics.beginFill("#fff").beginStroke("white").drawRect(0, 0, w, h);
rect.width = w + 1;
rect.height = h + 1;
rect.y = Math.round(y) + 0.5;
rect.x = Math.round(x) + 0.5;
stage.addChild(rect);
rects.push(rect);
}
if (type == "laser") {
var laser = new createjs.Shape();
if (w >= h) {
laser.graphics.beginFill("#c22").drawRect(0, 0, w, 1);
laser.width = w;
laser.height = 1;
} else {
laser.graphics.beginFill("#c22").drawRect(0, 0, 1, h);
laser.width = 1;
laser.height = h;
}
laser.shadow = new createjs.Shadow("#ff0000", 0, 0, 5);
laser.y = Math.round(y);
laser.x = Math.round(x);
stage.addChild(laser);
lasers.push(laser);
}
}
// Collision recoil:
function cls(clsdir) {
if (clsdir == "top") {
if (yvel <= 4) {
yvel = 0;
} else {
yvel = Math.round(yvel * -0.5);
}
}
if (clsdir == "left") {
if (xvel <= 4) {
xvel = 0;
} else {
xvel = Math.round(xvel * -0.5);
}
}
if (clsdir == "right") {
if (xvel >= -4) {
xvel = 0;
} else {
xvel = Math.round(xvel * -0.5);
}
}
if (clsdir == "bottom") {
if (yvel >= -4) {
yvel = 0;
} else {
yvel = Math.round(yvel * -0.5);
}
}
col = true;
}
// Die:
function die() {
circle.alpha = 1;
createjs.Tween.get(circle).to({
alpha: 0
}, 250).call(handleComplete);
function handleComplete() {
circle.x = stage.canvas.width / 2 - circle.radius;
circle.y = stage.canvas.height / 2 - circle.radius;
createjs.Tween.get(circle).to({
alpha: 1
}, 250);
yvel = 0;
xvel = 0;
yvelpast = 0;
xvelpast = 0;
}
yvel = yvel/2;
xvel = xvel/2;
}
// Set Intervals:
// Speed/Score:
setInterval(function () {
if (pause == false) {
speed = Math.abs(xvel) + Math.abs(yvel);
$(".speed").html("Speed: " + speed);
} else {
speed = Math.abs(xvelpast) + Math.abs(yvelpast);
$(".speed").html("Speed: " + speed + " (Paused)");
}
}, 175);
// Tick:
createjs.Ticker.on("tick", tick);
createjs.Ticker.setFPS(60);
function tick(event) {
// Input Handler:
if (up == true) {
yvel -= 2;
} else {
if (yvel < 0) {
yvel++;
}
}
if (left == true) {
xvel -= 2;
} else {
if (xvel < 0) {
xvel++;
}
}
if (right == true) {
xvel += 2;
} else {
if (xvel > 0) {
xvel--;
}
}
if (down == true) {
yvel += 2;
} else {
if (yvel > 0) {
yvel--;
}
}
// Volatility:
pastrmdir = rmdir;
rmdir = Math.floor((Math.random() * 20) + 1);
if (rmdir == 1 && pastrmdir != 4) {
yvel--;
}
if (rmdir == 2 && pastrmdir != 3) {
xvel--;
}
if (rmdir == 3 && pastrmdir != 2) {
xvel++;
}
if (rmdir == 4 && pastrmdir != 1) {
yvel++;
}
// Velocity limiter:
if (xvel > maxvel || xvel < maxvel * -1) {
(xvel > 0) ? xvel = maxvel : xvel = maxvel * -1;
}
if (yvel > maxvel || yvel < maxvel * -1) {
(yvel > 0) ? yvel = maxvel : yvel = maxvel * -1;
}
// Collision handler:
// xvel and yvel modifications must be before this!
rects.forEach(function (rect) { // Affect all rects
// Collision detection:
// (This MUST BE after every change in xvel/yvel)
// Next circle position calculation:
nextposx = circle.x + event.delta / 1000 * xvel * 30,
nextposy = circle.y + event.delta / 1000 * yvel * 30;
// Collision between objects (Rect and Circle):
if (nextposy + circle.height > rect.y && circle.y + circle.height < rect.y && circle.x + circle.width > rect.x && circle.x < rect.x + rect.width) {
cls("top");
}
if (nextposx + circle.width > rect.x && circle.x + circle.width < rect.x && circle.y + circle.height > rect.y && circle.y < rect.y + rect.height) {
cls("left");
}
if (nextposx < rect.x + rect.width && circle.x > rect.x + rect.width && circle.y + circle.height > rect.y && circle.y < rect.y + rect.height) {
cls("right");
}
if (nextposy < rect.y + rect.height && circle.y > rect.y + rect.height && circle.x + circle.width > rect.x && circle.x < rect.x + rect.width) {
cls("bottom");
}
rects.forEach(function (rect) {
// Check side-to-side collisions with other rects:
if (nextposy + circle.height > rect.y && circle.y + circle.height < rect.y && circle.x + circle.width > rect.x && circle.x < rect.x + rect.width) {
col = true;
}
if (nextposx + circle.width > rect.x && circle.x + circle.width < rect.x && circle.y + circle.height > rect.y && circle.y < rect.y + rect.height) {
col = true;
}
if (nextposx < rect.x + rect.width && circle.x > rect.x + rect.width && circle.y + circle.height > rect.y && circle.y < rect.y + rect.height) {
col = true;
}
if (nextposy < rect.y + rect.height && circle.y > rect.y + rect.height && circle.x + circle.width > rect.x && circle.x < rect.x + rect.width) {
col = true;
}
});
// Edge-to-edge collision between objects (Rect and Circle) - Note that this will not occur if a side-to-side collision occurred in the current frame!:
if (nextposy + circle.height > rect.y &&
nextposx + circle.width > rect.x &&
nextposx < rect.x + rect.width &&
nextposy < rect.y + rect.height &&
col == false) {
if (circle.y + circle.height < rect.y &&
circle.x + circle.width < rect.x) {
cls("top");
cls("left");
}
if (circle.y > rect.y + rect.height &&
circle.x + circle.width < rect.x) {
cls("bottom");
cls("left");
}
if (circle.y + circle.height < rect.y &&
circle.x > rect.x + rect.width) {
cls("top");
cls("right");
}
if (circle.y > rect.y + rect.height &&
circle.x > rect.x + rect.width) {
cls("bottom");
cls("right");
}
}
col = false;
// Stage collision:
if (nextposy < 0) { // Collided with TOP of stage. Trust me.
cls("bottom"); // Inverted clsdir is proposital!
}
if (nextposx < 0) {
cls("right");
}
if (nextposx + circle.width > stage.canvas.width) {
cls("left");
}
if (nextposy + circle.height > stage.canvas.height) {
cls("top");
}
});
// Laser collision handler:
lasers.forEach(function (laser) {
laser.alpha = Math.random() + 0.5;
nextposx = circle.x + event.delta / 1000 * xvel * 30,
nextposy = circle.y + event.delta / 1000 * yvel * 30;
if (nextposy + circle.height > laser.y && circle.y + circle.height < laser.y && circle.x + circle.width > laser.x && circle.x < laser.x + laser.width) {
circle.y = laser.y-circle.height;
die();
}
if (nextposx + circle.width > laser.x && circle.x + circle.width < laser.x && circle.y + circle.height > laser.y && circle.y < laser.y + laser.height) {
circle.x = laser.x-circle.width;
die();
}
if (nextposx < laser.x + laser.width && circle.x > laser.x + laser.width && circle.y + circle.height > laser.y && circle.y < laser.y + laser.height) {
circle.x = laser.x+laser.width;
die();
}
if (nextposy < laser.y + laser.height && circle.y > laser.y + laser.height && circle.x + circle.width > laser.x && circle.x < laser.x + laser.width) {
circle.y = laser.y+laser.height;
die();
}
});
// Velocity:
if (pause == true) {
xvel = 0;
yvel = 0;
}
circle.x += event.delta / 1000 * xvel * 20;
circle.y += event.delta / 1000 * yvel * 20;
// Stage.canvas 100% width and height:
stage.canvas.width = window.innerWidth;
stage.canvas.height = window.innerHeight;
// Update stage:
stage.update(event);
}
setTimeout(function () {
// Centre circle:
circle.x = stage.canvas.width / 2 - circle.radius;
circle.y = stage.canvas.height / 2 - circle.radius;
// Fade-in after loading:
$(".speed").css({
opacity: 1
});
$("canvas").css({
opacity: 1
});
}, 500);
}
$(function () {
init();
});
})();
</script>
<style>
* {
margin: 0;
}
html,
body {
-webkit-font-smoothing: antialiased;
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
font-weight: 300;
color: #fff;
background-color: #181818
}
.build {
position: absolute;
bottom: 5px;
right: 5px;
color: rgba(255, 255, 255, 0.05)
}
canvas {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
opacity: 0;
position: absolute;
top: 0;
left: 0;
-moz-transition: 5s ease;
-o-transition: 5s ease;
-webkit-transition: 5s ease;
transition: 5s ease
}
.speed {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
opacity: 0;
position: absolute;
top: 5px;
left: 5px;
color: #fff;
font-size: 16px;
-moz-transition: 5s ease;
-o-transition: 5s ease;
-webkit-transition: 5s ease;
transition: 5s ease
}
h2 {
text-align: center;
font-size: 22px;
font-weight: 700
}
p {
font-size: 16px;
margin: 0
}
</style>
</head>
<body>
<p class="speed"></p>
<p class="build">α256</p>
<canvas id="canvas">
<h2>Your browser doesn't support Canvas.</h2>
<p>Switch to <b>Chrome 33</b>, <b>Firefox 27</b> or <b>Safari 7</b>.</p>
</canvas>
</body>
</html>
JSFiddle
The game logic isn't really a problem, the reason it's slow is because you "create" a new canvas every tick by setting the width and height:
stage.canvas.width = window.innerWidth;
stage.canvas.height = window.innerHeight;
So, even if you set the canvas width and height to the same values they had, under the hood pretty much a new canvas is constructed. If you remove the lines above from the game loop it should run smoothly.
Just set the canvas width and height once and then listen for window resize and set it when the browser window changes size.
Running logic in a tick can be expensive, as is updating the canvas each frame. If you can, a lower framerate could be advisable - since it often isn't necessary to run at 60fps. If you want to keep refreshing the canvas at that rate, and you have any particularly expensive functions, such as pathfinding, collision, etc - you could always decouple that from your update loop, so it isn't running quite as often.
Note that stage updating can be very expensive, especially with vectors. If you can, find a solution that lets you cache vector content as bitmaps, and update the vector as little as possible. In your case, since you are just fading the fill - you might separate the fill from the outline, cache them separately, and then use alpha on the fill object. If you have a lot of shapes, you can reuse the caches between them, and you will see huge performance gains.
Best of luck!
Related
In vanilla javascript, if I made two squares, one square with movement like:
const context = document.getElementById("canvasmulti").getContext("2d");
canvasmulti.width = window.innerWidth;
canvasmulti.height = window.innerHeight;
//CHARACTER:
const square = {
height: 75,
jumping: true,
width: 75,
x: canvasmulti.width - 75,
xVelocity: 0,
y: canvasmulti.height / 2,
yVelocity: 0,
jumpHeight: 30
};
const square2 = {
height: 75,
jumping: true,
width: 75,
x: 500,
xVelocity: 0,
y: canvasmulti.height - 75,
yVelocity: 0,
jumpHeight: 30
};
//MOVEMENT:
const controller = {
left: false,
right: false,
up: false,
keyListener: function (event) {
let key_state = (event.type == "keydown") ? true : false;
switch (event.keyCode) {
case 37: // left arrow
controller.left = key_state;
break;
case 38: // up arrow
controller.up = key_state;
break;
case 39: // right arrow
controller.right = key_state;
break;
}
}
};
const loop = function () {
//controller one
if (controller.up && square.jumping == false) {
square.yVelocity -=square.jumpHeight;
square.jumping = true;}
if (controller.left) {
square.xVelocity -= 0.5;}
if (controller.right) {
square.xVelocity += 0.5;}
//controller one
square.yVelocity += 1.5;// gravity
square.x += square.xVelocity;
square.y += square.yVelocity;
square.xVelocity *= 0.9;// friction
square.yVelocity *= 0.9;// friction
// if square1 is falling below floor line
if (square.y > canvasmulti.height - 75) {
square.jumping = false;
square.y = canvasmulti.height - 75;
square.yVelocity = 0;
}
// Creates and fills square1 for each frame
context.fillStyle = "#8DAA9D"; // hex for cube color
context.beginPath();
context.rect(square.x, square.y, square.width, square.height);
context.fill();
//creates and full square2 for each frame
context.fillStyle = "#781818"; // hex for cube color
context.beginPath();
context.rect(square2.x, square2.y, square2.width, square2.height);
context.fill();
if (square.x <= square2.x + square2.width &&
square.x >= square2.x + square2.width - square2.width && square.y >= square2.y &&
square.y <= square2.y + square2.height)
{square.x = square2.x + square2.width; // set it to a position where they don't overlap
square.xVelocity = 0;}; // left square1 touching right square2
if (square.x + square.width >= square2.x && square.x + square.width <= square2.x + square2.width &&
square.y >= square2.y && square.y <= square2.y + square2.height ||
square.x + square.width >= square2.x && square.x + square.width <= square2.x + square2.width &&
square.y + square.height >= square2.y && square.y + square.height >= square2.y + square2.height)
{square.x = square2.x - square.width;
square.xVelocity = 0;}; // right square1 touching left square2
if (square.x <= square2.x + square2.width - 0.1 &&
square.x >= square2.x && square.y + square.height >= square2.y)
{
square.y = square2.y - square.height;
square.yVelocity = 0;}; // bottom left square1 touching top square2
if (square.x + square.width <= square2.x + square2.width &&
square.x + square.width >= square2.x + 0.1 && square.y + square.height >= square2.y)
{square.y = square2.y - square.height;
square.yVelocity = 0;}; // bottom right square1 touching top square2
window.requestAnimationFrame(loop);
};
//square1
window.addEventListener("keydown", controller.keyListener)
window.addEventListener("keyup", controller.keyListener);
window.requestAnimationFrame(loop);
Since the starting co-ordinate of the square is in the top left, if I wanted to make an if statement saying if the opposite corner was past a point (say square2.x), but also over a point(say square2.y), how would I do that?? You can't add or multiply the co-ordinates, because that would just make one of the co-ordinates larger rather than combining them both.
I am making a script that auto generates planets see codepen for example.
But the problem I have is that i want to make it less pixelated and I am having some problems doing that if i make the tiles 70 * 70 and tile size to 10 * 10 pixels it works fine. But i want to have it set to something like tiles 360 * 360 and size to 1 or 2 pixels. But when I try to do that I get maximum call stack error. So I tried to use the requestAnimationFrame but then it take ages to load is there a way to speed up the process?
var tileNum = 0;
var tiles;
var colorsLand;
var colorsWater;
var size = 360;
var tileSize = 2;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
window.onload = function () {
generatePlanet();
}
function generatePlanet() {
tileNum = 0;
tiles = [{ x: 0, y: 0, land: false }];
//Retrive colors
colorsLand = interpolateColors("rgb(" + getColor(true) + ")", "rgb(" + getColor(true) + ")", 6000);
colorsWater = interpolateColors("rgb(" + getColor(false) + ")", "rgb(" + getColor(false) + ")", 6000);
//Creates a array of my tiles and sets either water or land to them and calculates the % of being water/land
for (var i = 0; i < (size * size); i++) {
var currentTile = tiles[tiles.length - 1];
if (currentTile.x <= (size - 1)) {
var isLand = false;
if (currentTile.land == true || tiles.length > size && tiles[tiles.length - size].land == true) {
isLand = (Math.floor(Math.random() * 100) + 1) > 35;
}
else if (currentTile.land == true || tiles.length > size &&
(tiles[tiles.length - 1].land == true ||
tiles[tiles.length - size].land == true)) {
isLand = (Math.floor(Math.random() * 100) + 1) > size;
}
else {
isLand = (Math.floor(Math.random() * 100) + 1) > 99;
}
tiles.push({ x: currentTile.x + 1, y: currentTile.y, land: isLand });
}
else {
tiles.push({ x: 0, y: currentTile.y + 1, land: isLand });
}
}
drawPlanet()
}
//retrive a random color if it's a land tile i want it dark water i want light
function getColor(land) {
while (true) {
var r = Math.floor(Math.random() * 256) + 1
var g = Math.floor(Math.random() * 256) + 1
var b = Math.floor(Math.random() * 256) + 1
var hsp = Math.sqrt(
0.299 * (r * r) +
0.587 * (g * g) +
0.114 * (b * b)
);
//light color
if (hsp > 127.5 && land == false) {
return r + "," + g + "," + b;
}
//dark color
else if (hsp < 127.5 && land == true) {
return r + "," + g + "," + b;
}
}
}
//these 2 functions interpolateColor(s) takes 2 colors and gives me 'steps' colors between
function interpolateColors(color1, color2, steps) {
var stepFactor = 1 / (steps - 1),
interpolatedColorArray = [];
color1 = color1.match(/\d+/g).map(Number);
color2 = color2.match(/\d+/g).map(Number);
for (var i = 0; i < steps; i++) {
interpolatedColorArray.push(interpolateColor(color1, color2, stepFactor * i));
}
return interpolatedColorArray;
}
function interpolateColor(color1, color2, factor) {
if (arguments.length < 3) {
factor = 0.5;
}
var result = color1.slice();
for (var i = 0; i < 3; i++) {
result[i] = Math.round(result[i] + factor * (color2[i] - color1[i]));
}
return result;
};
//retrives a random color for land
function rndLandColor() {
return 'rgb(' + colorsLand[Math.floor(Math.random() * 5999) + 1] + ')';
}
//retrives a random color for water
function rndWaterColor() {
return 'rgb(' + colorsWater[Math.floor(Math.random() * 5999) + 1] + ')';
}
function drawPlanet() {
var RAF;
var i = 0, j = 0;
function animate() {
ctx.beginPath();
//fill in holes in the land that is bigger then 1
var score = 0;
if (tiles[tileNum - (size + 1)] !== undefined && tiles[tileNum + (size + 1)] !== undefined) {
if (tiles[tileNum].land == false) {
score++;
}
if (tiles[tileNum - 1].land == true) {
score++;
}
if (tiles[tileNum + 1].land == true) {
score++;
}
if (tiles[tileNum + (size + 1)].land == true) {
score++;
}
if (tiles[tileNum - (size + 1)].land == true) {
score++;
}
}
if (score >= 3) {
ctx.fillStyle = rndLandColor();
}
//cover single land tiles with water (if water tile is up,down,left and right of this tile)
else if (
tiles[tileNum - (size + 1)] !== undefined &&
tiles[tileNum + (size + 1)] !== undefined &&
tiles[tileNum - 1].land == false &&
tiles[tileNum + 1].land == false &&
tiles[tileNum - (size + 1)].land == false &&
tiles[tileNum + (size + 1)].land == false) {
ctx.fillStyle = rndWaterColor();
}
//cover single water tiles with land (if land tile is up,down,left and right of this tile)
else if (
tiles[tileNum - (size + 1)] !== undefined &&
tiles[tileNum + (size + 1)] !== undefined &&
tiles[tileNum - 1].land == true &&
tiles[tileNum + 1].land == true &&
tiles[tileNum - (size + 1)].land == true &&
tiles[tileNum + (size + 1)].land == true) {
ctx.fillStyle = rndLandColor();
}
//cover tile with land
else if (tiles[tileNum] !== undefined && tiles[tileNum].land == true) {
ctx.fillStyle = rndLandColor();
}
//cover tile with water
else if (tiles[tileNum] !== undefined && tiles[tileNum].land == false) {
ctx.fillStyle = rndWaterColor();
}
tileNum++;
ctx.fill();
ctx.closePath();
ctx.fillRect(tileSize * j, tileSize * i, tileSize, tileSize);
j++;
if (j >= (size + 1)) {
i += 1;
j = 0;
if (i >= (size + 1)) {
cancelAnimationFrame(RAF);
}
}
RAF = requestAnimationFrame(function () {
animate();
});
}
animate();
}
#canvas {
border: 10px solid #000000;
border-radius: 50%;
background-color: aquamarine;
}
.container {
width: 720px;
height: 720px;
position: relative;
}
.gradient {
position: absolute;
height: 730px;
width: 730px;
top: 0;
left: 0;
border-radius: 50%;
opacity: 0.8;
}
<div class="container">
<img class="gradient" src="https://www.mediafire.com/convkey/1f5a/cgu50lw1ehcp4fq6g.jpg" />
<canvas id="canvas" width="710" height="710"></canvas>
</div>
Do not use canvas drawing methods to perform pixel art.
Filling a path is a relatively slow operation, to draw pixels through fillRect(), is almost never the correct way.
Instead one should prefer manipulating an ImageData object directly, and paint it on the canvas only once.
If you need to set up a scale, then use an unscaled ImageBitmap, put it on your context and then upscale it using drawImage.
Here is an updated version of your script, where I did apply some not-so minor improvements like not generating colors for out-of-screen pixels, along with this ImageData manipulation technique.
It now runs fast enough to be launched synchronously. But if you need to improve it even more, note that your getColor seems rather inneficient, but I didn't touch it.
var tileNum = 0;
var tiles;
var colorsLand;
var colorsWater;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var tileSize = 2;
canvas.width = canvas.height = 710;
// 'size' should be your grid size, not the actual pixel size painted on screen
var size = Math.ceil(canvas.width / tileSize);
function generatePlanet() {
tileNum = 0;
tiles = [{
x: 0,
y: 0,
land: false
}];
//Retrive colors
colorsLand = interpolateColors(getColor(true), getColor(true), 6000);
colorsWater = interpolateColors(getColor(false), getColor(false), 6000);
//Creates a array of my tiles and sets either water or land to them and calculates the % of being water/land
for (var i = 0; i < (size * size); i++) {
var currentTile = tiles[tiles.length - 1];
if (currentTile.x <= (size - 1)) {
var isLand = false;
if (currentTile.land == true || tiles.length > size && tiles[tiles.length - size].land == true) {
isLand = (Math.floor(Math.random() * 100) + 1) > 35;
} else if (currentTile.land == true || tiles.length > size &&
(tiles[tiles.length - 1].land == true ||
tiles[tiles.length - size].land == true)) {
isLand = (Math.floor(Math.random() * 100) + 1) > size;
} else {
isLand = (Math.floor(Math.random() * 100) + 1) > 99;
}
tiles.push({
x: currentTile.x + 1,
y: currentTile.y,
land: isLand
});
} else {
tiles.push({
x: 0,
y: currentTile.y + 1,
land: isLand
});
}
}
drawPlanet()
}
//retrive a random color if it's a land tile i want it dark water i want light
function getColor(land) {
while (true) {
var r = Math.floor(Math.random() * 256) + 1
var g = Math.floor(Math.random() * 256) + 1
var b = Math.floor(Math.random() * 256) + 1
var hsp = Math.sqrt(
0.299 * (r * r) +
0.587 * (g * g) +
0.114 * (b * b)
);
//light color
if (hsp > 127.5 && land == false) {
return [r,g,b];
}
//dark color
else if (hsp < 127.5 && land == true) {
return [r,g,b];
}
}
}
//these 2 functions interpolateColor(s) takes 2 colors and gives me 'steps' colors between
function interpolateColors(color1, color2, steps) {
var stepFactor = 1 / (steps - 1),
interpolatedColorArray = [];
for (var i = 0; i < steps; i++) {
interpolatedColorArray.push(toUint32AARRGGBB(interpolateColor(color1, color2, stepFactor * i)));
}
return interpolatedColorArray;
}
function toUint32AARRGGBB(arr) {
return Number('0xFF' + arr.map(toHexString2).join(''))
}
function toHexString2(val) {
return val.toString(16)
.padStart(2, '0'); // padStart may need a polyfill
}
function interpolateColor(color1, color2, factor) {
if (arguments.length < 3) {
factor = 0.5;
}
var result = color1.slice();
for (var i = 0; i < 3; i++) {
result[i] = Math.round(result[i] + factor * (color2[i] - color1[i]));
}
return result;
};
//retrives a random color for land
function rndLandColor() {
return colorsLand[Math.floor(Math.random() * 5999) + 1];
}
//retrives a random color for water
function rndWaterColor() {
return colorsWater[Math.floor(Math.random() * 5999) + 1];
}
// now drawing synchronously:
function drawPlanet() {
var gridsize = size;
var rad = gridsize / 2;
// generate an ImageData, the size of our pixel grid
var imgData = new ImageData(gridsize, gridsize);
// work directly on Uint32 values (0xAARRGGBB on LittleEndian)
var data = new Uint32Array(imgData.data.buffer);
var score, y, x;
for (y = 0; y < gridsize; y++) {
for (x = 0; x < gridsize; x++) {
score = 0;
// if we are outside of the inner area
if (Math.hypot(rad - x, rad - y) > rad + 2) {
tileNum++;
continue;
}
//fill in holes in the land that is bigger then 1
if (tiles[tileNum - (gridsize + 1)] !== undefined && tiles[tileNum + (size + 1)] !== undefined) {
if (tiles[tileNum].land == false) {
score++;
}
if (tiles[tileNum - 1].land == true) {
score++;
}
if (tiles[tileNum + 1].land == true) {
score++;
}
if (tiles[tileNum + (gridsize + 1)].land == true) {
score++;
}
if (tiles[tileNum - (gridsize + 1)].land == true) {
score++;
}
}
if (score >= 3) {
color = rndLandColor();
}
//cover single land tiles with water (if water tile is up,down,left and right of this tile)
else if (
tiles[tileNum - (gridsize + 1)] !== undefined &&
tiles[tileNum + (gridsize + 1)] !== undefined &&
tiles[tileNum - 1].land == false &&
tiles[tileNum + 1].land == false &&
tiles[tileNum - (gridsize + 1)].land == false &&
tiles[tileNum + (gridsize + 1)].land == false) {
color = rndWaterColor();
}
//cover single water tiles with land (if land tile is up,down,left and right of this tile)
else if (
tiles[tileNum - (gridsize + 1)] !== undefined &&
tiles[tileNum + (gridsize + 1)] !== undefined &&
tiles[tileNum - 1].land == true &&
tiles[tileNum + 1].land == true &&
tiles[tileNum - (gridsize + 1)].land == true &&
tiles[tileNum + (gridsize + 1)].land == true) {
color = rndLandColor();
}
//cover tile with land
else if (tiles[tileNum] !== undefined && tiles[tileNum].land == true) {
color = rndLandColor();
}
//cover tile with water
else if (tiles[tileNum] !== undefined && tiles[tileNum].land == false) {
color = rndWaterColor();
}
tileNum++;
data[(y * gridsize) + x] = color;
}
}
// all done populating the ImageData
// put it on the context at scale(1,1)
ctx.putImageData(imgData, 0, 0);
// remove antialiasing
ctx.imageSmoothingEnabled = false;
// up-scale
ctx.scale(tileSize, tileSize);
// draw the canvas over itself
ctx.drawImage(ctx.canvas, 0, 0);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
generatePlanet();
#canvas {
border: 10px solid #000000;
border-radius: 50%;
background-color: aquamarine;
}
.container {
width: 720px;
height: 720px;
position: relative;
}
.gradient {
position: absolute;
height: 730px;
width: 730px;
top: 0;
left: 0;
border-radius: 50%;
opacity: 0.8;
}
<div class="container">
<img class="gradient" src="https://www.mediafire.com/convkey/1f5a/cgu50lw1ehcp4fq6g.jpg" />
<canvas id="canvas" width="710" height="710"></canvas>
</div>
Now, if I were in your position, I think I would even start looking somewhere else completely. For what you want to do, it seems that some noise generator could be more efficient with a more realistic output.
There is one such noise generator available in SVG filters, and hence accessible to Canvas2D API, however I have to admit that controlling it is not that easy.
But if you wish to take a look at it, here is a rough playground:
const controls = new Set();
function randColor() {
return '#' + (Math.floor((Math.random()*0xFFFFFF)))
.toString(16)
.padStart(6, 0);
}
function makeInput(type, options) {
return Object.assign(document.createElement('input'), {type}, options);
}
class Control {
constructor() {
this.color = makeInput('color', {value: randColor()});
this.freq = makeInput('range', {min: 0.0001, max:1, step: 0.0001, value: Math.random() / 20});
this.numOctaves = makeInput('range', {min: 1, max:10, step: 1, value: 7});
this.opacity = makeInput('range', {min:0.01, max:1, step: 0.001, value:1});
this.seed = Math.random() * 1000;
const remover = document.createElement('span');
remover.textContent = 'x';
remover.classList.add('remover');
const container = document.createElement('div');
container.classList.add('control');
container.append(
"color: ", this.color,
"baseFrequency: ", this.freq,
"numOctaves: ", this.numOctaves,
"opacity", this.opacity,
remover
);
document.querySelector('.controls').append(container);
remover.onclick = e => {
container.remove();
controls.delete(this);
draw();
};
this.color.oninput = this.freq.oninput = this.numOctaves.oninput = this.opacity.oninput = draw;
}
}
for(let i=0; i<3; i++) {
controls.add(new Control());
}
const main = c.getContext('2d');
const ctx = c.cloneNode().getContext('2d');
main.arc(c.width/2, c.height/2, Math.min(c.width, c.height)/2,0,Math.PI*2);
draw();
add_control.onclick = e => {
controls.add(new Control());
draw();
}
function draw() {
main.globalCompositeOperation = 'source-over';
main.clearRect(0,0,c.width,c.height);
controls.forEach(control => {
ctx.globalCompositeOperation = 'source-over';
ctx.filter = "none";
ctx.clearRect(0,0,c.width,c.height);
// update <filter>
turb.setAttribute('seed', control.seed);
turb.setAttribute('baseFrequency', control.freq.value);
turb.setAttribute('numOctaves', control.numOctaves.value);
// draw black and transp
ctx.filter = "url(#myFilter)"
ctx.fillRect(0,0,c.width, c.width);
// do the composition with solid color
ctx.filter = "none"
ctx.fillStyle = control.color.value;
ctx.globalCompositeOperation = 'source-in'
ctx.fillRect(0,0,c.width, c.width);
main.globalAlpha = control.opacity.value;
// draw on visible context
main.drawImage(ctx.canvas, 0,0)
main.globalAlpha = 1;
});
// cut-out as a circle
main.globalCompositeOperation = 'destination-in';
main.fill()
}
.control {
display: inline-block;
border: 1px solid;
padding: 6px;
position: relative
}
.control input {
display: block;
}
.control span {
position: absolute;
top: 6px;
right: 6px;
cursor: pointer;
}
#canvas {
border: 10px solid #000000;
border-radius: 50%;
background-color: aquamarine;
}
.container {
width: 360px;
height: 360px;
position: relative;
}
.gradient {
position: absolute;
height: 360px;
width: 360px;
top: 0;
left: 0;
border-radius: 50%;
opacity: 0.8;
}
<div class="controls">
<button id="add_control">add new layer</button><br>
</div>
<div class="container">
<canvas id="c" width="360" height="360"></canvas>
<svg>
<filter id="myFilter">
<feTurbulence type="fractalNoise" baseFrequency="0.045"
id="turb" result="turb"/>
<feComponentTransfer in="turb" result="contrast">
<feFuncR type="linear" slope="1.6" intercept="-0.15"/>
<feFuncG type="linear" slope="1.6" intercept="-0.15"/>
<feFuncB type="linear" slope="1.6" intercept="-0.15"/>
</feComponentTransfer>
<feColorMatrix in="contrast"
type="luminanceToAlpha" result="alpha"/>
</filter>
</svg>
<img class="gradient" src="https://www.mediafire.com/convkey/1f5a/cgu50lw1ehcp4fq6g.jpg" />
</div>
I am making an app for the Chrome Web Store. It is a clone of the Doodle Jump game. When I test and load it as an unpacked extension, this error keeps coming up.
Uncaught TypeError: Cannot read property 'getContext' of null
My code is here:
Javascript
function startGame() {
// RequestAnimFrame: a browser API for getting smooth animations
window.requestAnimFrame = (function() {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var width = 422,
height = 552;
canvas.width = width;
canvas.height = height;
//Variables for game
var platforms = [],
image = document.getElementById("sprite"),
player, platformCount = 10,
position = 0,
gravity = 0.2,
animloop,
flag = 0,
menuloop, broken = 0,
dir, score = 0, firstRun = true;
//Base object
var Base = function() {
this.height = 5;
this.width = width;
//Sprite clipping
this.cx = 0;
this.cy = 614;
this.cwidth = 100;
this.cheight = 5;
this.moved = 0;
this.x = 0;
this.y = height - this.height;
this.draw = function() {
try {
ctx.drawImage(image, this.cx, this.cy, this.cwidth, this.cheight, this.x, this.y, this.width, this.height);
} catch (e) {}
};
};
var base = new Base();
//Player object
var Player = function() {
this.vy = 11;
this.vx = 0;
this.isMovingLeft = false;
this.isMovingRight = false;
this.isDead = false;
this.width = 55;
this.height = 40;
//Sprite clipping
this.cx = 0;
this.cy = 0;
this.cwidth = 110;
this.cheight = 80;
this.dir = "left";
this.x = width / 2 - this.width / 2;
this.y = height;
//Function to draw it
this.draw = function() {
try {
if (this.dir == "right") this.cy = 121;
else if (this.dir == "left") this.cy = 201;
else if (this.dir == "right_land") this.cy = 289;
else if (this.dir == "left_land") this.cy = 371;
ctx.drawImage(image, this.cx, this.cy, this.cwidth, this.cheight, this.x, this.y, this.width, this.height);
} catch (e) {}
};
this.jump = function() {
this.vy = -8;
document.getElementById('audio').innerHTML='<audio src="sounds/pup.mp3" preload="auto" autoplay autobuffer></audio>'
};
this.jumpHigh = function() {
this.vy = -16;
document.getElementById('audio').innerHTML='<audio src="sounds/high.mp3" preload="auto" autoplay autobuffer></audio>'
};
};
player = new Player();
//Platform class
function Platform() {
this.width = 70;
this.height = 17;
this.x = Math.random() * (width - this.width);
this.y = position;
position += (height / platformCount);
this.flag = 0;
this.state = 0;
//Sprite clipping
this.cx = 0;
this.cy = 0;
this.cwidth = 105;
this.cheight = 31;
//Function to draw it
this.draw = function() {
try {
if (this.type == 1) this.cy = 0;
else if (this.type == 2) this.cy = 61;
else if (this.type == 3 && this.flag === 0) this.cy = 31;
else if (this.type == 3 && this.flag == 1) this.cy = 1000;
else if (this.type == 4 && this.state === 0) this.cy = 90;
else if (this.type == 4 && this.state == 1) this.cy = 1000;
ctx.drawImage(image, this.cx, this.cy, this.cwidth, this.cheight, this.x, this.y, this.width, this.height);
} catch (e) {}
};
//Platform types
//1: Normal
//2: Moving
//3: Breakable (Go through)
//4: Vanishable
//Setting the probability of which type of platforms should be shown at what score
if (score >= 5000) this.types = [2, 3, 3, 3, 4, 4, 4, 4];
else if (score >= 2000 && score < 5000) this.types = [2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4];
else if (score >= 1000 && score < 2000) this.types = [2, 2, 2, 3, 3, 3, 3, 3];
else if (score >= 500 && score < 1000) this.types = [1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3];
else if (score >= 100 && score < 500) this.types = [1, 1, 1, 1, 2, 2];
else this.types = [1];
this.type = this.types[Math.floor(Math.random() * this.types.length)];
//We can't have two consecutive breakable platforms otherwise it will be impossible to reach another platform sometimes!
if (this.type == 3 && broken < 1) {
broken++;
} else if (this.type == 3 && broken >= 1) {
this.type = 1;
broken = 0;
}
this.moved = 0;
this.vx = 1;
}
for (var i = 0; i < platformCount; i++) {
platforms.push(new Platform());
}
//Broken platform object
var Platform_broken_substitute = function() {
this.height = 30;
this.width = 70;
this.x = 0;
this.y = 0;
//Sprite clipping
this.cx = 0;
this.cy = 554;
this.cwidth = 105;
this.cheight = 60;
this.appearance = false;
this.draw = function() {
try {
if (this.appearance === true) ctx.drawImage(image, this.cx, this.cy, this.cwidth, this.cheight, this.x, this.y, this.width, this.height);
else return;
} catch (e) {}
};
};
var platform_broken_substitute = new Platform_broken_substitute();
//Spring Class
var spring = function() {
this.x = 0;
this.y = 0;
this.width = 26;
this.height = 30;
//Sprite clipping
this.cx = 0;
this.cy = 0;
this.cwidth = 45;
this.cheight = 53;
this.state = 0;
this.draw = function() {
try {
if (this.state === 0) this.cy = 445;
else if (this.state == 1) this.cy = 501;
ctx.drawImage(image, this.cx, this.cy, this.cwidth, this.cheight, this.x, this.y, this.width, this.height);
} catch (e) {}
};
};
var Spring = new spring();
function init() {
//Variables for the game
var dir = "left",
jumpCount = 0;
firstRun = false;
//Function for clearing canvas in each consecutive frame
function paintCanvas() {
ctx.clearRect(0, 0, width, height);
}
//Player related calculations and functions
function playerCalc() {
if (dir == "left") {
player.dir = "left";
if (player.vy < -7 && player.vy > -15) player.dir = "left_land";
} else if (dir == "right") {
player.dir = "right";
if (player.vy < -7 && player.vy > -15) player.dir = "right_land";
}
//Adding keyboard controls
document.onkeydown = function(e) {
var key = e.keyCode;
if (key == 37) {
dir = "left";
player.isMovingLeft = true;
} else if (key == 39) {
dir = "right";
player.isMovingRight = true;
}
if(key == 32) {
if(firstRun === true)
init();
else
reset();
}
};
document.onkeyup = function(e) {
var key = e.keyCode;
if (key == 37) {
dir = "left";
player.isMovingLeft = false;
} else if (key == 39) {
dir = "right";
player.isMovingRight = false;
}
};
//Accelerations produces when the user hold the keys
if (player.isMovingLeft === true) {
player.x += player.vx;
player.vx -= 0.15;
} else {
player.x += player.vx;
if (player.vx < 0) player.vx += 0.1;
}
if (player.isMovingRight === true) {
player.x += player.vx;
player.vx += 0.15;
} else {
player.x += player.vx;
if (player.vx > 0) player.vx -= 0.1;
}
//Jump the player when it hits the base
if ((player.y + player.height) > base.y && base.y < height) player.jump();
//Gameover if it hits the bottom
if (base.y > height && (player.y + player.height) > height && player.isDead != "lol") {
player.isDead = true;
document.getElementById('audio').innerHTML='<audio src="sounds/gameover.mp3" preload="auto" autoplay autobuffer></audio>'
}
//Make the player move through walls
if (player.x > width) player.x = 0 - player.width;
else if (player.x < 0 - player.width) player.x = width;
//Movement of player affected by gravity
if (player.y >= (height / 2) - (player.height / 2)) {
player.y += player.vy;
player.vy += gravity;
}
//When the player reaches half height, move the platforms to create the illusion of scrolling and recreate the platforms that are out of viewport...
else {
platforms.forEach(function(p, i) {
if (player.vy < 0) {
p.y -= player.vy;
}
if (p.y > height) {
platforms[i] = new Platform();
platforms[i].y = p.y - height;
}
});
base.y -= player.vy;
player.vy += gravity;
if (player.vy >= 0) {
player.y += player.vy;
player.vy += gravity;
}
score++;
}
//Make the player jump when it collides with platforms
collides();
if (player.isDead === true) gameOver();
}
//Spring algorithms
function springCalc() {
var s = Spring;
var p = platforms[0];
if (p.type == 1 || p.type == 2) {
s.x = p.x + p.width / 2 - s.width / 2;
s.y = p.y - p.height - 10;
if (s.y > height / 1.1) s.state = 0;
s.draw();
} else {
s.x = 0 - s.width;
s.y = 0 - s.height;
}
}
//Platform's horizontal movement (and falling) algo
function platformCalc() {
var subs = platform_broken_substitute;
platforms.forEach(function(p, i) {
if (p.type == 2) {
if (p.x < 0 || p.x + p.width > width) p.vx *= -1;
p.x += p.vx;
}
if (p.flag == 1 && subs.appearance === false && jumpCount === 0) {
subs.x = p.x;
subs.y = p.y;
subs.appearance = true;
jumpCount++;
}
p.draw();
});
if (subs.appearance === true) {
subs.draw();
subs.y += 8;
}
if (subs.y > height) subs.appearance = false;
}
function collides() {
//Platforms
platforms.forEach(function(p, i) {
if (player.vy > 0 && p.state === 0 && (player.x + 15 < p.x + p.width) && (player.x + player.width - 15 > p.x) && (player.y + player.height > p.y) && (player.y + player.height < p.y + p.height)) {
if (p.type == 3 && p.flag === 0) {
p.flag = 1;
jumpCount = 0;
return;
} else if (p.type == 4 && p.state === 0) {
player.jump();
p.state = 1;
} else if (p.flag == 1) return;
else {
player.jump();
}
}
});
//Springs
var s = Spring;
if (player.vy > 0 && (s.state === 0) && (player.x + 15 < s.x + s.width) && (player.x + player.width - 15 > s.x) && (player.y + player.height > s.y) && (player.y + player.height < s.y + s.height)) {
s.state = 1;
player.jumpHigh();
}
}
function updateScore() {
var scoreText = document.getElementById("score");
scoreText.innerHTML = score;
}
function gameOver() {
platforms.forEach(function(p, i) {
p.y -= 12;
});
if(player.y > height/2 && flag === 0) {
player.y -= 8;
player.vy = 0;
}
else if(player.y < height / 2) flag = 1;
else if(player.y + player.height > height) {
showGoMenu();
hideScore();
player.isDead = "lol";
}
}
//Function to update everything
function update() {
paintCanvas();
platformCalc();
springCalc();
playerCalc();
player.draw();
base.draw();
updateScore();
}
menuLoop = function(){return;};
animloop = function() {
update();
requestAnimFrame(animloop);
};
animloop();
hideMenu();
showScore();
}
function reset() {
hideGoMenu();
showScore();
player.isDead = false;
flag = 0;
position = 0;
score = 0;
base = new Base();
player = new Player();
Spring = new spring();
platform_broken_substitute = new Platform_broken_substitute();
platforms = [];
for (var i = 0; i < platformCount; i++) {
platforms.push(new Platform());
}
}
//Hides the menu
function hideMenu() {
var menu = document.getElementById("mainMenu");
menu.style.zIndex = -1;
}
//Shows the game over menu
function showGoMenu() {
var menu = document.getElementById("gameOverMenu");
menu.style.zIndex = 1;
menu.style.visibility = "visible";
var scoreText = document.getElementById("go_score");
scoreText.innerHTML = "Ваш результат " + score + " очков!";
}
//Hides the game over menu
function hideGoMenu() {
var menu = document.getElementById("gameOverMenu");
menu.style.zIndex = -1;
menu.style.visibility = "hidden";
}
//Show ScoreBoard
function showScore() {
var menu = document.getElementById("scoreBoard");
menu.style.zIndex = 1;
}
//Hide ScoreBoard
function hideScore() {
var menu = document.getElementById("scoreBoard");
menu.style.zIndex = -1;
}
function playerJump() {
player.y += player.vy;
player.vy += gravity;
if (player.vy > 0 &&
(player.x + 15 < 260) &&
(player.x + player.width - 15 > 155) &&
(player.y + player.height > 475) &&
(player.y + player.height < 500))
player.jump();
if (dir == "left") {
player.dir = "left";
if (player.vy < -7 && player.vy > -15) player.dir = "left_land";
} else if (dir == "right") {
player.dir = "right";
if (player.vy < -7 && player.vy > -15) player.dir = "right_land";
}
//Adding keyboard controls
document.onkeydown = function(e) {
var key = e.keyCode;
if (key == 37) {
dir = "left";
player.isMovingLeft = true;
} else if (key == 39) {
dir = "right";
player.isMovingRight = true;
}
if(key == 32) {
if(firstRun === true) {
init();
firstRun = false;
}
else
reset();
}
};
document.onkeyup = function(e) {
var key = e.keyCode;
if (key == 37) {
dir = "left";
player.isMovingLeft = false;
} else if (key == 39) {
dir = "right";
player.isMovingRight = false;
}
};
//Accelerations produces when the user hold the keys
if (player.isMovingLeft === true) {
player.x += player.vx;
player.vx -= 0.15;
} else {
player.x += player.vx;
if (player.vx < 0) player.vx += 0.1;
}
if (player.isMovingRight === true) {
player.x += player.vx;
player.vx += 0.15;
} else {
player.x += player.vx;
if (player.vx > 0) player.vx -= 0.1;
}
//Jump the player when it hits the base
if ((player.y + player.height) > base.y && base.y < height) player.jump();
//Make the player move through walls
if (player.x > width) player.x = 0 - player.width;
else if (player.x < 0 - player.width) player.x = width;
player.draw();
}
function update() {
ctx.clearRect(0, 0, width, height);
playerJump();
}
menuLoop = function() {
update();
requestAnimFrame(menuLoop);
};
menuLoop();
}
document.addEventListener("DOMContentLoaded", startGame, false);
<!DOCTYPE HTML>
<html>
<head>
<title>Doodle Jump</title>
<style type="text/css">
#import url(Gloria%20Hallelujah);
*{box-sizing: border-box;}
body {
margin: 0; padding: 0;
font-family: 'Gloria Hallelujah', cursive;
}
.container {
height: 552px;
width: 422px;
position: relative;
margin: 20px auto;
overflow: hidden;
}
canvas {
height: 552px;
width: 422px;
display: block;
background: url(images/Y0BMP.png) top left;
}
#scoreBoard {
width: 420px;
height: 50px;
background: rgba(182, 200, 220, 0.7);
position: absolute;
top: -3px;
left: 0;
z-index: -1;
border-image: url(images/5BBsR.png) 100 5 round;
}
#scoreBoard p {
font-size: 20px;
padding: 0;
line-height: 47px;
margin: 0px 0 0 5px;
}
img {display: none}
#mainMenu, #gameOverMenu {
height: 100%;
width: 100%;
text-align: center;
position: absolute;
top: 0;
left: 0;
z-index: 2;
}
#gameOverMenu {
visibility: hidden;
}
h2, h3, h1 {font-weight: normal}
h1 {
font-size: 60px;
color: #5a5816;
transform: rotate(-10deg);
margin: 0px;
}
h3 {text-align: right; margin: -10px 20px 0 0; color: #5e96be}
h3 a {color: #5a5816}
.button {
width: 105px;
height: 31px;
background: url(images/2WEhF.png) 0 0 no-repeat;
display: block;
color: #000;
font-size: 12px;
line-height: 31px;
text-decoration: none;
position: absolute;
left: 50%;
bottom: 50px;
margin-left: -53px;
}
.info {position: absolute; right: 20px; bottom: 00px; margin: 0; color: green}
.info .key {
width: 16px;
height: 16px;
background: url(images/2WEhF.png) no-repeat;
text-indent: -9999px;
display: inline-block;
}
.info .key.left {background-position: -92px -621px;}
.info .key.right {background-position: -92px -641px;}
</style>
</head>
<body><div style="position:absolute;z-index:999;padding:10px;top:0;right:0;background:#000" id="sxz03">
<div style="float:right;padding-left:10px"><img onclick="document.getElementById('sxz03').style.display='none'" src="images/x.gif" width="15" alt="" /></div>
<br>
<div style="position:absolute;top:-100px"></div>
</div>
<div class="container">
<canvas id="canvas"></canvas>
<div id="mainMenu">
<h1>doodle jump</h1>
<h3>A HTML5 game</h3>
<a class="button" href="javascript:init()">Play</a>
</div>
<div id="gameOverMenu">
<h1>Game Over!</h1>
<h3 id="go_score">Your score was ...</h3>
<a class="button" href="javascript:reset()">Play Again</a>
</div>
<!-- Preloading image ;) -->
<img id="sprite" src="images/2WEhF.png"/>
<div id="scoreBoard">
<p id="score">0</p>
</div>
</div>
<div id="audio"></div>
<script src="jquery.min.js"></script>
<script src="game.js"></script>
</body>
</html>
So when I run this, the intro works, but when I click the play button, nothing happens. If you want to run this code, I will put a download link for the folder that I used in this question.
I want to resize squares drawn on canvas (created by clicking) with mouse by moving from corners. Should only resize selected square. I have four different functions for each corner, and after resizing from more than one corner functions keep executing. How to prevent this?
var isMouseDown = false;
var canvas;
var ctx, size;
$(document).ready(function() {
canvas = $('#area')[0];
ctx = canvas.getContext('2d');
$('#area').on('mousedown', canvasClick);
$('#area').on('mouseup', up);
});
function up(event) {
isMouseDown = false;
resizing = false;
console.log("up");
return;
}
function Square(x, y, size) {
this.x = x;
this.y = y;
this.size = size;
this.isSelected = false;
}
var x, y;
function addSquare(event) {
x = event.pageX - canvas.offsetLeft;
y = event.pageY - canvas.offsetTop;
var size = parseInt(Math.random() * 81 + 49);
x -= size / 2;
y -= size / 2;
ctx.fillRect(x, y, size, size);
var square = new Square(x, y, size);
squares.push(square);
drawSquares();
}
var squares = new Array;
var previousSelectedSquare;
function drawSquares() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < squares.length; i++) {
var Square = squares[i];
ctx.beginPath();
ctx.fillStyle = "orange";
ctx.rect(Square.x, Square.y, Square.size, Square.size);
ctx.strokeStyle = "black";
if (Square.isSelected) {
ctx.lineWidth = 4;
} else {
ctx.lineWidth = 1;
}
ctx.fill();
ctx.stroke();
}
}
function canvasClick(event) {
isMouseDown = true;
x = event.pageX - canvas.offsetLeft;
y = event.pageY - canvas.offsetTop;
if (squares.length < 1) {
addSquare(event);
return;
}
var Square;
if (previousSelectedSquare) {
var prevX = previousSelectedSquare.x;
var prevY = previousSelectedSquare.y;
size = previousSelectedSquare.size;
}
var change = 7;
if (((x <= prevX + size + change) && (x >= prevX + size - change)) ||
((x >= prevX - change) && (x <= prevX + change)))
if (((y <= prevY + size + change) && (y >= prevY + size - change)) ||
((y <= prevY + change) && (y >= prevY - change))) {
console.log("borders");
$('#area').on('mousemove', resize(event));
return;
}
for (var i = squares.length - 1; i >= 0; i--) {
Square = squares[i];
if ((x >= Square.x && x <= Square.x + Square.size) && (y <= Square.y + Square.size && y >= Square.y)) {
Square.isSelected = true;
if (previousSelectedSquare != null)
previousSelectedSquare.isSelected = false;
if (previousSelectedSquare == Square)
previousSelectedSquare = null;
else
previousSelectedSquare = Square;
drawSquares();
return;
}
if (i === 0) {
addSquare(event);
return;
}
}
return;
}
// Part that resizes
var resizing = false;
function resize(event) {
if (!isMouseDown)
return;
resizing = true;
x = (event.pageX - canvas.offsetLeft);
y = (event.pageY - canvas.offsetTop);
size = previousSelectedSquare.size;
var centreX = previousSelectedSquare.x + size / 2;
var centreY = previousSelectedSquare.y + size / 2;
if (x > centreX && y < centreY) {
topr = true;
//top right
$('#area').on('mousemove', topR);
return;
}
if (x < centreX && y < centreY) {
//top left
$('#area').on('mousemove', topL);
return;
}
if (x > centreX && y > centreY) {
//bot right
$('#area').on('mousemove', botR);
return;
}
if (x < centreX && y > centreY) {
//bot left
$('#area').on('mousemove', botL);
return;
}
resizing = false;
return;
}
function topR(event) {
if (!resizing || !isMouseDown)
return;
$('#area').on('mouseup', up);
console.log("top right");
size = previousSelectedSquare.size;
var yb = previousSelectedSquare.y + size;
if (parseInt(yb - (event.pageY - canvas.offsetTop)) < 60)
return;
previousSelectedSquare.size = parseInt(yb - (event.pageY - canvas.offsetTop));
previousSelectedSquare.y = parseInt(event.pageY - canvas.offsetTop);
drawSquares();
return;
}
function topL(event) {
if (!resizing || !isMouseDown)
return;
$('#area').on('mouseup', up);
console.log("top left");
x = previousSelectedSquare.x + previousSelectedSquare.size;
y = previousSelectedSquare.y + previousSelectedSquare.size;
if (parseInt(y - (event.pageY - canvas.offsetTop)) < 60)
return;
previousSelectedSquare.size = parseInt(y - (event.pageY - canvas.offsetTop));
previousSelectedSquare.y = parseInt(event.pageY - canvas.offsetTop);
previousSelectedSquare.x = parseInt(event.pageX - canvas.offsetLeft);
drawSquares();
return;
}
function botR(event) {
if (!resizing || !isMouseDown)
return;
$('#area').on('mouseup', up);
console.log("bot right");
size = previousSelectedSquare.size;
if ((event.pageX - canvas.offsetLeft - previousSelectedSquare.x + size) / 2 < 60)
return;
previousSelectedSquare.size = parseInt((event.pageX - canvas.offsetLeft - previousSelectedSquare.x + size) / 2);
drawSquares();
return;
}
function botL(event) {
if (!resizing || !isMouseDown)
return;
$('#area').on('mouseup', up);
console.log("bot left");
var xr = previousSelectedSquare.x + previousSelectedSquare.size;
if (xr - event.pageX - canvas.offsetLeft < 60)
return;
previousSelectedSquare.size = parseInt(previousSelectedSquare.x + previousSelectedSquare.size - (event.pageX - canvas.offsetLeft));
previousSelectedSquare.x = parseInt(event.pageX - canvas.offsetLeft);
drawSquares();
return;
}
#area {
margin-top: 0;
border: 4px black solid;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id="area" width="1250" height="550"></canvas>
Using namespace events, after you check the position, remove other handlers then add the one for the current corner.
e.g.: $('#area').off('mousemove.botR').off('mousemove.topL').off('mousemove.botL').on('mousemove.topR', topR);
var isMouseDown = false;
var canvas;
var ctx, size;
$(document).ready(function() {
canvas = $('#area')[0];
ctx = canvas.getContext('2d');
$('#area').on('mousedown', canvasClick);
$('#area').on('mouseup', up);
});
function up(event) {
isMouseDown = false;
resizing = false;
console.log("up");
return;
}
function Square(x, y, size) {
this.x = x;
this.y = y;
this.size = size;
this.isSelected = false;
}
var x, y;
function addSquare(event) {
x = event.pageX - canvas.offsetLeft;
y = event.pageY - canvas.offsetTop;
var size = parseInt(Math.random() * 81 + 49);
x -= size / 2;
y -= size / 2;
ctx.fillRect(x, y, size, size);
var square = new Square(x, y, size);
squares.push(square);
drawSquares();
}
var squares = new Array;
var previousSelectedSquare;
function drawSquares() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < squares.length; i++) {
var Square = squares[i];
ctx.beginPath();
ctx.fillStyle = "orange";
ctx.rect(Square.x, Square.y, Square.size, Square.size);
ctx.strokeStyle = "black";
if (Square.isSelected) {
ctx.lineWidth = 4;
} else {
ctx.lineWidth = 1;
}
ctx.fill();
ctx.stroke();
}
}
function canvasClick(event) {
isMouseDown = true;
x = event.pageX - canvas.offsetLeft;
y = event.pageY - canvas.offsetTop;
if (squares.length < 1) {
addSquare(event);
return;
}
var Square;
if (previousSelectedSquare) {
var prevX = previousSelectedSquare.x;
var prevY = previousSelectedSquare.y;
size = previousSelectedSquare.size;
}
var change = 7;
if (((x <= prevX + size + change) && (x >= prevX + size - change)) ||
((x >= prevX - change) && (x <= prevX + change)))
if (((y <= prevY + size + change) && (y >= prevY + size - change)) ||
((y <= prevY + change) && (y >= prevY - change))) {
console.log("borders");
$('#area').on('mousemove', resize(event));
return;
}
for (var i = squares.length - 1; i >= 0; i--) {
Square = squares[i];
if ((x >= Square.x && x <= Square.x + Square.size) && (y <= Square.y + Square.size && y >= Square.y)) {
Square.isSelected = true;
if (previousSelectedSquare != null)
previousSelectedSquare.isSelected = false;
if (previousSelectedSquare == Square)
previousSelectedSquare = null;
else
previousSelectedSquare = Square;
drawSquares();
return;
}
if (i === 0) {
addSquare(event);
return;
}
}
return;
}
// Part that resizes
var resizing = false;
function resize(event) {
if (!isMouseDown)
return;
resizing = true;
x = (event.pageX - canvas.offsetLeft);
y = (event.pageY - canvas.offsetTop);
size = previousSelectedSquare.size;
var centreX = previousSelectedSquare.x + size / 2;
var centreY = previousSelectedSquare.y + size / 2;
if (x > centreX && y < centreY) {
topr = true;
//top right
$('#area').off('mousemove.botR').off('mousemove.topL').off('mousemove.botL').on('mousemove.topR', topR);
return;
}
if (x < centreX && y < centreY) {
//top left
$('#area').off('mousemove.topR').off('mousemove.botL').off('mousemove.botR').on('mousemove.topL', topL);
return;
}
if (x > centreX && y > centreY) {
//bot right
$('#area').off('mousemove.topR').off('mousemove.topL').off('mousemove.botL').on('mousemove.botR', botR);
return;
}
if (x < centreX && y > centreY) {
//bot left
$('#area').off('mousemove.topR').off('mousemove.topL').off('mousemove.botR').on('mousemove.botL', botL);
return;
}
resizing = false;
return;
}
function topR(event) {
if (!resizing || !isMouseDown)
return;
$('#area').on('mouseup', up);
console.log("top right");
size = previousSelectedSquare.size;
var yb = previousSelectedSquare.y + size;
if (parseInt(yb - (event.pageY - canvas.offsetTop)) < 60)
return;
previousSelectedSquare.size = parseInt(yb - (event.pageY - canvas.offsetTop));
previousSelectedSquare.y = parseInt(event.pageY - canvas.offsetTop);
drawSquares();
return;
}
function topL(event) {
if (!resizing || !isMouseDown)
return;
$('#area').on('mouseup', up);
console.log("top left");
x = previousSelectedSquare.x + previousSelectedSquare.size;
y = previousSelectedSquare.y + previousSelectedSquare.size;
if (parseInt(y - (event.pageY - canvas.offsetTop)) < 60)
return;
previousSelectedSquare.size = parseInt(y - (event.pageY - canvas.offsetTop));
previousSelectedSquare.y = parseInt(event.pageY - canvas.offsetTop);
previousSelectedSquare.x = parseInt(event.pageX - canvas.offsetLeft);
drawSquares();
return;
}
function botR(event) {
if (!resizing || !isMouseDown)
return;
$('#area').on('mouseup', up);
console.log("bot right");
size = previousSelectedSquare.size;
if ((event.pageX - canvas.offsetLeft - previousSelectedSquare.x + size) / 2 < 60)
return;
previousSelectedSquare.size = parseInt((event.pageX - canvas.offsetLeft - previousSelectedSquare.x + size) / 2);
drawSquares();
return;
}
function botL(event) {
if (!resizing || !isMouseDown)
return;
$('#area').on('mouseup', up);
console.log("bot left");
var xr = previousSelectedSquare.x + previousSelectedSquare.size;
if (xr - event.pageX - canvas.offsetLeft < 60)
return;
previousSelectedSquare.size = parseInt(previousSelectedSquare.x + previousSelectedSquare.size - (event.pageX - canvas.offsetLeft));
previousSelectedSquare.x = parseInt(event.pageX - canvas.offsetLeft);
drawSquares();
return;
}
#area {
margin-top: 0;
border: 4px black solid;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id="area" width="1250" height="550"></canvas>
How to limit the boundary of pan in fabric js?
I wrote the following code to limit the pan of my canvas but it is just not working. I want it to remember how much it has move outside of the canvas so that even after your pointer goes out of the canvas, you will have to move back the same amount to start dragging again. Currently, after the pointer move out of the canvas, the image will stick it. I cannot understand what I am doing wrong. Can someone help me?
Update
The following solution seems to work for zoom 1x. Still need to figure out how to solve it after zoom. But it is so verbose. Is there a better way to do this? https://jsfiddle.net/yxchng/0hL2khro/356/
canvas.on('mouse:move', function (opt) {
if (this.isDragging) {
var e = opt.e;
var zoom = canvas.getZoom();
var moveX = 0;
var moveY = 0;
if ((e.clientX - this.lastPosX) * this.offsetX > 0) {
this.offsetX += e.clientX - this.lastPosX;
} else if (e.clientX - this.lastPosX > 0 && this.offsetX < 0) {
moveX = e.clientX - this.lastPosX + this.offsetX;
if (moveX > 0) {
this.viewportTransform[4] += moveX;
this.offsetX = 0;
} else {
this.offsetX = moveX;
}
} else if (e.clientX - this.lastPosX < 0 && this.offsetX > 0) {
moveX = e.clientX - this.lastPosX + this.offsetX;
if (moveX < 0) {
this.viewportTransform[4] += moveX;
this.offsetX = 0;
} else {
this.offsetX = moveX;
}
} else {
this.viewportTransform[4] += e.clientX - this.lastPosX;
if (this.viewportTransform[4] < 0) {
this.offsetX = this.viewportTransform[4];
this.viewportTransform[4] = 0;
}
if (this.viewportTransform[4] > canvas.width) {
this.offsetX = this.viewportTransform[4] - canvas.width;
this.viewportTransform[4] = canvas.width;
}
}
if ((e.clientY - this.lastPosY) * this.offsetY > 0) {
this.offsetY += e.clientY - this.lastPosY;
} else if (e.clientY - this.lastPosY > 0 && this.offsetY < 0) {
moveY = e.clientY - this.lastPosY + this.offsetY;
if (moveY > 0) {
this.viewportTransform[5] += moveY;
this.offsetY = 0;
} else {
this.offsetY = moveY;
}
} else if (e.clientY - this.lastPosY < 0 && this.offsetY > 0) {
moveY = e.clientY - this.lastPosY + this.offsetY;
if (moveY < 0) {
this.viewportTransform[5] += moveY;
this.offsetY = 0;
} else {
this.offsetY = moveY;
}
} else {
this.viewportTransform[5] += e.clientY - this.lastPosY;
if (this.viewportTransform[5] < 0) {
this.offsetY = this.viewportTransform[5];
this.viewportTransform[5] = 0;
}
if (this.viewportTransform[5] > canvas.height) {
this.offsetY = this.viewportTransform[5] - canvas.height;
this.viewportTransform[5] = canvas.height;
}
}
this.requestRenderAll();
this.lastPosX = e.clientX;
this.lastPosY = e.clientY;
}
});