Reputation: 876
Following the turorial with the same exact code, but getting this error to a console when trying to draw an image to a canvas:
SpriteSheet.js:30 Uncaught (in promise) TypeError: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(CSSImageValue or HTMLImageElement or SVGImageElement or HTMLVideoElement or HTMLCanvasElement or ImageBitmap or OffscreenCanvas)
No idea why is this happening? The first image is drawed fine on the canvas but the second one is not showing up and getting this error. Using promises so all the images should be loaded before using them, right? It works fine if I change the end of the script.js file to drawBackground(level.backgrounds[1], context, sprites);
script.js
import SpriteSheet from './SpriteSheet.js';
import {loadImage, loadLevel} from './loaders.js';
function drawBackground(background, context, sprites) {
background.ranges.forEach(([x1, x2, y1, y2]) => {
for (let x = x1; x < x2; ++x) {
for (let y = y1; y < y2; ++y) {
sprites.drawTile(background.tile, context, x, y);
}
}
});
}
function loadBackgroundSprites() {
return loadImage('SEA01.png').then(image => {
const sprites = new SpriteSheet(image, 16, 16);
sprites.define('ocean', 0, 0);
return sprites;
});
return loadImage('ground.png').then(image=> {
const sprites = new SpriteSheet(image, 16, 16);
sprites.define('ground', 12, 0);
});
}
const canvas = document.getElementById('gamearea');
const context = canvas.getContext('2d');
Promise.all([
loadBackgroundSprites(),
loadLevel('1-1')
]).then(([sprites,level]) => {
console.log(level);
drawBackground(level.backgrounds[0], context, sprites);
});
loader.js
export function loadImage(url) {
return new Promise(resolve => {
const image = new Image();
image.addEventListener('load', () => {
resolve(image);
});
image.src = url;
});
}
export function loadLevel(name) {
return fetch(`/levels/${name}.json`)
.then(r => r.json());
}
spritesheet.js
export default class SpriteSheet {
constructor(image, w = 16, h = 16) {
this.image = image;
this.width = w;
this.height = h;
this.tiles = new Map();
}
define(name, x, y) {
const buffer = document.createElement('canvas');
buffer.height = this.height;
buffer.width = this.width;
buffer
.getContext('2d')
.drawImage(
this.image,
this.width * x,
this.height * y,
this.width,
this.height,
0,
0,
this.width,
this.height);
this.tiles.set(name, buffer);
}
draw(name, context, x, y) {
const buffer = this.tiles.get(name);
context.drawImage(buffer, x, y);
}
drawTile(name, context, x, y) {
this.draw(name, context, x * this.width, y * this.height);
}
}
1-1.json
{
"backgrounds": [
{
"tile": "ocean",
"ranges": [
[
0, 50,
0, 25
]
]
},
{
"tile": "ground",
"ranges": [
[
18, 25,
10, 15
]
]
}
]
}
Thanks in advance.
EDIT:
I updated my code to load the images in different functions, but get the same error:
script.js
import SpriteSheet from './SpriteSheet.js';
import {loadImage, loadLevel} from './loaders.js';
const canvas = document.getElementById('gamearea');
const context = canvas.getContext('2d');
function drawBackground(background, context, sprites) {
background.ranges.forEach(([x1, x2, y1, y2]) => {
for (let x = x1; x < x2; ++x) {
for (let y = y1; y < y2; ++y) {
sprites.drawTile(background.tile, context, x, y);
}
}
});
}
function drawMario(background, context, mario) {
background.ranges.forEach(([x1, x2, y1, y2]) => {
for (let x = x1; x < x2; ++x) {
for (let y = y1; y < y2; ++y) {
mario.drawTile(background.tile, context, x, y);
}
}
});
}
function loadBackgroundSprites() {
return loadImage('SEA01.png').then(image => {
const sprites = new SpriteSheet(image, 16, 16);
sprites.define('ocean', 0, 0);
return sprites;
});
}
function loadDirtSprite() {
return loadImage('ground.png').then(image => {
const mario = new SpriteSheet(image, 16, 16);
mario.define('ground', 12, 0);
return mario;
});
}
Promise.all([
loadBackgroundSprites(),
loadLevel('1-1'),
loadDirtSprite()
]).then(([sprites, level, mario]) => {
level.backgrounds.forEach(background => {
drawBackground(background, context, sprites, mario);
});
mario.draw('ground', context, 1, 16);
});
Upvotes: 1
Views: 7025
Reputation: 1
I know this was quite some time ago. But when you created a new promise in the loader.js module - Make sure to add a .catch((err) => console.log(err))
Also after you update if you're still seeing the problem trying Ctrl+Shift+R to refresh the static http cache which may still be caching your old code. If this helps, let me know!
loader.js
export function loadImage(url) {
return new Promise(resolve => {
const image = new Image();
image.addEventListener('load', () => {
resolve(image);
});
image.src = url;
}).catch((err) => console.log(err)); <--------ADD THIS
}
Upvotes: 0
Reputation: 19301
replaced answer:
Bug number 1.
Mentioned in question comment, the original code has an error in loadBackgroundSprites
:
function loadBackgroundSprites() {
return loadImage('SEA01.png').then(image => {
const sprites = new SpriteSheet(image, 16, 16);
sprites.define('ocean', 0, 0);
return sprites;
}); // <-- bad indent
return loadImage('ground.png').then(image=> {
const sprites = new SpriteSheet(image, 16, 16);
sprites.define('ground', 12, 0);
});
}
The line in the middle marked as "bad indent" is where the return statement started at the beginning of loadBackgroundSprites
ends and execution returns to the caller. The return
statement after the "bad indent" line never executes and generates a console message.
⚠ unreachable code after return statement [Learn More]
Because the warning is non-fatal, the function returns without error.
The first fix for this bug is to remove the unreachable code. Two additional fixes are required as discussed under the next bug.
loadImage
Bug number 2.
Instances of the SpriteSheet
class contain a single image, passed as the argument to the constructor. This image should contain an array of rectangular tiles of fixed width and height matching the 2nd and 3rd parameter values used in the constructor call. This is not what you are doing.
Actually, defining a tile in a SpriteSheet
instance copies a single tile from the sprite sheet image into a new, named Canvas object for use by drawing functions.
A suggested remedy for working with sprite sheets as designed is to create a backgrounds sprite sheet image that contains at least the two 16x16 tiles for ocean and ground. Their positions in the sprite sheet image (counting tiles left-across and top-down) determines how tile definition should be coded in loadBackgroundSprites
.
For this solution, remove code references to dirtSprite
. And remove image references to "SEA01.png" and "ground.png" - loadBackgroundSprites
should be loading the URL for a combined sprite sheet containing both tiles.
In the Promise.all
chain use use the forEach
version of the code to render the sprites according to the json file:
Promise.all([
loadBackgroundSprites(),
loadLevel('1-1')
]).then(([sprites,level]) => {
console.log(level);
level.backgrounds.forEach(background => {
drawBackground(background, context, sprites)
});
Upvotes: 1