HTML Canvas - Why so under utilized?

To answer this question, we must really understand the current state of HTML Canvas.

What is HTML Canvas?

If you have ever tried ‘drawing’ on a web page, the first thing that the search engine gives you is HTML Canvas. Well that is the 2019 definition of HTML canvas. Definition as in MDN:

The Canvas API provides a means for drawing graphics via JavaScript and the HTML '<canvas>' element. Among other things, it can be used for animation, game graphics, data visualization, photo manipulation, and real-time video processing. The Canvas API largely focuses on 2D graphics. The WebGL API, which also uses the '<canvas>' element, draws hardware-accelerated 2D and 3D graphic

Why have you not used HTML Canvas?

Well because developers did not recommend it. Well mostly. The ROI almost was not worth it, but not for game devs.

If you have ever worked on games on the web, then you are amogst those few who have actually seen the Canvas APIs. Lets get started with it already then.

Getting Started.

The first thing that I noticed was that it was possible to set the width & height of the canvas exclusively using any one of CSS, JS, or HTML (we will discuss why this bifurcation).  I chose to give them these using CSS. 100vw, 100vh. Simple Then, drew a line. Drew a ball.  The pixel density seemed very low. 

Enter: window.devicePixelRatio So lets setup an function setupCanvas to encapuslate all of this.

setupCanvas(canvasDomRef) {
        // Get the device pixel ratio, falling back to 1.
        const canvas = document.querySelector(canvasDomRef);
        var dpr = window.devicePixelRatio || 1;
        // Get the size of the canvas in CSS pixels.
        var rect = canvas.getBoundingClientRect();
        // Give the canvas pixel dimensions of their CSS
        // size * the device pixel ratio.
        canvas.width = rect.width * dpr;
        canvas.height = rect.height * dpr;
        this.width = canvas.width;
        this.height = canvas.height;
        this.ctx = canvas.getContext('2d');
        // Scale all drawing operations by the dpr, so you
        // don't have to worry about the difference.
        this.ctx.scale(dpr, dpr);
        return this.ctx;
      }

Also, its 2019. No fun having a static image there. We will paint!

Enter: requestAnimationFrame

for using request animation frame, the easist way is to do something like this:

...

function draw() {
    // this is where we will put our logic for eachcycle

    requestAnimationFrame(draw);
}

draw()

...

let complete this getting started session with this structure, of what I will use to play with Canvas.

.
├── project                 
│   ├── kaaro.html          # the html file with <canvas>
│   ├── kaaro.css           # Styles anyone?
│   └── kaaro.js            # Our main JS file - with draw() function
├── utils                    
│   └── kCanvas.js          # Reusable Canvas like our setupCanvas function
└── ...                     # Anything else.. or maybe I guess this file

Getting Comfortable

I feel all documentations should have a ‘Getting Comfortable’ heading, which can be optional. For this, we build a small game. A block breaker game (pr breakout game) is recommended to be precise.

I followed along this MDN Tutorial here: https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript Would highly recommended. My version when I was done with this is under attempt2. I hope to work on it more.

Sidenote: By now maybe its time to address this page: https://developer.mozilla.org/en-US/docs/Web This page is bible. Call this an assumptions, call this openions, or call this toolstack. This page is bible.

Our Mascot: Langton’s Ant

Langton’s Ant

We will use Langtos’s Ant to be the mascot we use to understand, and learn about Canavs. (a Ant stuck in our own personal 2D universe)

If you go on the wiki, it is a rabbit hole, but one that you would be happy to indulge in. Interestingly, I bet you will find one or more links there that you might deeply connect to (like the ones that shape who you are :P) for reasons beyond the scope of this Article. For me it was Minecraft.

For those, who smartly refrained from entering that rabbit hole, ahh, I will try an summerize for our context.

We have a huge grid, our html canvas, the Ant’s universe. We define a starting possition for this “Ant”, and then we have a set of rules for how this ant behaves in this world. Each grid position are colored, either black or white.

Rules:

These simple rules lead to amazing behavior which we now will simulate on our universe, the HTML Canvas.

Simulation 1

Attempt3 - part of the repo

Let start by creating a canvas using our utils,

import { kCanvas } from "../utils/mycanvas.js";

var kC = new kCanvas("#myCanvas");

Next lets create a class for our Langtons ant to keep things formal.

class LangtonAntGrid {
    
    constructor() {
        
    }
    
    init(x = 20, y =20) {       
        this.grid = [];
        this.max_x = x;
        this.max_y = y;
        this.currentPosition = Object.assign({}, {
            x: this.max_x/2,
            y: this.max_y/2,
            color: 0,
            heading: 0
        });
        for (let i=0; i<x; i++) {
            this.grid[i] = [];
            for (let j=0; j<y; j++) {
                this.grid[i][j] = Object.assign({}, {
                    color: 0
                });
            }
        }
    }
    updateGrid() {
        const currentStatus = Object.assign({}, this.currentPosition);

        console.log(currentStatus);
        // first update current box
        this.currentPosition.color = (this.currentPosition.color + 1)%2;
        this.grid[this.currentPosition.x][this.currentPosition.y].color = this.currentPosition.color;
        this.drawPosition(this.currentPosition);
        

        //move to next Box
        if (currentStatus.color === 0) {
            this.currentPosition.heading = (this.currentPosition.heading + 1)%4;
            console.log('right');
        } else if (currentStatus.color === 1) {
            this.currentPosition.heading = (this.currentPosition.heading + 3)%4;
            console.log('left');
        }

        switch(this.currentPosition.heading) {
            case 0: this.currentPosition.y--;
                break;
            case 1: this.currentPosition.x++;
                break;
            case 2: this.currentPosition.y++;
                break;
            case 3: this.currentPosition.x--;
                break;
        }
        this.currentPosition.color = this.grid[this.currentPosition.x][this.currentPosition.y].color;
        console.log(this.currentPosition);
        
    }

    getLog() {
        console.log(this.grid);
    }
    drawPosition(position) {
        kC.drawBlock(position.x, position.y, position.color);    
    }
}

Now to run it, lets create an instance and call this using our requestAnimationFrame loop.


var antGrid = new LangtonAntGrid();
antGrid.init(500,500);

kC.drawGrid(500,500, false);


function draw() {

    antGrid.updateGrid();
    
    requestAnimationFrame(draw);
}

draw();

If you are following along, now would have been the time, when you really get to simplicity and universal nature of the Ant.

Try playing around with the rules the ant can have. It can have more than 2 rules, more than 2 colors, and if you really up for it, more than 2 dimentions. Updating the constructor() to introduce 2 properties and update the updateGrid() function to use these 2 properties, to make more rules possible.

class LangtonAntGrid {
    
    constructor() {
        this.numberOfStates = 4;
        this.stateTransitions = ['R', 'L', 'L ', 'R'];
    }
    ...
    ...
    updateGrid() {
        const currentStatus = Object.assign({}, this.currentPosition);

        this.currentPosition.color = (this.currentPosition.color + 1)%(this.numberOfStates);
        this.grid[this.currentPosition.x][this.currentPosition.y].color = this.currentPosition.color;
        this.drawPosition(this.currentPosition);
        

        //move to next Box
        if(this.stateTransitions[currentStatus.color] === 'L') {
        // if (currentStatus.color === 0) {
            this.currentPosition.heading = (this.currentPosition.heading + 1)%4;
            // console.log('right');
        } else if (this.stateTransitions[currentStatus.color] === 'R') {
            this.currentPosition.heading = (this.currentPosition.heading + 3)%4;
            // console.log('left');
        }

        switch(this.currentPosition.heading) {
            case 0: this.currentPosition.y--;
                break;
            case 1: this.currentPosition.x++;
                break;
            case 2: this.currentPosition.y++;
                break;
            case 3: this.currentPosition.x--;
                break;
        }
        this.currentPosition.color = this.grid[this.currentPosition.x][this.currentPosition.y].color;
        
    }
    ...
    ...

We will work on building more generic ‘Turmite’, but for now lets focus on our newly born Ant and more specifically on the backgound, our universe, the Canvas.

Enter: CanvasRenderingContext2D.globalCompositeOperation This can be used to determine the blending mode of colors. introduce this line anywhere after ctx variable is loaded

kC.ctx.globalCompositeOperation = '<your choice!>';

My favoirite: mutiply

We see progressive darkenings of the path more taken!

Note: I run a local server and then serve the js file. I use

http-server .\. --cors=*

Next WebGL

Discussion, Support and Issues

For general support and discussion of this project, please join the Discord server: Discord Invite Link

Discord Server

To check known bugs and see planned changes and features for this project, please see the GitHub issues. Found a bug we don’t already have an issue for? Please report it in a new GitHub issue with as much detail as you can!