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
Физика ткани, которую можно подергать и даже порвать

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

Комментарии (12)


  1. hahenty
    18.05.2019 20:20
    +2

    Где Doom?


  1. androidovshchik
    18.05.2019 20:39
    +4

    Все бы неплохо, но это жуткое мерцание и Console was cleared (да, я его вижу)


  1. SadOcean
    18.05.2019 21:16

    Занятно, но так себе холст. Мерцание очень сильное.
    Сами по себе базовые программистские задачи (ткань, змейка, 3д) гораздо интереснее.
    Но почему бы и нет, нормальное экзотическое программирование.


  1. AngReload
    19.05.2019 07:28

    А в Firefox консоль не мерцает, однако.
    В raycasting текст поехал, символ ? отсутствует в monospace-шрифте. Я поменял его на -, а пол на - для контраста.
    Обычно в шрифтах есть только такие блочные символы — ---¦¦---


  1. Enmar
    19.05.2019 08:23

    Идея интересная.
    Жаль, что скорее всего никак не решить проблему Console was clear.
    Возможно, стоит попробовать просто скрывать ненужную информацию.
    В смысле вывести кучу переносов строки, старая картинка пропадет и ее перестать обновлять.
    А дальше нарисовать новую и обновлять ее.


    1. AngReload
      19.05.2019 09:27

      Вы предлагаете не очищать консоль, но проблема тут в том, что при накоплении сообщений консоль начнёт дико тормозить.
      Можете посмотреть это введя в консоль console.clear = () => {} на каких-нибудь динамичных демках, вроде симуляции ткани или в змейке (если успеете при этом не погибнуть).


      1. x67
        19.05.2019 12:08

        У нее наверняка есть какой то буффер, по крайней мере в случае адекватных разработчиков. И в хроме так и есть, скорее всего. Частенько захожу на нонейм сайты и там сообщения сыпятся без остановки.


        1. AngReload
          19.05.2019 13:11

          Ну, как я и говорил, попробуйте в Хроме сыграть в змейку без console.clear. Частота обновления консоли неиллюзорно падает, а реакция на нажатия клавиш управления запаздывает на секунды.


  1. assembled
    20.05.2019 07:01

    Для браузера лучше заменить вывод в консоль на вывод на страничку, можно будет даже цвета добавить. А вот для Node.js может сгодится, можно какую-нибудь игруху с простой графикой написать и играть потом через telnet, как когда-то на BBS-ках :)


    1. androidovshchik
      20.05.2019 13:53

      Тоже думаю, системная консоль более отзывчивая


    1. RealPeha Автор
      20.05.2019 16:40

      Вывод на страницу это уже совсем другое, для этого есть канвас и это уже не так интересно)
      А насчет Node.js то да, там консоль быстро перерисовывается, можно вообще что угодно сделать


  1. Shoom3301
    20.05.2019 07:01

    codepen.io/anon/pen/MQEJZV?editors=1111
    Больше года назад, в tinkoff делал конкурс (для статьи на N+1) и одно из заданий было нарисовать что-нибудь в консоли или сделать интерактивную игру.
    Даже пример остался: codepen.io/anon/pen/MQEJZV?editors=1111
    В то время почему то не зашло.