Canvas flood fill not filling to edge - javascript

I am using a flood fill algorithm to fill in circles drawn on the canvas. The issue I am having is that the algorithm isn't filling right up to the edge of the circle.
Here is the algorithm based on this blog post:
function paintLocation(startX, startY, r, g, b) {
var colorLayer = context1.getImageData(0, 0, canvasWidth, canvasHeight);
pixelPos = (startY * canvasWidth + startX) * 4;
startR = colorLayer.data[pixelPos];
startG = colorLayer.data[pixelPos + 1];
startB = colorLayer.data[pixelPos + 2];
var pixelStack = [
[startX, startY]
];
var drawingBoundTop = 0;
while (pixelStack.length) {
var newPos, x, y, pixelPos, reachLeft, reachRight;
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
pixelPos = (y * canvasWidth + x) * 4;
while (y-- >= drawingBoundTop && matchStartColor(colorLayer, pixelPos, startR, startG, startB)) {
pixelPos -= canvasWidth * 4;
}
pixelPos += canvasWidth * 4;
++y;
reachLeft = false;
reachRight = false;
while (y++ < canvasHeight - 1 && matchStartColor(colorLayer, pixelPos, startR, startG, startB)) {
colorPixel(colorLayer, pixelPos, r, g, b);
if (x > 0) {
if (matchStartColor(colorLayer, pixelPos - 4, startR, startG, startB)) {
if (!reachLeft) {
pixelStack.push([x - 1, y]);
reachLeft = true;
}
} else if (reachLeft) {
reachLeft = false;
}
}
if (x < canvasWidth - 1) {
if (matchStartColor(colorLayer, pixelPos + 4, startR, startG, startB)) {
if (!reachRight) {
pixelStack.push([x + 1, y]);
reachRight = true;
}
} else if (reachRight) {
reachRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
context1.putImageData(colorLayer, 0, 0);
}
Please see the JSFiddle or the below image to see what I mean. Clicking inside any circles will change the colour between yellow and black (the issue is far more visible with black).
I've read that the issue could be something to do with the anti-aliasing and I have tried turning it off with context1.imageSmoothingEnabled = true; but it didn't make a difference.
I have also tried changing my matchStartColour function as per this question but that doesn't help.
function matchStartColor(colorLayer, pixelPos, startR, startG, startB) {
var r = colorLayer.data[pixelPos];
var g = colorLayer.data[pixelPos + 1];
var b = colorLayer.data[pixelPos + 2];
return (r == startR && g == startG && b == startB);
}
I think it might have something to do with the fact that the circles have no fill colour and the background of the canvas isn't white but it is transparent black. I have tried changing the canvas background to white but that also didn't help.

Use flood fill to create a Mask
I just happened to do a floodFill the other day that addresses the problem of antialiased edges.
Rather than paint to the canvas directly, I paint to a byte array that is then used to create a mask. The mask allows for the alpha values to be set.
The fill can have a tolerance and a toleranceFade that control how it deals with colours that approch the tolerance value.
When pixel's difference between the start colour and tolerance are greater than (tolerance - toleranceFade) I set the alpha for that pixel to 255 - ((differance - (tolerance - toleranceFade)) / toleranceFade) * 255 which creates a nice smooth blend at the edges of lines. Though it does not work for all situations for high contrast situations it is an effective solution.
The example below shows the results of with and without the toleranceFade. The blue is without the toleranceFade, the red is with the tolerance set at 190 and the toleranceFade of 90.
You will have to play around with the setting to get the best results for your needs.
function showExample(){
var canvas = document.createElement("canvas");
canvas.width = 200;
canvas.height = 200;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
ctx.fillStyle = "white"
ctx.fillRect(0,0,canvas.width,canvas.height)
ctx.lineWidth = 4;
ctx.strokeStyle = "black"
ctx.beginPath();
ctx.arc(100,100,90,0,Math.PI * 2);
ctx.arc(120,100,60,0,Math.PI * 2);
ctx.stroke();
ctx.fillStyle = "blue";
floodFill.fill(100,100,1,ctx)
ctx.fillStyle = "red";
floodFill.fill(40,100,190,ctx,null,null,90)
}
// FloodFill2D from https://github.com/blindman67/FloodFill2D
var floodFill = (function(){
"use strict";
const extent = {
top : 0,
left : 0,
bottom : 0,
right : 0,
}
var keepMask = false; // if true then a mask of the filled area is returned as a canvas image
var extentOnly = false; // if true then the extent of the fill is returned
var copyPixels = false; // if true then creating a copy of filled pixels
var cutPixels = false; // if true and copyPixels true then filled pixels are removed
var useBoundingColor = false; // Set the colour to fill up to. Will not fill over this colour
var useCompareColor = false; // Rather than get the pixel at posX,posY use the compareColours
var red, green, blue, alpha; // compare colours if
var canvas,ctx;
function floodFill (posX, posY, tolerance, context2D, diagonal, area, toleranceFade) {
var w, h, painted, x, y, ind, sr, sg, sb, sa,imgData, data, data32, RGBA32, stack, stackPos, lookLeft, lookRight, i, colImgDat, differance, checkColour;
toleranceFade = toleranceFade !== undefined && toleranceFade !== null ? toleranceFade : 0;
diagonal = diagonal !== undefined && diagonal !== null ? diagonal : false;
area = area !== undefined && area !== null ? area : {};
area.x = area.x !== undefined ? area.x : 0;
area.y = area.y !== undefined ? area.y : 0;
area.w = area.w !== undefined ? area.w : context2D.canvas.width - area.x;
area.h = area.h !== undefined ? area.h : context2D.canvas.height - area.y;
// vet area is on the canvas.
if(area.x < 0){
area.w = area.x + area.w;
area.x = 0;
}
if(area.y < 0){
area.h = area.y + area.h;
area.y = 0;
}
if(area.x >= context2D.canvas.width || area.y >= context2D.canvas.height){
return false;
}
if(area.x + area.w > context2D.canvas.width){
area.w = context2D.canvas.width - area.x;
}
if(area.y + area.h > context2D.canvas.height){
area.h = context2D.canvas.height - area.y;
}
if(area.w <= 0 || area.h <= 0){
return false;
}
w = area.w; // width and height
h = area.h;
x = posX - area.x;
y = posY - area.y;
if(extentOnly){
extent.left = x; // set up extent
extent.right = x;
extent.top = y;
extent.bottom = y;
}
if(x < 0 || y < 0 || x >= w || y >= h){
return false; // fill start outside area. Don't do anything
}
if(tolerance === 255 && toleranceFade === 0 && ! keepMask){ // fill all
if(extentOnly){
extent.left = area.x; // set up extent
extent.right = area.x + w;
extent.top = area.y;
extent.bottom = area.y + h;
}
context2D.fillRect(area.x,area.y,w,h);
return true;
}
if(toleranceFade > 0){ // add one if on to get correct number of steps
toleranceFade += 1;
}
imgData = context2D.getImageData(area.x,area.y,area.w,area.h);
data = imgData.data; // image data to fill;
data32 = new Uint32Array(data.buffer);
painted = new Uint8ClampedArray(w*h); // byte array to mark painted area;
function checkColourAll(ind){
if( ind < 0 || painted[ind] > 0){ // test bounds
return false;
}
var ind4 = ind << 2; // get index of pixel
if((differance = Math.max( // get the max channel difference;
Math.abs(sr - data[ind4++]),
Math.abs(sg - data[ind4++]),
Math.abs(sb - data[ind4++]),
Math.abs(sa - data[ind4++])
)) > tolerance){
return false;
}
return true
}
// check to bounding colour
function checkColourBound(ind){
if( ind < 0 || painted[ind] > 0){ // test bounds
return false;
}
var ind4 = ind << 2; // get index of pixel
differance = 0;
if(sr === data[ind4] && sg === data[ind4 + 1] && sb === data[ind4 + 2] && sa === data[ind4 + 3]){
return false
}
return true
}
// this function checks the colour of only selected channels
function checkColourLimited(ind){ // check only colour channels that are not null
var dr,dg,db,da;
if( ind < 0 || painted[ind] > 0){ // test bounds
return false;
}
var ind4 = ind << 2; // get index of pixel
dr = dg = db = da = 0;
if(sr !== null && (dr = Math.abs(sr - data[ind4])) > tolerance){
return false;
}
if(sg !== null && (dg = Math.abs(sg - data[ind4 + 1])) > tolerance){
return false;
}
if(sb !== null && (db = Math.abs(sb - data[ind4 + 2])) > tolerance){
return false;
}
if(sa !== null && (da = Math.abs(sa - data[ind4 + 3])) > tolerance){
return false;
}
diferance = Math.max(dr, dg, db, da);
return true
}
// set which function to check colour with
checkColour = checkColourAll;
if(useBoundingColor){
sr = red;
sg = green;
sb = blue;
if(alpha === null){
ind = (y * w + x) << 2; // get the starting pixel index
sa = data[ind + 3];
}else{
sa = alpha;
}
checkColour = checkColourBound;
useBoundingColor = false;
}else if(useCompareColor){
sr = red;
sg = green;
sb = blue;
sa = alpha;
if(red === null || blue === null || green === null || alpha === null){
checkColour = checkColourLimited;
}
useCompareColor = false;
}else{
ind = (y * w + x) << 2; // get the starting pixel index
sr = data[ind]; // get the start colour that we will use tolerance against.
sg = data[ind + 1];
sb = data[ind + 2];
sa = data[ind + 3];
}
stack = []; // paint stack to find new pixels to paint
lookLeft = false; // test directions
lookRight = false;
stackPos = 0;
stack[stackPos++] = x;
stack[stackPos++] = y;
while (stackPos > 0) { // do while pixels on the stack
y = stack[--stackPos]; // get the pixel y
x = stack[--stackPos]; // get the pixel x
ind = x + y * w;
while (checkColour(ind - w)) { // find the top most pixel within tollerance;
y -= 1;
ind -= w;
}
//checkTop left and right if allowing diagonal painting
if(diagonal && y > 0){
if(x > 0 && !checkColour(ind - 1) && checkColour(ind - w - 1)){
stack[stackPos++] = x - 1;
stack[stackPos++] = y - 1;
}
if(x < w - 1 && !checkColour(ind + 1) && checkColour(ind - w + 1)){
stack[stackPos++] = x + 1;
stack[stackPos++] = y - 1;
}
}
lookLeft = false; // set look directions
lookRight = false; // only look is a pixel left or right was blocked
while (checkColour(ind) && y < h) { // move down till no more room
if(toleranceFade > 0 && differance >= tolerance-toleranceFade){
painted[ind] = 255 - (((differance - (tolerance - toleranceFade)) / toleranceFade) * 255);
painted[ind] = painted[ind] === 0 ? 1 : painted[ind]; // min value must be 1
}else{
painted[ind] = 255;
}
if(extentOnly){
extent.left = x < extent.left ? x : extent.left; // Faster than using Math.min
extent.right = x > extent.right ? x : extent.right; // Faster than using Math.min
extent.top = y < extent.top ? y : extent.top; // Faster than using Math.max
extent.bottom = y > extent.bottom ? y : extent.bottom; // Faster than using Math.max
}
if (checkColour(ind - 1) && x > 0) { // check left is blocked
if (!lookLeft) {
stack[stackPos++] = x - 1;
stack[stackPos++] = y;
lookLeft = true;
}
} else if (lookLeft) {
lookLeft = false;
}
if (checkColour(ind + 1) && x < w -1) { // check right is blocked
if (!lookRight) {
stack[stackPos++] = x + 1;
stack[stackPos++] = y;
lookRight = true;
}
} else if (lookRight) {
lookRight = false;
}
y += 1; // move down one pixel
ind += w;
}
if(diagonal && y < h){ // check for diagonal areas and push them to be painted
if(checkColour(ind - 1) && !lookLeft && x > 0){
stack[stackPos++] = x - 1;
stack[stackPos++] = y;
}
if(checkColour(ind + 1) && !lookRight && x < w - 1){
stack[stackPos++] = x + 1;
stack[stackPos++] = y;
}
}
}
if(extentOnly){
extent.top += area.y;
extent.bottom += area.y;
extent.left += area.x;
extent.right += area.x;
return true;
}
canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
ctx = canvas.getContext("2d");
ctx.fillStyle = context2D.fillStyle;
ctx.fillRect(0, 0, w, h);
colImgDat = ctx.getImageData(0, 0, w, h);
if(copyPixels){
i = 0;
ind = 0;
if(cutPixels){
while(i < painted.length){
if(painted[i] > 0){
colImgDat.data[ind] = data[ind];
colImgDat.data[ind + 1] = data[ind + 1];
colImgDat.data[ind + 2] = data[ind + 2];
colImgDat.data[ind + 3] = data[ind + 3] * (painted[i] / 255);
data[ind + 3] = 255 - painted[i];
}else{
colImgDat.data[ind + 3] = 0;
}
i ++;
ind += 4;
}
context2D.putImageData(imgData, area.x, area.y);
}else{
while(i < painted.length){
if(painted[i] > 0){
colImgDat.data[ind] = data[ind];
colImgDat.data[ind + 1] = data[ind + 1];
colImgDat.data[ind + 2] = data[ind + 2];
colImgDat.data[ind + 3] = data[ind + 3] * (painted[i] / 255);
}else{
colImgDat.data[ind + 3] = 0;
}
i ++;
ind += 4;
}
}
ctx.putImageData(colImgDat,0,0);
return true;
}else{
i = 0;
ind = 3;
while(i < painted.length){
colImgDat.data[ind] = painted[i];
i ++;
ind += 4;
}
ctx.putImageData(colImgDat,0,0);
}
if(! keepMask){
context2D.drawImage(canvas,area.x,area.y,w,h);
}
return true;
}
return {
fill : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade);
ctx = undefined;
canvas = undefined;
},
getMask : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
keepMask = true;
floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade);
ctx = undefined;
keepMask = false;
return canvas;
},
getExtent : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
extentOnly = true;
if(floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade)){
extentOnly = false;
return {
top : extent.top,
left : extent.left,
right : extent.right,
bottom : extent.bottom,
width : extent.right - extent.left,
height : extent.bottom - extent.top,
}
}
extentOnly = false;
return null;
},
cut : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
cutPixels = true;
copyPixels = true;
floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade);
cutPixels = false;
copyPixels = false;
ctx = undefined;
return canvas;
},
copy : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
cutPixels = false;
copyPixels = true;
floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade);
copyPixels = false;
ctx = undefined;
return canvas;
},
setCompareValues : function(R,G,B,A){
if(R === null && G === null && B === null && A === null){
return;
}
red = R;
green = G;
blue = B;
alpha = A;
useBoundingColor = false;
useCompareColor = true;
},
setBoundingColor : function(R,G,B,A){
red = R;
green = G;
blue = B;
alpha = A;
useCompareColor = false;
useBoundingColor = true;
}
}
}());
showExample();
Red floodFill.fill(40,100,190,ctx,null,null,90) tolerance 190, tolerance fade 90<br>Blue floodFill.fill(100,100,1,ctx) tolerance 1.<br>
For more info see readme at Github FloodFill2D

Related

making a rondom maze generated map

i am a begginer in javascript and i am trying to make a maze game, i followed the instructions on yt to generate random maze map. but when we came to the end and i tried to generete it. it wouldnt work. then i then i checked it in validator and console if there are any problems with it and i couldnt find any of it. so i here i am asking anyone could help me.
this is my code:
let maze = document.querySelector(".maze");
let ctx = maze.getContext("2d");
let current;
class Maze{
constructor(size,rows,columns){
this.size = size;
this.rows = rows;
this.columns = columns;
this.grid = [];
this.stack = [];
}
setup(){
for(let r = 0; r < this.rows; r++){
let row = [];
for (let c = 0; c < this.columns; c++){
let cell = new Cell(r,c, this.grid,this.size);
row.push(cell);
}
this.grid.push(row);
}
current = this.grid[0][0];
}
draw(){
maze.width=this.size;
maze.height=this.size;
maze.style.background = "black";
current.visited = true;
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.columns; c++) {
let grid = this.grid;
grid[r][c].show(this.size, this.rows, this.columns);
}
}
let next = current.checkNeighbours();
if (next) {
next.visited = true;
this.stack.push(current);
current.highlight(this.columns);
current.removeWalls(current, next);
current = next;
}else if (this.stack.length > 0) {
let cell = this.stack.pop();
current = cell;
current.highlight(this.columns);
}
if(this.stack.length == 0){
return;
}
window.requestAnimationFrame(() => {
this.draw();
});
}
}
class Cell{
constructor(rowNum,colNum,parentGrid,parentSize){
this.rowNum = rowNum;
this.colNum = colNum;
this.parentGrid = parentGrid;
this.parentSize = parentSize;
this.visited = false;
this.walls = {
topWall : true,
rightWall : true,
bottomWall : true,
leftwall : true,
};
}
checkNeighbours() {
let grid = this.parentGrid;
let row = this.rowNum;
let col = this.colNum;
let neighbours = [];
let top = row !== 0 ? grid[row-1][col] : undefined;
let right = col !== grid.length-1 ? grid[row][col+1] : undefined;
let bottom = row !== grid.length-1 ? grid[row+1][col] : undefined;
let left = col !== 0 ? grid[row][col-1] : undefined;
if(top && !top.visited) neighbours.push(top);
if(right && !right.visited) neighbours.push(right);
if(bottom && !bottom.visited) neighbours.push(bottom);
if(left && !left.visited) neighbours.push(left);
if(neighbours,length !== 0){
let random = Math.floor(Math.random() * neighbours.length);
return neighbours[random];
}else{
return undefined;
}
}
drawTopWall(x,y,size,columns){
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + size / columns, y);
ctx.stroke();
}
drawRightWall(x,y,size,columns,rows){
ctx.beginPath();
ctx.moveTo(x + size / columns, y);
ctx.lineTo(x + size / columns, y + size / rows);
ctx.stroke();
}
drawBottomWall(x,y,size,columns,rows){
ctx.beginPath();
ctx.moveTo(x, y + size / rows);
ctx.lineTo(x + size / columns, y + size / rows);
ctx.stroke();
}
drawLeftWall(x,y,size,columns,rows){
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y + size / rows);
ctx.stroke();
}
highlight(columns){
let x = (this.colNum * this.parentSize) / columns + 1;
let y = (this.rowNum * this.parentSize) / columns + 1;
ctx.fillStyle = "purple";
ctx.fillRect(
x,
y,
this.parentSize / columns - 3,
this.parentSize / columns - 3
);
}
removeWall(cell1,cell2){
let x = cell1.colNum - cell2.colNum;
if (x === 1) {
cell1.walls.leftWall = false;
cell2.walls.rightWall = false;
} else if (x === -1) {
cell1.walls.rightWall = false;
cell2.walls.leftWall = false;
}
let y = cell1.rowNum - cell2.rowNum;
if (y === 1) {
cell1.walls.topWall = false;
cell2.walls.bottomWall = false;
} else if (y === -1) {
cell1.walls.bottomWall = false;
cell2.walls.topWall = false;
}
}
show(size,rows,columns){
let x = (this.colNum * size) / columns;
let y = (this.rowNum * size) / rows;
ctx.strokeStyle = "White";
ctx.fillStyle = "black";
ctx.lineWidth = 2;
if (this.walls.topWall) this.drawTopWall(x, y, size, columns, rows);
if (this.walls.rightWall) this.drawRightWall(x, y, size, columns, rows);
if (this.walls.bottomWall) this.drawBottomWall(x, y, size, columns, rows);
if (this.walls.leftwall) this.drawLeftWall(x, y, size, columns, rows);
if(this.visited){
ctx.fillRect(x + 1, y + 1, size / columns - 2, size / rows - 2);
}
}
}
let newMaze = new Maze(700,20,20);
newMaze.setup();
newMaze.draw();
<canvas class="maze"></canvas>
This line
if(neighbours,length !== 0){
...
}
doesn't look right. Maybe you want if(neighbours.length !== 0){ ... } (note the dot instead of the comma).
Also,
current.removeWalls() should be current.removeWall() (without the "s")

Fill randomly shaped area of single colour by clicking on it

I'm trying to fill a random area enclosed by other areas of a different color.
I'm currently using 4-pixel connectivity to achieve this. It works, however, the filling takes way to long (Firefox askes me multiple times to stop the script. Chrome often just terminates it because the maximum call stack is exceeded). My question: How can I optimize my code or what other algorithms should I use?
PS: I fixed the mistake Wan Chap mentioned.
EDIT: I now added a global variable canImageData and changed the getRGBStringFromPixel() function. The filling works much faster now but I'm constantly getting exceeded call stack errors now leading to bigger areas only being filled partially. Does anybody have an example of a javascript fit version of the aforementioned algorithm?
EDIT FINAL: I found a different much more performant solution digging through another site's js source. I posted the solution as an answer below.
My code:
var can = document.getElementById('can');
var ctx = can.getContext('2d');
var canImageData = null;
var RGB_STRING_WHITE = '255-255-255-255';
function colour_fill_4_connectivity(x, y, RGBString_fill_colour, RGBString_region_colour){
try {
var currentPixelRGBString = getRGBStringFromPixel(x, y);
if(currentPixelRGBString == RGBString_fill_colour || currentPixelRGBString != RGBString_region_colour)
return;
setPixelFromRGBString(x, y, RGBString_fill_colour);
if(x < can.width)
colour_fill_4_connectivity(x + 1, y, RGBString_fill_colour, RGBString_region_colour);
if(x > 0)
colour_fill_4_connectivity(x - 1, y, RGBString_fill_colour, RGBString_region_colour);
if(y < can.height)
colour_fill_4_connectivity(x, y + 1, RGBString_fill_colour, RGBString_region_colour);
if(y > 0)
colour_fill_4_connectivity(x, y - 1, RGBString_fill_colour, RGBString_region_colour);
} catch(e) {
console.log('ERROR: colour_fill_4_connectivity(' + x + ', ' + y + ', ' + RGBString_fill_colour + ', ' + RGBString_region_colour + ') -> ' + e);
}
}
function fillColor(x, y, RGBString_fill_colour, RGBString_region_colour) {
colour_fill_4_connectivity(x, y, RGBString_fill_colour, RGBString_region_colour);
}
function RGBStringToArray(valRGB) {
return valRGB.split('-');
}
function getRGBStringFromPixel(x, y) {
var data = canImageData.data;
var startIndex = (x + y * can.width) * 4;
return data[startIndex] + '-' + data[startIndex + 1] + '-' + data[startIndex + 2] + '-' + data[startIndex + 3];
}
function setPixelFromRGBString(x, y, valRGB) {
var imageData = ctx.createImageData(1, 1);
var data = imageData.data;
var rgbArr = RGBStringToArray(valRGB);
var startIndex = (x + y * can.width) * 4;
data[0] = rgbArr[0];
data[1] = rgbArr[1];
data[2] = rgbArr[2];
data[3] = rgbArr[3];
ctx.putImageData(imageData, x, y);
canImageData.data[startIndex] = rgbArr[0];
canImageData.data[startIndex + 1] = rgbArr[1];
canImageData.data[startIndex + 2] = rgbArr[2];
canImageData.data[startIndex + 3] = rgbArr[3];
}
// TEST
function _test_getRGBStringFromPixel() {
var res = [];
for(var y = 0; y < can.height; y++) {
for(var x = 0; x < can.width; x++) {
res.push({x:x, y:y, color:getRGBStringFromPixel(x, y)});
}
}
return JSON.stringify(res);
}
var img = new Image();
img.onload = (function(e) {
can.width = this.width;
can.height = this.height;
ctx.drawImage(this, 0, 0);
canImageData = ctx.getImageData(0, 0, can.width, can.height);
}).bind(img);
img.src = 'test-image.png';
can.addEventListener('click', function(e) {
var clickX = e.clientX, clickY = e.clientY;
if(e.button === 0 && clickX <= can.width && clickY <= can.height)
fillColor(clickX, clickY, RGB_STRING_WHITE, getRGBStringFromPixel(clickX, clickY));
}, false);
body {
padding: 0;
margin: 0;
}
<canvas id="can" width="600" height="400"></canvas>
It seems it just a minor mistake, in function setPixelFromRGBString:
Change data[1] = rgbArr[2]; to data[2] = rgbArr[2];
var can = document.getElementById('can');
var ctx = can.getContext('2d');
var RGB_STRING_WHITE = '255-255-255-255';
function colour_fill_4_connectivity(x, y, RGBString_fill_colour, RGBString_region_colour) {
//try {
var currentPixelRGBString = getRGBStringFromPixel(x, y);
if (currentPixelRGBString == RGBString_fill_colour || currentPixelRGBString != RGBString_region_colour)
return;
setPixelFromRGBString(x, y, RGBString_fill_colour);
if (x > 0)
colour_fill_4_connectivity(x - 1, y, RGBString_fill_colour, RGBString_region_colour);
if (x < can.width)
colour_fill_4_connectivity(x + 1, y, RGBString_fill_colour, RGBString_region_colour);
if (y > 0)
colour_fill_4_connectivity(x, y - 1, RGBString_fill_colour, RGBString_region_colour);
if (y < can.height)
colour_fill_4_connectivity(x, y + 1, RGBString_fill_colour, RGBString_region_colour);
//} catch(e) {
//console.log('ERROR: colour_fill_4_connectivity(' + x + ', ' + y + ', ' + RGBString_fill_colour + ', ' + RGBString_region_colour + ') -> ' + e);
//}
}
function RGBStringToArray(valRGB) {
return valRGB.split('-');
}
function getRGBStringFromPixel(x, y) {
var data = ctx.getImageData(x, y, x + 1, y + 1).data;
return data[0] + '-' + data[1] + '-' + data[2] + '-' + data[3];
}
function setPixelFromRGBString(x, y, valRGB) {
var imageData = ctx.createImageData(1, 1);
var data = imageData.data;
var rgbArr = RGBStringToArray(valRGB);
data[0] = rgbArr[0];
data[1] = rgbArr[1];
data[1] = rgbArr[2];
data[3] = rgbArr[3];
ctx.putImageData(imageData, x, y);
}
// TEST
var img = new Image();
img.onload = (function(e) {
ctx.drawImage(img, 0, 0);
}).bind(img);
img.src = 'https://static1.squarespace.com/static/593357e715d5dbea570d2118/593ee768893fc0375f9c6fd5/5c7499a7e79c707a50772134/1551194769831/Test+pattern.png?format=1000w';
can.addEventListener('click', function(e) {
var clickX = e.clientX,
clickY = e.clientY;
if (e.button === 0 && clickX <= can.width && clickY <= can.height)
colour_fill_4_connectivity(clickX, clickY, RGB_STRING_WHITE, getRGBStringFromPixel(clickX, clickY));
}, false);
body {
padding: 0;
margin: 0;
}
<canvas id="can" width="600" height="400"></canvas>
After some research, I decided to dig into the source of http://skribbl.io which has a reliable and fast filling tool. They found an iterative solution. I extracted a snippet from the source file and changed parts to make it easier to read:
function DrawingBoard(canvas) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext("2d");
}
DrawingBoard.prototype.getPixel = function(imageData, x, y) {
var startIndex = 4 * (y * imageData.width + x);
return startIndex >= 0 && startIndex < imageData.data.length ? [imageData.data[startIndex], imageData.data[startIndex + 1], imageData.data[startIndex + 2]] : [0, 0, 0]
}
DrawingBoard.prototype.setPixel = function(imageData, startIndex, r, g, b) {
startIndex >= 0 && startIndex < imageData.data.length && (imageData.data[startIndex] = r, imageData.data[startIndex + 1] = g, imageData.data[startIndex + 2] = b, imageData.data[startIndex + 3] = 255)
}
DrawingBoard.prototype.floodFill = function(startX, startY, r, g, b) {
var imageData = this.canvasCtx.getImageData(0, 0, this.canvas.width, this.canvas.height), points = [[startX, startY]], targetPixelRGB = this.getPixel(imageData, startX, startY);
if (r != targetPixelRGB[0] || g != targetPixelRGB[1] || b != targetPixelRGB[2]) {
for (
var c = function(t) {
var e = imageData.data[t],
i = imageData.data[t + 1],
c = imageData.data[t + 2];
if (e == r && i == g && c == b) return false;
var u = Math.abs(e - targetPixelRGB[0]),
h = Math.abs(i - targetPixelRGB[1]),
l = Math.abs(c - targetPixelRGB[2]);
return u < 1 && h < 1 && l < 1
},
imageHeight = imageData.height, imageHeightWidth = imageData.width; points.length;
) {
var point, pointX, pointY, index, y, m;
for (point = points.pop(), pointX = point[0], pointY = point[1], index = 4 * (pointY * imageHeightWidth + pointX); pointY-- >= 0 && c(index);) {
index -= 4 * imageHeightWidth;
}
for (index += 4 * imageHeightWidth, ++pointY, y = 0, m = 0; pointY++ < imageHeight - 1 && c(index);) {
this.setPixel(imageData, index, r, g, b);
pointX > 0;
c(index - 4) ? y || (points.push([pointX - 1, pointY]), y = 1) : y && (y = 0);
pointX < imageHeightWidth - 1;
c(index + 4) ? m || (points.push([pointX + 1, pointY]), m = 1) : m && (m = 0);
index += 4 * imageHeightWidth;
}
}
this.canvasCtx.putImageData(imageData, 0, 0)
}
}
// TEST
var db = new DrawingBoard(document.getElementById('can'));
var img = new Image();
var fillColor = [255, 0, 255];
img.onload = (function() {
db.canvasCtx.drawImage(this, 0, 0);
}).bind(img);
img.src = 'test.png';
addEventListener('click', function(e) {
var cx = e.clientX, cy = e.clientY;
if(
e.button === 0 &&
cx > 0 && cx < db.canvas.width &&
cy > 0 && cy < db.canvas.height
) {
db.floodFill(cx, cy, fillColor[0], fillColor[1], fillColor[2]);
}
}, false);
body {
padding: 0;
margin: 0;
}
<canvas id="can" width="600" height="400"></canvas>
Working fiddle can be found here: https://jsfiddle.net/f0kuwa5e/

Why are my Raycaster Sprites blinking and how can I fix it?

My raycaster's sprites keep blinking, and I think it has to do with ZBuffer.
Using this for reference: https://lodev.org/cgtutor/index.html
I have done plenty of research and can't find any JS raycasters that answer my problem. By sprites, I mean billboarded images, and when you move they blink like an atari game that is being pushed to its limits.
Here is the project on JSFiddle: https://jsfiddle.net/Vakore/bsvx1m26/
Here is where I am using ZBuffer:
for (var stripe = drawStartX; stripe < drawEndX; stripe++) {
var texX =
floor((256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth) / spriteWidth) / 256;
// the conditions in the if are:
// 1) it's in front of camera plane so you don't see things behind you
// 2) it's on the screen (left)
// 3) it's on the screen (right)
// 4) ZBuffer, with perpendicular distance
var ruffer = 0;
if (sprite[i][2] == "barrel") {
ruffer = 576 - 64;
}
if (sprite[i][2] == "pillar") {
ruffer = 576;
}
if (sprite[i][2] == "greenlight") {
ruffer = 576 + 64;
}
if (
transformY > 0 &&
stripe > 0 &&
stripe < w &&
transformY < ZBuffer[stripe]
) {
can.drawImage(
document.getElementById(sprite[i][2]),
texX + ruffer, 0, 1, texHeight, stripe, drawStartY, 1, drawEndY - drawStartY
);
}
}
ZBuffer was declared and assigned an array value earlier in the code.
var keys = {
"a": false,
"s": false,
"d": false,
"A": false,
"S": false,
"D": false,
"W": false,
"Z": false,
"X": false,
"C": false,
"ENTER": false
};
document.addEventListener("keydown", function(e) {
e.preventDefault();
if (e.keyCode === 16)
keys["SHIFT"] = true;
if (e.keyCode === 37)
keys["a"] = true;
if (e.keyCode === 39)
keys["d"] = true;
if (e.keyCode === 65)
keys["A"] = true;
if (e.keyCode === 68)
keys["D"] = true;
if (e.keyCode === 83)
keys["S"] = true;
if (e.keyCode === 87)
keys["W"] = true;
if (e.keyCode === 40)
keys["S"] = true;
if (e.keyCode === 38)
keys["W"] = true;
if (e.keyCode === 67)
keys["C"] = true;
if (e.keyCode === 88)
keys["X"] = true;
if (e.keyCode === 90)
keys["Z"] = true;
if (e.keyCode === 13)
keys["ENTER"] = true;
})
document.addEventListener("keyup", function(e) {
e.preventDefault();
if (e.keyCode === 16)
keys["SHIFT"] = false;
if (e.keyCode === 37)
keys["a"] = false;
if (e.keyCode === 39)
keys["d"] = false;
if (e.keyCode === 65)
keys["A"] = false;
if (e.keyCode === 68)
keys["D"] = false;
if (e.keyCode === 83)
keys["S"] = false;
if (e.keyCode === 87)
keys["W"] = false;
if (e.keyCode === 40)
keys["S"] = false;
if (e.keyCode === 38)
keys["W"] = false;
if (e.keyCode === 67)
keys["C"] = false;
if (e.keyCode === 88)
keys["X"] = false;
if (e.keyCode === 90)
keys["Z"] = false;
})
/*
Basic functions(ease of access)
*/
var canvas = document.getElementById("canva");
var can = canvas.getContext("2d");
canvas.requestPointerLock = canvas.requestPointerLock || canvas.mozRequestPointerLock;
document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;
canvas.onclick = function() {
canvas.requestPointerLock();
clicked = true;
};
var mouseX = 0;
var mouseMove = function(e) {
mouseX += e.movementX;
};
var rect = function(x, y, w, h) {
can.fillRect(x, y, w, h);
};
var arc = function(x, y, r, start, stop) {
can.beginPath();
can.arc(x, y, r, start, stop);
can.fill();
};
var circle = function(x, y, r) {
arc(x, y, r, 0, 360);
};
var fill = function(r, g, b, a) {
if (a === undefined) {
a = 1;
}
can.fillStyle = "rgb(" + r + "," + g + "," + b + "," + a + ")";
};
var font = function(siz) {
can.font = siz;
};
var textAlign = function(ali) {
can.textAlign = ali;
};
var text = function(txt, x, y, w, h) {
can.fillText(txt, x, y, w, h);
};
var quad = function(x1, y1, x2, y2, x3, y3, x4, y4) {
can.beginPath();
can.moveTo(x1, y1);
can.lineTo(x2, y2);
can.lineTo(x3, y3);
can.lineTo(x4, y4);
can.closePath();
can.fill();
};
var random = function(min, max) {
return round(Math.random() * (max - min)) + min;
};
var translate = function(x, y) {
can.save();
can.translate(x, y);
};
var scale = function(w, h) {
can.scale(w, h);
}
var trestore = function() {
can.restore();
};
var floor = function(num) {
return Math.floor(num);
};
var sqrt = function(num) {
return Math.sqrt(num);
};
var abs = function(num) {
return Math.abs(num);
};
var color = function(r, g, b, a) {
return [r, g, b, a];
};
var dist = function() {
return true;
};
var cos = function(num) {
return Math.cos(num);
}
var sin = function(num) {
return Math.sin(num);
}
var dist = function(x1, y1, x2, y2) {
dx = x1 - x2;
dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
};
//Begin the raycaster
var scaler = 3; //This reduces the resolution so that it can display large rooms without lagging
var w = 640 / scaler;
var h = 480 / scaler;
var texWidth = 64;
var texHeight = 64;
var rotSpeed = 0.1;
var moveSpeed = 0.1;
var worldMap = [
"1111111111111111111111111111111111111111",
"1001000001234567800000000000000000000001",
"1001000000000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1001000000000000000000000000000000000001",
"1001000000000000000000000000222200000001",
"1001000000000000000000000000200200000001",
"1111000000000000000000000000200200000001",
"1000000000000000000000000033300333000001",
"8008000000000000000000000000000000000001",
"8000800000000000000000000033300333000001",
"8000080000000000000000000000300300000001",
"8000008000000000000000000000333300000001",
"8888888000000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1666006660000000000000000000000000000001",
"1000000060000000000000000000000000000001",
"1000000060000000000000000000000000000001",
"1000000060000000000000000000000000000001",
"1000000060000000000000000000000000000001",
"1666666660000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1000000000000000000000000000000000000001",
"1000000040444444444444400000000000000001",
"1000000040000000000000400000000000000001",
"1000000040000000000000400000000000000001",
"1000000040000000000000400000000000000001",
"1000000044444444444444400000000000000001",
"1000000000000000000000000000000000000001",
"1000000000000007775777000000000000000001",
"1000000000000007000007000000000000000001",
"1000000000000005000005000000000000000001",
"1000000000000007000007000000000000000001",
"1000000000000000000007000000000000000001",
"1111111111111117775777111111111111111111",
];
try {
var ZBuffer = [];
var sprite = [
[5, 5, "barrel", 0], //Y and X are reversed
[6.5, 2.5, "pillar", 1],
[6.5, 1.5, "pillar", 1.5],
[8, 6, "greenlight", 2],
];
var posX = 2,
posY = 2,
dirX = -1,
dirY = 0,
planeX = 0,
planeY = 0.66; //The 2d raycaster version of the camera plane
} catch (e) {
alert(e);
}
var castRays = function() {
for (var x = 0; x < w; x++) {
var cameraX = 2 * x / (w) - 1; //x-coordinate in camera space
var rayDirX = dirX + planeX * cameraX;
var rayDirY = dirY + planeY * cameraX;
//which box of the map we're in
var mapX = floor(posX);
var mapY = floor(posY);
//length of ray from current position to next x or y-side
var sideDistX;
var sideDistY;
//length of ray from one x or y-side to next x or y-side
var deltaDistX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX));
var deltaDistY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY));
var perpWallDist = 0;
//what direction to step in x or y-direction (either +1 or -1)
var stepX = 0;
var stepY = 0;
var hit = 0; //was there a wall hit?
var side = 0; //was a NS or a EW wall hit?
//calculate step and initial sideDist
if (rayDirX < 0) {
stepX = -1;
sideDistX = (posX - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - posX) * deltaDistX;
}
if (rayDirY < 0) {
stepY = -1;
sideDistY = (posY - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - posY) * deltaDistY;
}
while (hit == 0) {
//jump to next map square, OR in x-direction, OR in y-direction
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
//Check if ray has hit a wall
if (worldMap[mapX][mapY] == 0) {}
if (worldMap[mapX][mapY] != 0) {
hit = 1;
}
} //end of while loop
//Calculate distance of perpendicular ray (Euclidean distance will give fisheye effect!)
if (side == 0) {
perpWallDist = (mapX - posX + (1 - stepX) / 2) / rayDirX;
} else {
perpWallDist = (mapY - posY + (1 - stepY) / 2) / rayDirY;
}
//Calculate height of line to draw on screen
var lineHeight = abs(floor(h / perpWallDist));
//calculate lowest and highest pixel to fill in current stripe
var drawStart = -lineHeight / 2 + h / 2;
//if(drawStart < 0) {drawStart = 0;}
var drawEnd = lineHeight / 2 + h / 2;
//if(drawEnd >= h) {drawEnd = h - 1;}
//texturing calculations
//var texNum = worldMap[mapX][mapY] - 1; //1 subtracted from it so that texture 0 can be used!
//calculate value of wallX
var wallX = 0; //where exactly the wall was hit
if (side == 0) {
wallX = posY + perpWallDist * rayDirY;
} else {
wallX = posX + perpWallDist * rayDirX;
}
wallX -= floor((wallX));
//x coordinate on the texture
var texX = floor(wallX * (texWidth));
if (side == 0 && rayDirX > 0) {
texX = texWidth - texX - 1;
}
if (side == 1 && rayDirY < 0) {
texX = texWidth - texX - 1;
}
//Untextured variant
/*var currentColor = color(255, 255, 255);
if (worldMap[mapX][mapY] == "1") {currentColor = color(150, 150, 150);}
if (worldMap[mapX][mapY] == "2") {currentColor = color(255, 255, 255);}
if (worldMap[mapX][mapY] == "3") {currentColor = color(255,0,0);}
if (worldMap[mapX][mapY] == "4") {currentColor = color(0,0,255);}
if (worldMap[mapX][mapY] == "5") {currentColor = color(0,255,0);}
if (worldMap[mapX][mapY] == "6") {currentColor = color(255,0,255);}
if (worldMap[mapX][mapY]=="7") {currentColor = color(0,255,255);}
if (side === 1) {currentColor = [currentColor[0] - 40, currentColor[1] - 40, currentColor[2] - 40];}
//if (dist(mapX, mapY, px, pz) > 10) {continue;}
fill(currentColor[0],currentColor[1],currentColor[2]);
rect(x, drawStart, 1, drawEnd - drawStart);*/
var currentImg = "bluestone";
if (worldMap[mapX][mapY] == "1") {
currentImg = "bluestone";
}
if (worldMap[mapX][mapY] == "2") {
currentImg = "greystone";
}
if (worldMap[mapX][mapY] == "3") {
currentImg = "wood";
}
if (worldMap[mapX][mapY] == "4") {
currentImg = "colorstone";
}
if (worldMap[mapX][mapY] == "5") {
currentImg = "eagle";
}
if (worldMap[mapX][mapY] == "6") {
currentImg = "mossy";
}
if (worldMap[mapX][mapY] == "7") {
currentImg = "redbrick";
}
if (worldMap[mapX][mapY] == "8") {
currentImg = "purplestone";
}
//if (worldMap[mapX][mapY] =="a") {currentImg = "barrel";}
//if (dist(mapX, mapY, posX, posY) > 20) {continue;}
can.drawImage(document.getElementById(currentImg), texX + ((worldMap[mapX][mapY] - 1) * 64), 0, 1, texHeight, x, drawStart, 1, drawEnd - drawStart);
//fill(255, 0, 0);rect(x, drawStart, 1, drawEnd - drawStart);
if (side === 1) {
fill(0, 0, 0, 0.3);
rect(x, drawStart, 1, drawEnd - drawStart);
}
//Set the ZBuffer
ZBuffer[x] = perpWallDist;
//FLOOR CASTING(not using till figure out how to do without lag)
var floorXWall = 0,
floorYWall = 0;
if (side == 0 && rayDirX > 0) {
floorXWall = mapX;
floorYWall = mapY + wallX;
} else if (side == 0 && rayDirX < 0) {
floorXWall = mapX + 1.0;
floorYWall = mapY + wallX;
} else if (side == 1 && rayDirY > 0) {
floorXWall = mapX + wallX;
floorYWall = mapY;
} else {
floorXWall = mapX + wallX;
floorYWall = mapY + 1.0;
}
var distWall, distPlayer, currentDist;
distWall = perpWallDist;
distPlayer = 0.0;
for (var y = drawEnd + 1; y < h; y++) {
//if (dist(mapX, mapY, posX, posY) > 5) {continue;}
currentDist = h / (2.0 * y - h); //you could make a small lookup table for this instead
var weight = (currentDist - distPlayer) / (distWall - distPlayer);
var currentFloorX = weight * floorXWall + (1.0 - weight) * posX;
var currentFloorY = weight * floorYWall + (1.0 - weight) * posY;
var floorTexX, floorTexY;
floorTexX = floor(currentFloorX * texWidth) % texWidth;
floorTexY = floor(currentFloorY * texHeight) % texHeight;
//OPTIMIZE FOR LESS LAG!!!!!!!!!! (search
//FLOOR
//can.drawImage(document.getElementById("greystone"), floorTexX, floorTexY, texWidth, texHeight, x, y, texWidth, texHeight);
//CEILING
//can.drawImage(document.getElementById("wood"), floorTexX, floorTexY, texWidth, 1, x, h - y, texWidth, 1);
} //End of the 'y' loop
} //end the loop
//SPRITE CASTING
//Sort sprites
sprite.sort(function(a, b) {
return b[3] - a[3];
}); //Sort the depth of each sprite
//Draw sprites
for (var i = 0; i < sprite.length; i++) {
var spriteX = sprite[i][0] - posX;
var spriteY = sprite[i][1] - posY;
sprite[i][3] = abs((posX - sprite[i][0]) - (posY - sprite[i][1]));
var invDet = 1.0 / (planeX * dirY - dirX * planeY);
var transformX = invDet * (dirY * spriteX - dirX * spriteY);
var transformY = invDet * (-planeY * spriteX + planeX * spriteY); //this is actually the depth inside the screen, that what Z is in 3D(Thanks lodev.org for explanation!)
var spriteScreenX = floor((w / 2) * (1 + transformX / transformY));
//calculate height of the sprite on screen
var spriteHeight = abs(floor(h / (transformY))); //using "transformY" instead of the real distance prevents fisheye
//calculate lowest and highest pixel to fill in current stripe
var drawStartY = -spriteHeight / 2 + h / 2;
//if (drawStartY < 0) {drawStartY = 0;} Don't need THIS
var drawEndY = spriteHeight / 2 + h / 2;
//if (drawEndY >= h) {drawEndY = h - 1;} Or THIS
//calculate width of the sprite
var spriteWidth = abs(floor(h / (transformY)));
var drawStartX = -spriteWidth / 2 + spriteScreenX;
//if (drawStartX < 0) {drawStartX = 0;} Nor this
var drawEndX = spriteWidth / 2 + spriteScreenX;
//if (drawEndX >= w) {drawEndX = w - 1;} Nor dis
//loop through every vertical stripe of the sprite on screen
for (var stripe = drawStartX; stripe < drawEndX; stripe++) {
var texX = floor(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth / spriteWidth) / 256;
//the conditions in the if are:
//1) it's in front of camera plane so you don't see things behind you
//2) it's on the screen (left)
//3) it's on the screen (right)
//4) ZBuffer, with perpendicular distance
var ruffer = 0;
if (sprite[i][2] == "barrel") {
ruffer = 576 - 64;
}
if (sprite[i][2] == "pillar") {
ruffer = 576;
}
if (sprite[i][2] == "greenlight") {
ruffer = 576 + 64;
}
if (transformY > 0 && stripe > 0 && stripe < w && transformY < ZBuffer[stripe]) {
can.drawImage(document.getElementById(sprite[i][2]), texX + ruffer, 0, 1, texHeight, stripe, drawStartY, 1, drawEndY - drawStartY);
}
}
} //End of 'i' loop
//Bottom of dat
if (keys["W"]) {
if (worldMap[floor(posX + dirX * moveSpeed)][floor(posY)] == false) {
posX += dirX * moveSpeed
};
if (worldMap[floor(posX)][floor(posY + dirY * moveSpeed)] == false) {
posY += dirY * moveSpeed
};
}
//move backwards if no wall behind you
if (keys["S"]) {
if (worldMap[floor(posX - dirX * moveSpeed)][floor(posY)] == false) {
posX -= dirX * moveSpeed;
}
if (worldMap[floor(posX)][floor(posY - dirY * moveSpeed)] == false) {
posY -= dirY * moveSpeed;
}
}
if (keys["D"]) {
if (worldMap[floor(posX + planeX * moveSpeed)][floor(posY)] == false) {
posX += planeX * moveSpeed
};
if (worldMap[floor(posX)][floor(posY + planeY * moveSpeed)] == false) {
posY += planeY * moveSpeed
};
}
if (keys["A"]) {
if (worldMap[floor(posX - planeX * moveSpeed)][floor(posY)] == false) {
posX -= planeX * moveSpeed;
}
if (worldMap[floor(posX)][floor(posY - planeY * moveSpeed)] == false) {
posY -= planeY * moveSpeed;
}
}
//rotate to the right
mouseX = -mouseX / 75;
var oldDirX = dirX;
dirX = dirX * cos(mouseX) - dirY * sin(mouseX);
dirY = oldDirX * sin(mouseX) + dirY * cos(mouseX);
var oldPlaneX = planeX;
planeX = planeX * cos(mouseX) - planeY * sin(mouseX);
planeY = oldPlaneX * sin(mouseX) + planeY * cos(mouseX);
mouseX = 0;
if (keys["d"]) {
//both camera direction and camera plane must be rotated
var oldDirX = dirX;
dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed);
dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed);
var oldPlaneX = planeX;
planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed);
planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed);
}
//rotate to the left
if (keys["a"]) {
//both camera direction and camera plane must be rotated
var oldDirX = dirX;
dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed);
dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed);
var oldPlaneX = planeX;
planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed);
planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed);
}
};
var run = function() {
try {
if (document.pointerLockElement === canvas || document.mozPointerLockElement === canvas) {
document.addEventListener("mousemove", mouseMove, false);
} else {
document.removeEventListener("mousemove", mouseMove, false);
} //Mouse stuff
translate(0, 0);
scale(scaler, scaler);
fill(40, 40, 40);
rect(0, 0, w, h / 2);
fill(80, 80, 80);
rect(0, h / 2, w, h / 2);
if (keys["C"]) {
rotSpeed = 0.04;
} else {
rotSpeed = 0.1;
}
/*if (keys["W"]) {fill(255, 0, 0);}
if (keys["A"]) {fill(0, 255, 0);}
if (keys["S"]) {fill(0, 0, 255);}
if (keys["D"]) {fill(255, 0, 255);}
if (keys["X"]) {fill(255, 255, 0);}
if (keys["Z"]) {fill(0, 255, 255);}*/
castRays();
trestore();
requestAnimationFrame(run);
} catch (e) {
alert(e);
}
};
run();
<html>
<title>Jailbreak</title>
<div class="center">
<canvas width="640" height="480" id="canva"></canvas>
<br>
<img id="barrel" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="pillar" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="greenlight" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="bluestone" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="greystone" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="wood" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="colorstone" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="eagle" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="mossy" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="redbrick" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
<img id="purplestone" src="https://lodev.org/cgtutor/images/wolftexturesobj.gif">
</div>
</html>
UPDATE: I tried changing
if (
transformY > 0 &&
stripe > 0 &&
stripe < w &&
transformY < ZBuffer[stripe]//Pay attention to this line
) {
to:
if (
transformY > 0 &&
stripe > 0 &&
stripe < w &&
transformY > ZBuffer[stripe]//Now look
) {
Which basically reverses the check to see if it is behind a wall. Surprisingly, the blinking continues.
The answer was pretty simple. It was because stripe is not an even value, and if you were to have an array like this: var thisArray = [1, 2, 3, 4]; and you were to try and grab var money = thisArray[1.5]; the value undefined would be returned.
So I changed this:
if (
transformY > 0 &&
stripe > 0 &&
stripe < w &&
transformY < ZBuffer[stripe]
) {
To this:
if (
transformY > 0 &&
stripe > 0 &&
stripe < w &&
transformY < ZBuffer[Math.round(stripe)]
) {
Math.floor also works, since it also returns an integer value.

HTML JS Canvas Game: Tile Collision Bug Makes Player Teleport Up

I'm a beginner at game development and have been struggling with getting collision done right between an array of tiles and a player rectangle. This game features jumping and gravity. First of all the collision works, but is very clunky. Sometimes when the player ends up on top of a tile and a little to the edge, it will instantly teleport to either the right or left side (depending on what edge/corner) and fall of it. This also happens when colliding with the bottom of the tile; the player will instantly teleport to the side and go up further. From what I understand the tile collision detector confuses the collision with one or the other side because when the player hits the edge of a tile the detector reads it as if it collided with both and decides to place the player elsewhere base on the highest coordinate velocity (aka speedX and speedY). I figured this out by setting speedY = 0 every time it hits the top of a tile, which fixed the issue, but another one came out of it. Now, if the player is on top of a tile and then falls of and shortly strafe back, it doesn't collide with the tile's side, but it rather quickly goes back on top of it again.
I just need some tips on how i should resolve this, because everything I try leads to another problem. I've heard this is a common bug among developing 2D tile based games.
Here is a jsfiddle with the code in action: https://jsfiddle.net/8121u356/
And here is the display of my entire code:
function startGame() {
gameArea.start();
actor = new player(32, 32, "green", 32, 32);
}
var mapArray = [
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,1,1,1,0,0,0,1,1,0,0,0,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
];
var levelRows = 20;
var levelCols = 20;
var gameArea = {
canvas : document.getElementById('canvas'),
start : function() {
this.context = this.canvas.getContext("2d");
document.body.insertBefore(this.canvas, document.body.childNodes[0]);
requestAnimationFrame(updateGameArea);
window.addEventListener('keydown', function (e) {
gameArea.keys = (gameArea.keys || []);
gameArea.keys[e.keyCode] = true;
});
window.addEventListener('keyup', function (e) {
gameArea.keys = (gameArea.keys || []);
gameArea.keys[e.keyCode] = false;
})
},
clear : function(){
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
},
render : function() {
context = this.canvas.getContext("2d");
var tileSize = 32;
for(i=0;i<levelRows;i++){
for(j=0;j<levelCols;j++){
if(mapArray[i][j]==1){
context.fillStyle = "gray";
context.fillRect(j*tileSize,i*tileSize,tileSize,tileSize);
}
}
}
}
};
function TileCollisionManager(object) {
let tileSize = 32;
let baseCol = Math.floor(object.x / tileSize);
let baseRow = Math.floor(object.y / tileSize);
let colOverlap = object.x % tileSize;
let rowOverlap = Math.floor(object.y % tileSize);
if (object.speedX > 0) {
if ((mapArray[baseRow][baseCol + 1] && !mapArray[baseRow][baseCol]) ||
(mapArray[baseRow + 1][baseCol + 1] && !mapArray[baseRow + 1][baseCol] && rowOverlap)) {
object.x = baseCol * tileSize;
}
}
if (object.speedX < 0) {
if ((!mapArray[baseRow][baseCol + 1] && mapArray[baseRow][baseCol]) ||
(!mapArray[baseRow + 1][baseCol + 1] && mapArray[baseRow + 1][baseCol] && rowOverlap)) {
object.x = (baseCol + 1) * tileSize;
}
}
if (object.speedY > 0) {
if ((mapArray[baseRow + 1][baseCol] && !mapArray[baseRow][baseCol]) ||
(mapArray[baseRow + 1][baseCol + 1] && !mapArray[baseRow][baseCol + 1] && colOverlap)) {
object.y = ((baseRow) * tileSize);
object.jumping = false;
object.speedY = 0;
}
}
if (object.speedY < 0) {
if ((!mapArray[baseRow + 1][baseCol] && mapArray[baseRow][baseCol]) ||
(!mapArray[baseRow + 1][baseCol + 1] && mapArray[baseRow][baseCol + 1] && colOverlap)) {
object.y = (baseRow + 1) * tileSize;
object.speedY = 5;
}
}
}
function updateGameArea() {
gameArea.clear();
gameArea.render();
actor.update();
actor.newPos();
actor.speedX = 0;
actor.speedY += actor.gravity;
if (gameArea.keys && gameArea.keys[39]) {
actor.speedX = 4;
}
if (gameArea.keys && gameArea.keys[37]) {
actor.speedX = -4;
}
if (gameArea.keys && gameArea.keys[32]) {
if (!actor.jumping) {
actor.jumping = true;
actor.speedY = -actor.speed * 3;
}
}
TileCollisionManager(actor);
requestAnimationFrame(updateGameArea);
}
function player (width, height, color, x, y) {
this.width = width;
this.height = height;
this.x = x;
this.y = y;
this.speedX=0;
this.speedY=0;
this.gravity=0.3;
this.speed=3;
this.jumping=false;
this.color = color;
this.update = function () {
ctx = gameArea.context;
ctx.fillStyle = this.color;
ctx.fillRect(
this.x,
this.y,
this.width, this.height);
};
this.newPos = function () {
this.x += this.speedX;
this.y += this.speedY;
};
A quick fix for you.
I have seen you post this question for the 3rd time. You are not getting an answer because the best solution is rather a lot of code, complex, and requires a lot of changes to your code.
So what I have done is create a very quick and simple solution.
Solve collisions in the correct order.
Rather than check the position at the end of a move, I changed the code to check at every pixel moved. This is needed as you have to find the collisions in the correct order as the player moves from one position to the next. If you hit a wall on the side before the top or bottom, or the other way around it makes a difference, and is what is causing you to have problems. You checked x first then y, which for many situation is the wrong way around.
I also added a object to the actor called canMove It has 4 properties that are set at the start of each frame and are used to prevent the player moving in a direction that is blocked. If you let the player move in a blocked direction it will get stuck on the wall while you have the key down in that direction.
I hacked into your code
Sorry I made a bit of a mess but am short on time.
Also to help me write the changes I made a few other mods, I scaled that game to fit the window (the scaling and resize is all done in the clear function). I changed the keyboard interface to prevent default on keys pressed and set up arrow to jump as well as space (I hate using space to jump :P). Also change the map to use strings as it is a pain typing in changes as an array.
I was not sure how you wanted the actor to react when it is hit on the head. I made it so that it bounces down at the same speed as it moves up, but it does make it harder to jump and slide into narrow passages.
So I think I have most of it done, so you can move on with your game.
If you have questions, ask in the comments.
// NOTE var | 0 is the same as Math.floor(var)
var mapArray = [
"# #",
"# #",
"# ### #",
"# # #",
"# ## ##### #",
"# #",
"# #",
"# ## #",
"# ## #",
"# #",
"# ##### #",
"# #",
"# #",
"# ##### #",
"# #",
"# #",
"# # ## #",
"# ### #",
"# ##### ## #",
"####################",
].map(row => row.split("").map(cell=>cell==="#" ? 1 : 0));
var levelRows = 20;
var levelCols = 20;
var tileX = 32;
var tileY = 32;
var gameArea = {
canvas : document.getElementById('canvas'),
ctx : document.getElementById('canvas').getContext("2d"),
keys : { // set them here so that can block defaults
"37" : false,
"38" : false, // also jump
"39" : false,
"32" : false, // jump
},
start : function() {
document.body.insertBefore(this.canvas, document.body.childNodes[0]);
requestAnimationFrame(updateGameArea);
function keyEvent(e) {
if(gameArea.keys["" + e.keyCode] !== undefined){
gameArea.keys["" + e.keyCode] = e.type === "keydown"
e.preventDefault();
}
}
addEventListener('keydown', keyEvent);
addEventListener('keyup', keyEvent);
focus();
},
clear(){
var minSize = Math.min(innerWidth,innerHeight);
if (this.ctx.canvas.width !== minSize|| this.ctx.canvas.height !== minSize) {
this.ctx.canvas.width = minSize;
this.ctx.canvas.height = minSize;
}
this.ctx.setTransform(1,0,0,1,0,0);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// the next line scales the canvas rendering to fit.
this.ctx.setTransform(
minSize / (levelCols * tileX),
0,
0,
minSize/ (levelRows * tileY),
0,0
);
},
render() {
var ctx = this.ctx;
for(i=0;i<levelRows;i++){
for(j=0;j<levelCols;j++){
if(mapArray[i][j]==1){
ctx.fillStyle = "gray";
ctx.fillRect(j*tileX,i*tileY,tileX,tileY);
}
}
}
}
};
function updateGameArea() {
gameArea.clear();
actor.canMove.check();
actor.speedX = 0;
if(actor.canMove.down){
actor.speedY += actor.gravity;
}
if (gameArea.keys[39] && actor.canMove.right) {
actor.speedX = 4;
}
if (gameArea.keys[37] && actor.canMove.left) {
actor.speedX = -4;
}
if (actor.canMove.up && (gameArea.keys[32] || gameArea.keys[38])) { //jump
if (!actor.jumping) {
actor.jumping = true;
actor.speedY = -actor.speed * 3;
}
}
actor.move(); // collision is done here
gameArea.render();
actor.draw();
requestAnimationFrame(updateGameArea);
}
function Player (width, height, color, x, y) { //player component
this.width = width;
this.height = height;
this.x = x;
this.y = y;
this.speedX=0;
this.speedY=0;
this.gravity=0.3;
this.speed=3;
this.jumping=false;
this.color = color;
this.canMove = {
left : true,
right : true,
up : true,
down : true,
actor : this,
clear(){
this.left = true;
this.right = true;
this.up = true;
this.down = true;
},
check(){
this.clear();
var x = this.actor.x | 0;
var y = this.actor.y | 0;
var cx = x / tileX | 0;
var cy = y / tileY | 0;
if(x % tileX === 0){
if(getMap(cx-1,cy) === 1){
this.left = false;
if(y % tileY !== 0 && getMap(cx-1,cy +1) === 1){
this.left = false;
}
}
if(getMap(cx+1,cy) === 1){
this.right = false;
if(y % tileY !== 0 && getMap(cx+1,cy +1) === 1){
this.right = false;
}
}
}
if(y % tileY === 0){
if(getMap(cx,cy-1) === 1){
this.up = false;
if(x % tileX !== 0 && getMap(cx+1,cy -1) === 1){
this.up = false;
}
}
if(getMap(cx,cy+1) === 1){
this.down = false;
if(x % tileX !== 0 && getMap(cx+1,cy +1) === 1){
this.down = false;
}
}
}
}
};
this.draw = function () {
var ctx = gameArea.ctx;
ctx.fillStyle = this.color;
ctx.fillRect( this.x,this.y, this.width, this.height);
};
this.move = function() {
var x = this.x;
var y = this.y;
var sx = this.speedX;
var sy = this.speedY;
var speed = Math.sqrt(sx * sx + sy * sy);
if(speed > 0){
sx /= speed;
sy /= speed;
for(var i = 0; i < speed; i++){
var xx = (x + sx * i) | 0;
var yy = (y + sy * i) | 0;
var cx = xx / tileX | 0;
var cy = yy / tileY | 0;
if(sy > 0){
if(getMap(cx,cy+1) === 1 || (xx % tileX !== 0 && getMap(cx + 1,cy+1))){
this.y = y = cy * tileY;
this.speedY = sy = speed < 4 ? 0 : -3;
this.jumping = false;
}
}else if(sy < 0){
if(getMap(cx,cy) === 1 || (xx % tileX !== 0 && getMap(cx + 1,cy))){
cy += 1;
this.y = y = cy * tileY;
this.speedY = sy = -sy; // changing -sy to 0 will stick momentarily to the roof.
}
}
if(sx > 0){
if(getMap(cx+1,cy) === 1 || (yy % tileY !== 0 && getMap(cx + 1,cy+1))){
this.x = x = cx * tileX;
this.speedX = sx = 0;
}
}else if(sx < 0){
if(getMap(cx,cy) === 1 || (yy % tileY !== 0 && getMap(cx,cy+1))){
cx += 1;
this.x = x = cx * tileX;
this.speedX = sx = 0;
}
}
}
}
this.x += this.speedX;
this.y += this.speedY;
}
}
function getMap(x,y){
if(y < 0 || y >= levelRows || x < 0 || x >= levelCols){
return 1;
}
return mapArray[y][x];
}
gameArea.start();
actor = new Player(32, 32, "green", 32, 32);
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id = "canvas" width="640" height="640"></canvas>

How to make a game on javascript with canvas that makes a sprite appear to be jumping

I am new to javascript, and am trying to make a game that would hopefully end up isometric (I don't care so much about that, as long as I get an idea of how to). My code is:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
canvas {
border:1px solid #d3d3d3;
background-color: #f1f1f1;
}
</style>
</head>
<body onload="startGame()">
<script>
var myGamePiece;
function startGame() {
myGamePiece = new component(30, 30, "blue", 225, 225);
myGameArea.start();
}
var myGameArea = {
canvas : document.createElement("canvas"),
start : function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
document.body.insertBefore(this.canvas, document.body.childNodes[0]);
this.frameNo = 0;
this.interval = setInterval(updateGameArea, 20);
window.addEventListener('keydown', function (e) {
e.preventDefault();
myGameArea.keys = (myGameArea.keys || []);
myGameArea.keys[e.keyCode] = (e.type == "keydown");
})
window.addEventListener('keyup', function (e) {
myGameArea.keys[e.keyCode] = (e.type == "keydown");
})
},
stop : function() {
clearInterval(this.interval);
},
clear : function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y, type) {
this.type = type;
this.width = width;
this.height = height;
this.speed = 0;
this.angle = 0;
this.moveAngle = 0;
this.x = x;
this.y = y;
this.update = function() {
ctx = myGameArea.context;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.fillStyle = color;
ctx.fillRect(this.width / -2, this.height / -2, this.width, this.height);
ctx.restore();
}
this.newPos = function() {
this.angle += this.moveAngle * Math.PI / 180;
this.x += this.speed * Math.sin(this.angle);
this.y -= this.speed * Math.cos(this.angle);
}
}
function updateGameArea() {
myGameArea.clear();
myGamePiece.moveAngle = 0;
myGamePiece.speed = 0;
if (myGameArea.keys && myGameArea.keys[37]) {myGamePiece.x -=2; }
if (myGameArea.keys && myGameArea.keys[39]) {myGamePiece.x += 2; }
if (myGameArea.keys && myGameArea.keys[38]) {myGamePiece.y -= 1; }
if (myGameArea.keys && myGameArea.keys[40]) {myGamePiece.y += 1; }
if (myGameArea.keys && myGameArea.keys[32]) {myGamePiece.y -= 3;}
myGamePiece.newPos();
myGamePiece.update();
}
</script>
<p></p>
</body>
</html>
which I mostly copied and pasted from another website (http://www.w3schools.com/games/tryit.asp?filename=trygame_movement_keyboard). What I want to know is how to make it so that when the player presses space, myGamePiece goes up and down to appear to be jumping; making it move up a certain number of spaces, but then return back to the coordinates it was before.
Game Physics. JUMPING the basics
Real world V game world.
Game jumping is usually done non deterministically, that means you are not sure when or where the play may land. Very much not like real life. In real life once you jump, where and when you land is up to gravity and air friction, unless you can fly the result of jumping is up to the universe.
In the game world this is far from true. The jumper can usually change direction, double jump, do some hang time, or combo accelerated power punch down. All these things can happen at any time depending on the input of the user. Also gravity in the game world does not act like real gravity, sometimes some thing fall faster because they are heavy, some things need a second or to to feel the effect of gravity. The list goes on.
FALLING
BUT with all that said the game must still do the important thing that makes falling unlike an elevator ride. When in free fall you accelerate, every instance of time your speed changes, when you jump up you decelerate, when you fall you accelerate. We have our position y and our speed dy (delta y) to add gravity (g) we add a constant to the speed, when traveling up the screen (dy is < 0) or down the gravity changes the speed in the same direction at the same rate.
So every frame, add gravity dy += g then add our speed to our position y += dy. And that is it a very simple simulation of gravity, which if you measure time in game frames is also a perfect simulation of real gravity (near a big thing like the earth)
Thus the best way to do things like jumping, and the gravity that comes into play is to do it frame by frame.
Lets define what we need to do a jump.
A simple character
var c = {
x : ?, // this character's position
y : ?,
dx : ?, // the amount to move per frame The players velocity in x and y
dy : ?,
w : ?, // the character's width and height
h : ?,
onGround : false, // a flag to indicate on the ground or not
}
And some environment info
const GROUND_Y = canvas.height - 10; // where the ground is
const GRAVITY = 1; // in pixels per frame
Then every frame we update the character checking if on the ground and if not applying gravity and checking for the ground.
c.update = function(){
if(this.onGround){ // nothing to do but wait
}else{ // must be in the air
// Every frame the player accelerates down by the pull of gravity
// so increase the player y speed
this.dy += GRAVITY; // apply the gravity to the speed.
// now add the y speed to the y position
this.y += this.dy;
// Now we must check for the ground which if the player position x,y is for
// its center the ground will be half it's height away
if(this.y + (this.h / 2) > GROUND_Y){ // have we hit the ground
// yes stop downward motion
this.dy = 0;
// the speed may have put the character slightly below the ground
// so fix the postion so that it is correct
this.y = GROUND_Y - this.h /2; // set position to the ground - half its height
// And set the flag to indicate that the character is on the ground
this.onGround = true;
}
}
}
So that is gravity taken care of.
JUMPING
To jump we apply a force that accelerates us away from the ground. This force is only for an instant, once of the ground we have nothing to push against so we can apply no more force, it is up to gravity to bring us down. As gravity has been sorted in the above function all we need to do is the apply the jumping force.
const JUMP_ACCELERATION = GRAVITY * 20; // the bigger this number the higher the jump
Now add the function to make the jump
c.jump = function(){
// check if we can jump. That is are we on the ground
if(this.onGround){
// flag that we are no longer on the ground and left to the will of gravity
this.onGround = false;
// then apply the change in speed.
this.dy -= JUMP_ACCELERATION; // subtract jump accel from the speed
// to give a negative speed (up)
}
}
And that is it, the gravity function will take care of everything for you so you must call the c.update Function once every frame, the jump function you call only once per jump.
JUMPING IT DEMO
Click mouse to jump, a non challenging flappy It.
Taken from an old project this demo shows a very simple jumping character. The object name is it and the functions you want to look at are it.update(), it.jump(), and it.preJump() The code you want is between the comments //Answer code
All the character can do is jump, it can multy jump, and can jump higher if you click and hold the mouse, then release to jump.
/** ImageTools.js begin **/
var imageTools = (function () {
var tools = {
canvas : function (width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage : function (width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
loadImage : function (url, cb) {
var i = new Image();
i.src = url;
i.addEventListener('load', cb);
i.addEventListener('error', cb);
return i;
},
image2Canvas : function (img) {
var i = this.canvas(img.width, img.height);
i.ctx = i.getContext("2d");
i.drawImage(i, 0, 0);
return i;
},
drawSpriteLinked : function(image,spriteIndex, x, y, scale, ang, alpha) {
var w,h,spr;
spr = image.sprites[spriteIndex];
w = spr.w; h = spr.h;
ctx.globalAlpha = alpha;
var xdx = Math.cos(ang) * scale;
var xdy = Math.sin(ang) * scale;
ctx.save();
ctx.transform(xdx, xdy, -xdy, xdx, x, y);
ctx.drawImage(image, spr.x, spr.y, w, h, -w/2, -h/2, w, h);
ctx.restore();
},
drawSprite : function(image,spriteIndex, x, y, scale, ang, alpha) {
var w,h,spr;
spr = image.sprites[spriteIndex];
w = spr.w; h = spr.h;
ctx.globalAlpha = alpha;
ctx.setTransform(scale, 0, 0, scale, x, y);
ctx.rotate(ang);
ctx.drawImage(image, spr.x, spr.y, w, h, -w/2, -h/2, w, h);
},
drawSpriteSLinked : function(image,spriteIndex, x, y, scale, scaleX, ang, alpha) {
var w,h,spr;
spr = image.sprites[spriteIndex];
w = spr.w; h = spr.h;
ctx.globalAlpha = alpha;
var xdx = Math.cos(ang) * scale;
var xdy = Math.sin(ang) * scale;
ctx.save()
ctx.transform(xdx * scaleX, xdy * scaleX, -xdy, xdx, x, y);
ctx.drawImage(image, spr.x, spr.y, w, h, -w/2, -h/2, w, h);
ctx.restore();
},
drawSpriteS : function(image,spriteIndex, x, y, scale, scaleX, ang, alpha) {
var w,h,spr;
spr = image.sprites[spriteIndex];
w = spr.w; h = spr.h;
ctx.globalAlpha = alpha;
ctx.setTransform(scale * scaleX, 0, 0, scale, x, y);
ctx.rotate(ang);
ctx.drawImage(image, spr.x, spr.y, w, h, -w/2, -h/2, w, h);
},
hex2RGBA : function(hex){
if(typeof hex === "string"){
var str = "rgba(";
if(hex.length === 4 || hex.length === 5){
str += (parseInt(hex.substr(1,1),16) * 16) + ",";
str += (parseInt(hex.substr(2,1),16) * 16) + ",";
str += (parseInt(hex.substr(3,1),16) * 16) + ",";
if(hex.length === 5){
str += (parseInt(hex.substr(3,1),16) / 16);
}else{
str += "1";
}
return str + ")";
}
if(hex.length === 7 || hex.length === 8){
str += parseInt(hex.substr(1,2),16) + ",";
str += parseInt(hex.substr(3,2),16) + ",";
str += parseInt(hex.substr(5,2),16) + ",";
if(hex.length === 5){
str += (parseInt(hex.substr(7,2),16) / 255).toFixed(3);
}else{
str += "1";
}
return str + ")";
}
return "rgba(0,0,0,0)";
}
},
createGradient : function(ctx, type, x, y, xx, yy, colours){
var i,g,c;
var len = colours.length;
if(type.toLowerCase() === "linear"){
g = ctx.createLinearGradient(x,y,xx,yy);
}else{
g = ctx.createRadialGradient(x,y,xx,x,y,yy);
}
for(i = 0; i < len; i++){
c = colours[i];
if(typeof c === "string"){
if(c[0] === " #"){
c = this.hex2RGBA(c);
}
g.addColorStop(Math.min(1,i / (len -1)),c); // need to clamp top to 1 due to floating point errors causes addColorStop to throw rangeError when number over 1
}
}
return g;
},
};
return tools;
})();
/** ImageTools.js end **/
/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars
var canvas, ctx, mouse;
var globalTime = 0;
var globalTimeInt = 0;
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
c.id = CANVAS_ELEMENT_ID;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
var resized = false;
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
resized = true;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){
cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2;
if(it !== undefined){
it = createIt(cw,ch,sprites);
}
}
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
}
}
return mouse;
})();
var done = function(){
window.removeEventListener("resize",resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = U;
L("All done!")
}
resizeCanvas(); // create and size canvas
resized = false;
mouse.start(canvas,true); // start mouse on canvas and block context menu
window.addEventListener("resize",resizeCanvas); // add resize event
function drawText(text,x,y,size,col){
var f = size + "px Arial";
if(f !== ctx.font){
ctx.font = f;
}
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = col;
ctx.fillText(text,x,y);
}
function drawLoad(){
if(!resourcesReady || !canPlay){
drawText(message,cw,ch * 0.5, FONT_SIZE, MESSAGE_COL);
if (!canPlay && resourcesReady){
drawText("Try reloading the page.",cw,ch * 0.5 + FONT_SIZE + 8,Math.floor(FONT_SIZE /2) ,MESSAGE_COL);
}else{
drawText("Loading resources." ,cw,ch * 0.5 + FONT_SIZE + 8,Math.floor(FONT_SIZE /2) ,MESSAGE_COL);
}
}else{
if(message !== ""){
drawText(message,cw,ch * 0.5, FONT_SIZE, MESSAGE_COL);
}
}
}
const FONT = "px Arial"
const FONT_SIZE = Math.max(Math.floor(window.innerHeight/20),24)
ctx.textAlign = "center";
ctx.textBaseline = "middle";
function loaded(e){
if(e.type !== "error"){
this.sprites = [
{ x : 0, y : 0, w : 74, h : 116, },
{ x : 0, y : 126, w : 100, h : 113, },
{ x : 75, y : 0, w : 29, h : 42, },
{ x : 75, y : 43, w : 17, h : 22, },
{ x : 0, y : 249, w : 42, h : 18, },
{ x : 75, y : 66, w : 17, h : 15, },
{ x : 75, y : 82, w : 17, h : 12, },
{ x : 75, y : 95, w : 16, h : 9, },
{ x : 75, y : 105, w : 7, h : 7, },
{ x : 0, y : 268, w : 11, h : 5, },
]
resourcesReady = true;
canPlay = true;
it = createIt(cw,ch,this );
message = "";
return;
}
resourcesReady = true;
message = "LOAD FAILED!"
}
var it = null; // it is the character
var resourcesReady = false;
var canPlay = false;
var message = "Please Wait..."
const MESSAGE_COL = "white";
//var sprites = imageTools.loadImage("GreenIt.png",loaded )
var sprites = imageTools.loadImage("http://i.stack.imgur.com/ED6oC.png",loaded )
var background = imageTools.createImage(8,8);
background.ctx.fillStyle = imageTools.createGradient(ctx,"linear",0,0,8,8,["#0AF","#05A"]);
background.ctx.fillRect(0,0,8,8);
var ground = imageTools.createImage(8,32);
ground.ctx.fillStyle = imageTools.createGradient(ctx,"linear",0,0,8,32,["#0A0","#450","#754"]);
ground.ctx.fillRect(0,0,8,32);
ground.ctx.fillStyle = "black";
ground.ctx.fillRect(0,0,8,4);
const GROUND_OFFSET = 32;
const GRAV = 1;
var landed = false;
const MESSAGES = [
"Click mouse button to Jump",
"Click hold ... release to to add power to jump",
"Double click to double jump",
""
];
var messageCount = 0;
var fly = { // something to see
x : 0,
y : 0,
dx : 0,
dy : 0,
wait : 0,
onTheWall : false,
update : function(){
if(this.wait <= 0){
this.wait = Math.random() * 200+ 60;
this.onTheWall = Math.random() < 0.1 ? true : false;
if(this.onTheWall){
this.dx = 0;
this.dy = 0;
}else{
this.wait = Math.random() < 0.2 ? 10 : this.wait;
var x = (Math.random()-0.5) * 200;
var y = (Math.random()-0.5) * 200;
this.dx = (x - this.x) / this.wait;
this.dx = (y - this.y) / this.wait;
}
}else{
this.wait -= 1;
this.x += this.dx;
this.y += this.dy;
}
}
};
/*==============================================================================================
// Answer code
==============================================================================================*/
// info to define the character
const IT = {
body : 0, // sprite indexes
bodyFly : 1,
footDown : 2,
eyeOpen : 3,
foot : 4,
mouthOpen : 5,
eyeShut : 6,
mouthSmirk : 7,
eyeBall : 8,
mouth : 9, // sprite index end
grav : GRAV, // grav accel
maxJumpPower : 40,
minJump : 10,
jumpPower : 30, // mutiplys squat amount to give jump power
squatRate : 1, // how quick the squat is
squatResist : 0.8, // limits the amount of squat
landingBlinkTime : 30, // how long blink is on landing
blinkTime : 15, // how many frames to close eyes
blinkRate : 60 * 3, // 60 is one second . Time between blinks average
eyePos : {x : 0.13, y : -0.1}, // position as fraction of size
footPos : {x : 0.3, y : 0.5}, // position as fraction of size
lookAtGround : 1, // look ats
lookAtMouse : 2,
lookAtUser : 3,
lookAtFly : 4,
angle: 0,
jumpDy: 0, // the jump up speed used to rotate It when in air
}
// Function updates the character
const updateIt = function(){
if(this.blink > 0){
this.blink -= 1;
}
if(this.blinkTimer > 0){
this.blinkTimer -= 1;
if(this.blinkTimer === 0){
this.blink = IT.blinkTime;
}
}else{
// the two randoms create a random number that has a gausian distrabution centered on 0.5
// this creates a more realistic set of numbers.
this.blinkTimer = Math.floor(IT.blinkRate * (Math.random() + Math.random())/2 + IT.blinkRate / 2);
this.lookAt = Math.random() < 0.33 ? IT.lookAtUser : (Math.random() < 0.5 ? IT.lookAtMouse : IT.lookAtFly);
}
if(!this.onGround){
this.squat = 0;
//-------------------------------------
// do gravity
this.dy += IT.grav;
this.y += this.dy;
this.x += this.dx;
this.x = (this.x + ctx.canvas.width) % ctx.canvas.width;
var rotFraction = (this.jumpDy - this.dy) / this.jumpDy;
this.angle = this.jumpAngle * -rotFraction ;
if(this.dy > 13){
this.lookAt = IT.lookAtGround;
}
// check for the ground
if(this.y + this.tall / 2 > h - GROUND_OFFSET){
this.y = h - GROUND_OFFSET - this.tall / 2;
this.blink = Math.floor(IT.landingBlinkTime * (this.dy / 20));
this.blinkTimer = this.blink + 30;
this.squat = this.dy;
this.dy = 0;
this.onGround = true;
this.angle = -this.jumpAngle
}
}else{
this.squat *= IT.squatResist;
}
}
// draw the character
const drawIt = function(){
var bod = IT.body;
var spr = this.img.sprites;
var eye = this.blink > 0 ? IT.eyeShut : IT.eyeOpen;
var foot = IT.foot;
var footBehind = false; // draw feet behind or infront of body
if(!this.onGround){
if(this.dy >= 0){
if(this.dy > 2){
bod = IT.bodyFly;
}
}else{
footBehind = true;
foot = IT.footDown;
}
}
var xdx = Math.cos(this.angle);
var xdy = Math.sin(this.angle);
var px = this.x; // pivot
var py = this.y + 50;
var x = this.x ;
var y = this.y + this.squat;
var t = this.tall;
var f = this.fat;
if(footBehind){
if(!this.onGround){
var r = 1 - Math.min(1,-this.dy / 10);
imageTools.drawSpriteS(this.img,foot,x + f * IT.footPos.x,y - this.squat+ t * IT.footPos.y,1,-1,r,1);
imageTools.drawSprite(this.img,foot,x - f * IT.footPos.x,y - this.squat + t * IT.footPos.y,1,r,1);
}
}
ctx.setTransform(xdx,xdy,-xdy,xdx,px,py);
imageTools.drawSpriteLinked(this.img,bod,x - px,y - py,1,0,1);
if(!footBehind){
if(this.onGround){
imageTools.drawSpriteS(this.img,foot,x + f * IT.footPos.x,y - this.squat+ t * IT.footPos.y,1,-1,0,1);
imageTools.drawSprite(this.img,foot,x - f * IT.footPos.x,y - this.squat + t * IT.footPos.y,1,0,1);
}else{
var r = this.dy / 10;
imageTools.drawSpriteS(this.img,foot,x + f * IT.footPos.x,y - this.squat+ t * IT.footPos.y,1,-1,r,1);
imageTools.drawSprite(this.img,foot,x - f * IT.footPos.x,y - this.squat + t * IT.footPos.y,1,r,1);
}
}
if(this.blink){
ctx.setTransform(xdx,xdy,-xdy,xdx,px,py);
imageTools.drawSpriteLinked(this.img,eye,x + f * IT.eyePos.x - px, y + t * IT.eyePos.y - py,1,0,1);
imageTools.drawSpriteSLinked(this.img,eye,x - f * IT.eyePos.x - px, y + t * IT.eyePos.y - py,1,-1,0,1);
}else{
ctx.setTransform(xdx,xdy,-xdy,xdx,px,py);
imageTools.drawSpriteLinked(this.img,eye,x + f * IT.eyePos.x - px, y + t * IT.eyePos.y - py,1,0,1);
imageTools.drawSpriteSLinked(this.img,eye,x - f * IT.eyePos.x - px, y + t * IT.eyePos.y - py,1,-1,0,1);
var eyeDir = 0;
var eyeDist = 0;
if(this.blink === 0){
if(this.lookAt === IT.lookAtGround){
eyeDir = Math.PI/2;
eyeDist = 0.3;
}else if(this.lookAt === IT.lookAtUser){
eyeDir = 0;
eyeDist = 0;
}else if(this.lookAt === IT.lookAtFly){
eyeDir = Math.atan2(fly.y, fly.x);
eyeDist = (Math.hypot(fly.y ,fly.x) / 200) * 0.3;
}else{
eyeDir = Math.atan2(mouse.y - this.y, mouse.x - this.x);
eyeDist = (Math.hypot(this.y - mouse.y,this.x - mouse.x) / (Math.min(w,h)/2)) * 0.3;
}
eyeDist = Math.max(-0.3, Math.min(0.3, eyeDist));
var ex,ey;
ex = Math.cos(eyeDir) * spr[IT.eyeOpen].w * eyeDist;
ey = Math.sin(eyeDir) * spr[IT.eyeOpen].h * eyeDist;
imageTools.drawSpriteLinked(this.img, IT.eyeBall, x + f * IT.eyePos.x + ex - px, y + t * IT.eyePos.y + ey-py,1,0,1);
imageTools.drawSpriteLinked(this.img, IT.eyeBall, x - f * IT.eyePos.x + ex - px, y + t * IT.eyePos.y + ey-py,1,0,1);
}
}
}
// While mouse is down squat and prep to jump
const preJump = function(){
this.squat += IT.squatRate;
this.jumpPower += 0.5;
if(this.jumpPower > 30 && this.wiggle === 0) {
this.wiggle = 1;
}
this.jumpReady = true;
}
// when mouse released apply jump force
const jumpIt = function(){
var power = -IT.jumpPower * Math.min(IT.maxJumpPower,Math.max(IT.minJump,this.jumpPower))/IT.maxJumpPower;
this.dy = Math.sin(this.angle + Math.PI /2) * power;
this.dx = Math.cos(this.angle + Math.PI /2) * power;
if(this.onGround){
this.jumpDy = this.dy;
this.jumpAngle = this.angle;
}
this.wiggle = 0;
this.jumpPower = 0;
this.jumpReady = false;
this.squat = 0;
this.onGround = false;
}
// creates a character
var createIt = function(x,y,img){
return {
img : img,
x : x, // position
y : y,
dx : 0, // deltat speed
dy : 0,
sqaut : 0, // for landing and pre jump slight squat
onGround : false,
jumpPower : 0,
blink : 0, // blink controls
blinkTimer : 0,
lookAt : "ground", /// where to look
jumpReady : false, // flags if ready to jump
tall : img.sprites[IT.body].h, // how tall
fat : img.sprites[IT.body].w, // how wide
draw : drawIt, // functions
update : updateIt,
jump : jumpIt,
squatF : preJump,
}
}
function display(){ // put code in here
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.drawImage(background,0,0,w,h)
ctx.drawImage(ground,0,h-GROUND_OFFSET,w,GROUND_OFFSET);
fly.update()
drawLoad();
if(canPlay){
if(messageCount < MESSAGES.length){
if(it.onGround && !landed){
landed = true;
message = MESSAGES[messageCount];
messageCount += 1;
}
}
if(resized) { // to prevent resize display issue
resized = false;
it.y = h - GROUND_OFFSET - it.tall / 2;
}
if(it.onGround) {
it.angle = Math.atan2((it.y + 130)-10, it.x- mouse.x) / 3;
it.angle = it.angle < -1 ? -1 : it.angle > 1 ? 1 : it.angle;
it.angle = Math.pow(Math.abs(it.angle),0.5) * Math.sign(it.angle);
it.angle -= Math.PI / 4;
if(it.wiggle > 0.1) {
it.angle += Math.sin((it.wiggle * Math.PI) ** 2) * 0.01 * it.wiggle;
it.wiggle *= 0.95;
}
}
if(mouse.buttonRaw & 1){
it.squatF();
}else{
if(it.jumpReady){
it.jump();
landed = false;
}
}
it.update();
it.draw();
}
//ctx.clearRect(0,0,w,h);
}
/*==============================================================================================
// Answer End
==============================================================================================*/
function update(timer){ // Main update loop
globalTimeInt = Math.floor(globalTime = timer);
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
I recommend you check this (part 1) and this(part 2) tutorial that I have followed.
Your "jump" animation just boils down to creating a jump function that sets a max jump height to your object and sets a boolean to var jumping = true. As long as your character is "jumping" you increment the y position of your character.
Once you get to your desired height, create a land function that does the opposite.
Make a Javascript setinterval to update the height of object after every 20ms.
after every 20s set height = Initial_Height + u*t - (1/2)gt^2
use g = 9.8, u = some constant according to your screen.
t = Time passed till now. Which mean, Initially t=0, on first update t=20ms, on second update t=40ms.
Basically, you are simulating real life jumping in gravity.

Categories