I've recently been watching some of Notch's streams on twitch and was interested in one of his rendering techniques for the Ludum Dare challenge a few years ago. I tried converting his java code to javascript and am running into some problems, and it's because I'm still new to ctx.putimagedata from raw created pixels values.
Why is this app drawing the intended output 4 times, and not being scaled to the window? Is there something I'm missing where I should be iterating with a multiplicate or divisor of 4 due to the way the array is shaped? I'm confused so just going to post this here. The only fix I've found is if I adjust this.width and this.height to be multiplied by 4, but that is drawing outside the bounds of the canvas I believe and causes performance to go awful and is not really a valid solution to the problem.
class in question:
document.addEventListener('DOMContentLoaded', () => {
//setup
document.body.style.margin = 0;
document.body.style.overflow = `hidden`;
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext("2d");
ctx.fillRect(0, 0, canvas.width, canvas.height);
//global helpers
const randomint = (lower, upper) => {
return Math.floor((Math.random() * upper+1) + lower);
}
const genrandomcolor = () => {
return [randomint(0, 255), randomint(0, 255), randomint(0, 255), 1/randomint(1, 2)];
}
class App {
constructor(){
this.scale = 15;
this.width = window.innerWidth;
this.height = window.innerHeight;
this.pixels = [];
this.fov = 10;
this.ub = 0;
this.lr = 0;
this.keys = {
up: false,
down: false,
left: false,
right: false
}
this.speed = 4;
}
update(){
this.keys.up ? this.ub++ : null;
this.keys.down ? this.ub-- : null;
this.keys.left ? this.lr-- : null;
this.keys.right ? this.lr++ : null;
}
draw(){
this.drawspace()
}
drawspace(){
for(let y = 0; y < this.height; y++){
let yd = (y - this.height / 2) / this.height;
yd < 0 ? yd = -yd : null;
const z = this.fov / yd;
for (let x = 0; x < this.width; x++){
let xd = (x - this.width /2) / this.height * z;
const xx = (xd+this.lr*this.speed) & this.scale;
const zz = (z+this.ub*this.speed) & this.scale;
this.pixels[x+y*this.width] = xx * this.scale | zz * this.scale;
}
}
const screen = ctx.createImageData(this.width, this.height);
for (let i = 0; i<this.width*this.height*4; i++){
screen.data[i] = this.pixels[i]
}
ctx.putImageData(screen, 0, 0);
}
}
const app = new App;
window.addEventListener('resize', e => {
canvas.width = app.width = window.innerWidth;
canvas.height = app.height = window.innerHeight;
})
//events
document.addEventListener("keydown", e => {
e.keyCode == 37 ? app.keys.left = true : null;
e.keyCode == 38 ? app.keys.up = true : null;
e.keyCode == 39 ? app.keys.right = true : null;
e.keyCode == 40 ? app.keys.down = true : null;
})
document.addEventListener("keyup", e => {
e.keyCode == 37 ? app.keys.left = false : null;
e.keyCode == 38 ? app.keys.up = false : null;
e.keyCode == 39 ? app.keys.right = false : null;
e.keyCode == 40 ? app.keys.down = false : null;
})
//game loop
const fps = 60;
const interval = 1000 / fps;
let then = Date.now();
let now;
let delta;
const animate = time => {
window.requestAnimationFrame(animate);
now = Date.now();
delta = now - then;
if (delta > interval) {
then = now - (delta % interval)
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
app.update();
app.draw();
}
}
animate();
});
The ImageData.data object is an Uint8ClampedArray representing the 4 Red, Green, Blue, and Alpha channels of every pixels, each channel represented as 8 bits (values in the range 0-255).
This means that to set a pixel, you need to set its 4 channels independently:
const r = data[0];
const g = data[1];
const b = data[2];
const a = data[3];
This represents the first pixel of our ImageData (the one at the top left corner).
So to be able to loop through all the pixels, we need to create a loop that will allow us to go from one pixel to an other. This is done by iterating 4 indices at a time:
for(
let index = 0;
index < data.length;
index += 4 // increment by 4
) {
const r = data[index + 0];
const g = data[index + 1];
const b = data[index + 2];
const a = data[index + 3];
...
}
Now each pixels will get traversed as they need to be:
//setup
document.body.style.margin = 0;
document.body.style.overflow = `hidden`;
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext("2d");
ctx.fillRect(0, 0, canvas.width, canvas.height);
//global helpers
const randomint = (lower, upper) => {
return Math.floor((Math.random() * upper + 1) + lower);
}
const genrandomcolor = () => {
return [randomint(0, 255), randomint(0, 255), randomint(0, 255), 1 / randomint(1, 2)];
}
class App {
constructor() {
this.scale = 15;
this.width = window.innerWidth;
this.height = window.innerHeight;
this.pixels = [];
this.fov = 10;
this.ub = 0;
this.lr = 0;
this.keys = {
up: false,
down: false,
left: false,
right: false
}
this.speed = 4;
}
update() {
this.keys.up ? this.ub++ : null;
this.keys.down ? this.ub-- : null;
this.keys.left ? this.lr-- : null;
this.keys.right ? this.lr++ : null;
}
draw() {
this.drawspace()
}
drawspace() {
for (let y = 0; y < this.height; y++) {
let yd = (y - this.height / 2) / this.height;
yd < 0 ? yd = -yd : null;
const z = this.fov / yd;
for (let x = 0; x < this.width; x++) {
let xd = (x - this.width / 2) / this.height * z;
const xx = (xd + this.lr * this.speed) & this.scale;
const zz = (z + this.ub * this.speed) & this.scale;
this.pixels[x + y * this.width] = xx * this.scale | zz * this.scale;
}
}
const screen = ctx.createImageData(this.width, this.height);
for (let i = 0, j=0; i < screen.data.length; i += 4) {
j++; // so we can iterate through this.pixels
screen.data[i] = this.pixels[j]; // r
screen.data[i + 1] = this.pixels[j], // g
screen.data[i + 2] = this.pixels[j] // b
screen.data[i + 3] = 255; // full opacity
}
ctx.putImageData(screen, 0, 0);
}
}
const app = new App;
window.addEventListener('resize', e => {
canvas.width = app.width = window.innerWidth;
canvas.height = app.height = window.innerHeight;
})
//events
document.addEventListener("keydown", e => {
e.keyCode == 37 ? app.keys.left = true : null;
e.keyCode == 38 ? app.keys.up = true : null;
e.keyCode == 39 ? app.keys.right = true : null;
e.keyCode == 40 ? app.keys.down = true : null;
})
document.addEventListener("keyup", e => {
e.keyCode == 37 ? app.keys.left = false : null;
e.keyCode == 38 ? app.keys.up = false : null;
e.keyCode == 39 ? app.keys.right = false : null;
e.keyCode == 40 ? app.keys.down = false : null;
})
//game loop
const fps = 60;
const interval = 1000 / fps;
let then = Date.now();
let now;
let delta;
const animate = time => {
window.requestAnimationFrame(animate);
now = Date.now();
delta = now - then;
if (delta > interval) {
then = now - (delta % interval)
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
app.update();
app.draw();
}
}
animate();
But note that you could also use an other view over the ArrayBuffer, and work on each pixels directly as 32bits values:
//setup
document.body.style.margin = 0;
document.body.style.overflow = `hidden`;
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext("2d");
ctx.fillRect(0, 0, canvas.width, canvas.height);
//global helpers
const randomint = (lower, upper) => {
return Math.floor((Math.random() * upper + 1) + lower);
}
const genrandomcolor = () => {
return [randomint(0, 255), randomint(0, 255), randomint(0, 255), 1 / randomint(1, 2)];
}
class App {
constructor() {
this.scale = 15;
this.width = window.innerWidth;
this.height = window.innerHeight;
this.pixels = [];
this.fov = 10;
this.ub = 0;
this.lr = 0;
this.keys = {
up: false,
down: false,
left: false,
right: false
}
this.speed = 4;
}
update() {
this.keys.up ? this.ub++ : null;
this.keys.down ? this.ub-- : null;
this.keys.left ? this.lr-- : null;
this.keys.right ? this.lr++ : null;
}
draw() {
this.drawspace()
}
drawspace() {
for (let y = 0; y < this.height; y++) {
let yd = (y - this.height / 2) / this.height;
yd < 0 ? yd = -yd : null;
const z = this.fov / yd;
for (let x = 0; x < this.width; x++) {
let xd = (x - this.width / 2) / this.height * z;
const xx = (xd + this.lr * this.speed) & this.scale;
const zz = (z + this.ub * this.speed) & this.scale;
this.pixels[x + y * this.width] = xx * this.scale | zz * this.scale;
}
}
const screen = ctx.createImageData(this.width, this.height);
// use a 32bits view
const data = new Uint32Array(screen.data.buffer);
for (let i = 0, j=0; i < this.width * this.height; i ++) {
// values are 0-255 range, we convert this to 0xFFnnnnnn 32bits
data[i] = (this.pixels[i] / 255 * 0xFFFFFF) + 0xFF000000;
}
ctx.putImageData(screen, 0, 0);
}
}
const app = new App;
window.addEventListener('resize', e => {
canvas.width = app.width = window.innerWidth;
canvas.height = app.height = window.innerHeight;
})
//events
document.addEventListener("keydown", e => {
e.keyCode == 37 ? app.keys.left = true : null;
e.keyCode == 38 ? app.keys.up = true : null;
e.keyCode == 39 ? app.keys.right = true : null;
e.keyCode == 40 ? app.keys.down = true : null;
})
document.addEventListener("keyup", e => {
e.keyCode == 37 ? app.keys.left = false : null;
e.keyCode == 38 ? app.keys.up = false : null;
e.keyCode == 39 ? app.keys.right = false : null;
e.keyCode == 40 ? app.keys.down = false : null;
})
//game loop
const fps = 60;
const interval = 1000 / fps;
let then = Date.now();
let now;
let delta;
const animate = time => {
window.requestAnimationFrame(animate);
now = Date.now();
delta = now - then;
if (delta > interval) {
then = now - (delta % interval)
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
app.update();
app.draw();
}
}
animate();