We play in the browser console

image
How beautiful this world is,

console.log () is a good method to output debugging information to the web console. You can display numbers, strings, arrays, objects, functions, plain text, and besides, you can add formatting, color, background, and quite a few other styles to all of this ... Is that all? Is that all this method alone can do? Well ... What about the implementation in the console of a simple platformer, Ray casting algorithm, or tissue physics?

For those who came here just to watch, I will leave a link to the demo at the very beginning:

GitHub: GitHub
Live example: Demo

Open the page, press F12, stretch the console wider and go to the demo that interests you. And of course you need to focus on the page in order to be able to control the image in the console.
It is advisable to run in chrome, but there may be a case where the characters used to display the picture may not be supported and displayed as squares. As an option, download the code for yourself and change the output characters to others.

And now a little more about this

Console as a canvas


Let's look at the console.log () method and the console as a whole, not as a debugging tool, but as a canvas. Yes, as a place where we can draw a little and even make it move. In addition, no one canceled Unicode characters.

I implemented methods for "drawing" in the console, like methods for working with canvas. But compared to a real canvas, the output to the console and, moreover, its redrawing imposes great limitations, which unfortunately cannot be circumvented (at least I think so). About them in order.

Pixel size


When drawing on canvas, we are dealing with pixels, which has a size, attention, pixel on your monitor! When displayed in the console, the “pixel” takes on a slightly different concept. Yes, with respect to the console, this is its peculiar pixel, but with respect to the real pixel, is it just a special character, for example such? But there is a slight restriction on characters, or rather a recommendation: it should have a height equal to the height of the line break in the console. But this is only if we want to get a beautiful picture (as much as possible).

Redrawing


This is the main problem since The console is not designed to update data frequently. We bring them there, deduce and deduce. There is console.clear () which clears it, but I think it is used very rarely. Oh yes, but not in my case, where everything is based on the fact that you need to constantly clean it and display the text again. Here is just one console.clear () causes its complete overload, which is accompanied by a millisecond flashing. And if you need to redraw it constantly with some frequency, then people with hypersensitivity are better off not looking at it. But unfortunately nothing can be done about it.

Color


As I wrote at the beginning, formatting can be applied to the output, but in my case I decided not to be content with a black and white image, which can be achieved thanks to a wide selection of Unicode characters.

More detailed limitations can be seen on live examples, the links to which I left at the end of the article. You can already familiarize yourself with them, but for now I will describe the drawing process itself. All this I designed in a small library with which at the end I implement simple games and the Raycasting algorithm

We draw in the console


I adopted the names of the methods, as well as the drawing process from the canvas (later I will describe why) and this is what ended up

engine.js
const canvas = {
    width: 70,
    height: 40,
    getContext(type) {
        if (type != '2d') {
            return console.log('Only 2d');
        }
        return new Context2D(type);
    }
}
class Context2D {
    constructor(type) {
        this.fillStyle = '?';
        this.emptyStyle = '?';
        this.map = [];
        for (let i = 0; i < canvas.height; i++) {
            this.map[i] = [];
            for (let j = 0; j < canvas.width; j++) {
                this.map[i][j] = this.emptyStyle;
            }
        }
        this.path = [];
        this.clear();
    }
    fillRect(x, y, width, height) {
        for (let i = y; i < y + height; i++) {
            for (let j = x; j < x + width; j++) {
                if (!this.map[i]) break;
                this.map[i][j] = this.fillStyle;
            }
        }
        this.draw();
    }
    strokeRect(x, y, width, height) {
        for (let j = x; j < x + width; j++) {
            this.map[y][j] = this.fillStyle;
            this.map[y + height - 1][j] = this.fillStyle;
        }
        for (let i = y + 1; i < y + height - 1; i++) {
            this.map[i][x] = this.fillStyle;
            this.map[i][x + width - 1] = this.fillStyle;
        }
        this.draw();
    }
    clearRect(x, y, width, height) {
        for (let i = y; i < y + height; i++) {
            for (let j = x; j < x + width; j++) {
                this.map[i][j] = this.emptyStyle;
            }
        }
        this.draw();
    }
    beginPath() {
        this.path = [];
    }
    moveTo(x, y) {
        this.path.push([Math.round(x), Math.round(y), true]);
    }
    lineTo(x, y) {
        this.path.push([Math.round(x), Math.round(y)]);
    }
    closePath() {
        if (!this.path.length) return false
        this.path.push([this.path[0][0], this.path[0][1]]);
    }
    stroke() {
        const path = this.path;
        for (let i = 0; i < path.length - 1; i++) {
            const x0 = path[i][0];
            const y0 = path[i][1];
            const x1 = path[i+1][0];
            const y1 = path[i+1][1];
            this.fillPixel(x1, y1);
            if (path[i+1][2]) continue;
            const deltaX = Math.abs(x1 - x0);
            const deltaY = Math.abs(y1 - y0);
            const signX = x0 < x1 ? 1 : -1;
            const signY = y0 < y1 ? 1 : -1;
            let error = deltaX - deltaY;
            let x = x0;
            let y = y0;
            while(x !== x1 || y !== y1) {
                this.fillPixel(x, y)
                const error2 = error * 2;
                if (error2 > -deltaY) {
                    error -= deltaY;
                    x += signX;
                }
                if (error2 < deltaX) {
                    error += deltaX;
                    y += signY;
                }
            }
        }
        this.draw();
    }
    fillPixel(x, y) {
        if (!this.map[y]) return false;
        this.map[y][x] = this.fillStyle;
    }
    arc(x1, y1, r) {
        let x = 0;
        let y = r;
        let delta = 1 - 2 * r;
        let error = 0;
        while (y >= 0) {
            this.moveTo(x1 + x, y1 + y);
            this.moveTo(x1 + x, y1 - y);
            this.moveTo(x1 - x, y1 + y);
            this.moveTo(x1 - x, y1 - y);
            error = 2 * (delta + y) - 1;
            if (delta < 0 && error <= 0) {
                delta += 2 * ++x + 1;
                continue;
            }
            if (delta > 0 && error > 0) {
                delta -= 2 * --y + 1;
                continue;
            }
            delta += 2 * (++x - y--);
        }
        this.draw()
    }
    draw() {
        this.clear();
        //2D to String
        const map = this.map.map(val => val.join('')).join('\n');
        console.log(map);
    }
    clear() {
        console.clear();
    }
}


Now we include this file in the html file, open the console and can try several methods

canvas.width = 70
canvas.height = 30
const ctx = canvas.getContext('2d')
ctx.beginPath()
ctx.moveTo(30, 5)
ctx.lineTo(30, 25)
ctx.moveTo(30, 15)
ctx.lineTo(35, 13)
ctx.lineTo(38, 13)
ctx.lineTo(40, 16)
ctx.lineTo(40, 25)
ctx.stroke()

Here is the result.

image

At the output, the image I intended is obtained, everything is drawn by coordinates and by analogy with canvas.

Cool examples


My plans were to make it possible to transfer a regular canvas game to a game in console as simple as possible. For this, I implemented the same methods with minimal changes. What does it mean? And the fact that for the implementation of any game I just take ready-made on canvas, I correct a few lines of code and it starts in console!

Actually this is what I did. And the first thing that occurred to me (except for the small square that can be moved around the console) was to implement the Raycasting
algorithm. I did not write the algorithm itself, but simply lent it here and changed several lines and launched it in the console.

image

It looks impressive, doesn't it?

Here are some more screenshots from what I transferred to the console.

image
Snake

image
Physics of tissue that can be pulled and even torn

Again, the implementation of this snake and tissue physics is not mine, I just adapted it for the console. In the source files, I left links to the original sources.

Also popular now: