Играем в консоли браузера

в 15:41, , рубрики: 3d, canvas, console.log, javascript, raycasting, ненормальное программирование

image
Как прекрасен этот мир

сonsole.log() — хороший метод, чтобы вывести отладочную информацию в веб-консоль. Можно выводить числа, строки, массивы, объекты, функции, обычный текст, к тому же, ко всему этому можно добавлять форматирование, цвет, фон и вообще довольно много других стилей… И это всё? Это всё, что может один этот метод? Ну… А как насчет реализации в консоли простого платформера, алгоритма Ray casting или физики ткани?

Для тех, кто зашел сюда просто чтобы посмотреть я оставлю ссылку на демо в самом начале:

GitHub: GitHub
Живой пример: Демо

Открываете страницу, нажимаете F12, растягиваете консоль пошире и переходите на интересующую Вас демку. И конечно же нужно сфокусироваться на странице, чтобы иметь возможность управлять изображением в консоли.
Желательно запускать в хроме, но может быть случай, когда символы используемые для вывода картинки могут не поддерживаться и отображаться в виде квадратиков. Как вариант — скачать себе код и сменить символы вывода на другие.

А теперь чуть-чуть подробнее об этом

Консоль как холст

Давайте посмотрим на метод console.log() и на консоль в целом не как на средство отладки, а как на холст. Да, как на место, где мы сможем немного «порисовать» и даже заставить это двигаться. К тому же Unicode-символы никто не отменял.

Я реализовал методы для «рисования» в консоли подобно методам для работы с canvas. Но по сравнению с настоящим холстом вывод в консоль и более того её перерисовка накладывает большие ограничения, которые к сожалению никак не обойти (по крайней мере я так думаю). Про них по порядку.

Размер пикселя

При рисовании на canvas мы имеем дело с пикселями, который имеет размер, внимание, пикселя на вашем мониторе! При отображении в консоли «пиксель» приобретает немного иное понятие. Да, относительно консоли это её своеобразный пиксель, но относительно настоящего пикселя это всего лишь специальный символ, к примеру такой ?. Но и на символы есть небольшое ограничение, а точнее рекомендация: он должен иметь высоту равную высоте переноса строки в консоли. Но это только если мы хотим получить красивую картинку (на сколько это вообще возможно).

Перерисовка

Это является основной проблемой т.к. консоль не создана для того, чтобы на ней часто обновлять данные. Мы их туда выводим, выводим и выводим. Есть console.clear() который её очищает, но я так думаю используется он крайне редко. Ах да, только не в моем случае, где всё построено на том, что необходимо постоянно её очищать и снова выводить текст. Вот только лишь один console.clear() вызывает полную её перегрузку, которое сопровождается миллисекундным миганием. А если необходимо перерисовывать её постоянно с какой-то частотой, то людям с повышенной чувствительностью лучше не смотреть на это. Но к сожалению с этим ничего не поделать.

Цвет

Как я писал в начале к выводу можно применять форматирование, но в своём случае я решил всё не и довольствоваться черно-белым изображение, добиться которого можно благодаря широкому выбору Unicode-символов.

Более детально ограничения можно будет увидеть на живых примерах, ссылки на которые я оставил в конце статьи. Вы можете уже сейчас ознакомиться с ними, а я пока опишу сам процесс рисования. Всё это я оформил в небольшой библиотеке с помощью которой в конце я реализую простые игры и алгоритм Raycasting

Рисуем в консоли

Названия методов, а также процесс рисования я перенял из canvas (позже опишу зачем) и вот что в итоге получилось

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();
    }
}

Теперь подключаем этот файл в html файл, открываем консоль и можем испытать несколько методов

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()

Вот результат

image

На выходе получается задуманное мной изображение, всё рисуется по координатам и по аналогии с canvas.

Примеры покруче

В моих планах было сделать возможность максимально простого переноса обычной игры на canvas в игру в console. Именно для этого я реализовал такие же методы с минимальными изменениями. Что это значит? А то, что для реализации какой-либо игры я просто беру уже готовое на canvas, правлю несколько строчек кода и оно запуститься в console!

Собственно именно этим я и занялся. И первое, что мне пришло в голову (кроме квадратика, который можно двигать по консоли), так это реализовать алгоритм Raycasting
Сам алгоритм я не писал, а просто одолжил его тут и изменив несколько строчек запустил в консоли.

image

Выглядит впечатляюще, не правда ли?

Вот еще несколько скриншотов из того, что я перенес в консоль.

image
Змейка

image
Физика ткани, которую можно подергать и даже порвать

Опять же, реализация этой змейки и физики ткани не моя, я только адаптировал это под консоль. В исходных файлах я оставил ссылки на оригинальные источники.

Автор: RealPeha

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js