Путь к пробуждению консоли, как насчет печати анимации?

JavaScript Canvas

введение

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

Кто-то встал и сказал: «Консоль, я дам тебе подработку».

Так было устроено на площади первой полосы, неподвижно под всеобщим вниманием, подняв знамя работодателя:

Положите баннер, и все снова станет скучным.

Ну почему бы и нет, позвольте мне помочь вам.


тестовое задание

«Консоль, дай мне пощупать твои кости».

«Ну, да, он поставляется с растровым геном».

Я коснулся своего безбородого подбородка и сказал.

console.log('%c你%c说%c什么%c?', 'background: #000; color: #fff','color: blue','color: red; border-bottom: 1px solid red','background: blue; color: #fff; border-radius: 50%;');

"Эй, есть также изменения стиля!"

«Это коллекция xx, которую я написал от руки два дня назад. Я думаю, что ваши кости очень странные. Вам не нужно 998, вам нужно только 9 юаней и 8, и вы можете бесплатно отправить ее домой!»

Консоль заколебалась после прослушивания, и мне было слишком поздно останавливаться.


Построить

【Внезапно серьезное лицо】

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

console.log, очевидно, удовлетворяет требованиям, необходимым для сборки изображения:

  • Позиция печати символа может соответствовать положению расположения пикселя.
  • Стиль CSS символа может соответствовать цветовому стилю пикселя.

И пусть содержимое этого изображения перемещается, вы можете включить «рендеринг», например, холст,每次“渲染”时先使用console.clear()清除掉上一次打印出的字符,然后计算场景中需移动的字符本次所在的位置,打印出字符到该位置,一定时间间隔后进行下一次“渲染”操作。

Для достижения вышеуказанных эффектов необходимо построить двухмерную картину мира глазами консоли.

Полное двухмерное изображение может состоять из нескольких подизображений, а именно элементов, таких как это сердце с решеткой 💗:

 ##   ##
#### ####
 #######
  #####
    #

Поместите его в сцену изображения, и у него есть свойство position в этой сцене.

При этом несколько элементов также можно поместить в группу (группу), группа помещается в сцену, элементы в группе будут вычислять положение относительно группы, то есть положение можно перемещать с помощью группу в целом, как показано на следующем рисунке:

scene与group均是图像容器,只有element是携带了子图像信息的实体。

Далее, чтобы напечатать все в сцене в своем собственном положении и стиле, нужен рендерер.renderer将场景中的element逐个取出,计算出其对应的绝对位置坐标后,将element包含的像素信息,一一映射到renderer所维护的二维数组canvas中;После того, как процесс окончен, полученный холст представляет собой всю информацию об изображении, содержащуюся в сцене, распечатывает ее на экране, и задача отображения этого изображения выполнена.

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


выполнить

Сначала возьмем имя:ConsoleCanvas

1. Реализуйте класс сценыScene

Sceneсебя как контейнер, в атрибутеelementsСохраняет элементы, содержащиеся в сцене,addМетод используется для добавления элементов в сцену.Если добавление является комбинацией, элементы в комбинации будут извлечены и помещены в сцену.

window.ConsoleCanvas = new function() {
    // 场景
    this.Scene = function(name = '', style) {
        // 场景元素集合
        this.elements = [];
        // 场景样式
        this.style = Object.prototype.toString.call(style) === '[object Array]' ? style : [];
        // 场景名称
        this.name = name.toString();
    };
    // 场景添加元素或组合
    this.Scene.prototype.add = function(ele) {
        if (!ele) {
            return;
        }
        ele.belong = this;
        // 添加的元素是组合元素
        if (ele.isGroup) {
            // 提出组合里的元素归入场景
            this.elements.push(...ele.elements);
            return;
        }
        this.elements.push(ele);
    };
    
    /* 后续代码块均承接此处 */
    
}

2. Реализовать класс элементаElement

//vals:元素字符内容,style:元素样式,z_index:层叠优先级,position:位置
this.Element = function(vals = [[]], style = [], z_index = 1, position) {
    // 元素随机id
    this.id = Number(Math.random().toString().substr(3, 1) + Date.now()).toString(36);
    this.vals = vals;
    this.style = style;
    this.z_index = z_index;
    // 元素缩放值
    this.scale_x = 1;
    this.scale_y = 1;
    this.position = {
        x: position && position.x ? position.x : 0,
        y: position && position.y ? position.y : 0
    },
    // 元素所属的组合
    this.group = null;
    // 元素所属的场景
    this.belong = null;
};

в элементеvalsСвойство представляет собой двумерный массив, содержащий растровое изображение элемента. Например, предыдущая форма сердца будет храниться в vals в следующем виде:

this.vals = [
    [' ','#','#',' ',' ',' ','#','#'],
    ['#','#','#','#',' ','#','#','#','#'],
    [' ','#','#','#','#','#','#','#'],
    [' ',' ','#','#','#','#','#'],
    [' ',' ',' ',' ','#']
];

ДатьElementМетод добавления класса:

  • clone, скопируйте элемент, здесь просто копияvals,style,z_index,positionи другую информацию для создания новых элементов.
// 元素克隆
this.Element.prototype.clone = function() {
    return new this.constructor(JSON.parse(JSON.stringify(this.vals)), this.style.concat(), this.z_index, this.position);
};
  • remove, удаляет сам элемент со сцены:
// 元素删除
this.Element.prototype.remove = function() {
    // 获取元素所属场景
    let scene = this.group ? this.group.belong : this.belong;
    // 根据元素id从场景中查询到该元素index
    let index = scene.elements.findIndex((ele) => {
        return ele.id === this.id;
    });
    if (index >= 0) {
        // 从场景中去除该元素项
        scene.elements.splice(index, 1);
    }
};
  • width, получить или установить ширину наименьшей ограничивающей рамки элемента:
// 元素获取宽度或者设置宽度(裁剪宽度)
this.Element.prototype.width = function(width) {
    width = parseInt(width);
    if (width && width > 0) {
        // 设置宽度,只用于裁剪,拓宽无效
        for (let j = 0; j < this.vals.length; j++) {
            this.vals[j].splice(width);
        }
        return width;
    } else {
        // 获取宽度
        return Math.max.apply(null, this.vals.map((v) => {
            return v.length;
        }));
    }
};
  • height, получить или установить высоту наименьшей ограничивающей рамки элемента:
// 元素获取高度或者设置高度(裁剪高度)
this.Element.prototype.height = function(height) {
    height = parseInt(height);
    if (height && height > 0) {
        // 设置高度,只用于裁剪,拓高无效
        this.vals.splice(height);
        return height;
    } else {
        // 获取高度
        return this.vals.length;
    }
};
  • scaleX, каждый пиксель необходимо переместить в нижнее положение в соответствии со значением масштабирования.Во избежание искажения, которое может возникнуть при уменьшении и увеличении масштаба, исходная копия шаблона символа элемента скрывается и сохраняется, а каждое масштабирование работает по оригинальной схеме.
// 元素横坐标缩放
this.Element.prototype.scaleX = function(multiple, flag) {
    let i, j;
    let scaleY = this.scale_y;
    multiple = +multiple;
    if (this.valsCopy) {
        // 每次变换使用原始图案进行
        this.vals = JSON.parse(JSON.stringify(this.valsCopy));
    } else {
        // 首次使用时保存原图案副本
        this.valsCopy = JSON.parse(JSON.stringify(this.vals));
    }
    if (!flag) {
        // 使用原始图案重新缩放纵坐标(避免失真),flag用于避免循环嵌套
        this.scaleY(this.scale_y, true);
    }
    if (multiple < 1) {
        for (j = 0; j < this.vals.length; j++) {
            for (i = 0; i < this.vals[j].length; i++) {
                [this.vals[j][Math.ceil(i * multiple)], this.vals[j][i]] = [this.vals[j][i], ' '];
            }
        }
        // 裁去缩小后的多余部分
        for (j = 0; j < this.vals.length; j++) {
            this.vals[j].splice(Math.ceil(this.vals[j].length * multiple));
        }
        this.scale_x = multiple;
    } else if (multiple > 1) {
        for (j = 0; j < this.vals.length; j++) {
            for (i = this.vals[j].length - 1; i > 0; i--) {
                [this.vals[j][Math.ceil(i * multiple)], this.vals[j][i]] = [this.vals[j][i], ' '];
            }
        }
        // 填充放大后的未定义像素
        for (j = 0; j < this.vals.length; j++) {
            for (i = this.vals[j].length - 1; i > 0; i--) {
                if (this.vals[j][i] === undefined) {
                    this.vals[j][i] = ' ';
                }
            }
        }
        this.scale_x = multiple;
    } else {
        this.scale_x = 1;
        return;
    }
};
  • scaleY, принцип тот же, что и у scaleX, разница в том, что scaleX обходит мигрированные пиксели строка за строкой, а scaleY обходит мигрированные пиксели столбец за столбцом.
// 元素纵坐标缩放
this.Element.prototype.scaleY = function(multiple, flag) {
    let i, j;
    multiple = +multiple;
    if (this.valsCopy) {
        // 每次变换使用原始图案
        this.vals = JSON.parse(JSON.stringify(this.valsCopy));
    } else {
        // 首次使用时保存原图案副本
        this.valsCopy = JSON.parse(JSON.stringify(this.vals));
    }
    if (!flag) {
        // 使用原始图案重新缩放横坐标(避免失真),flag用于避免循环嵌套
        this.scaleX(this.scale_x, true);
    }
    let length = this.width();
    if (multiple < 1) {
        for (i = 0; i < length; i++) {
            for (j = 0; j < this.vals.length; j++) {
                [this.vals[Math.floor(j * multiple)][i], this.vals[j][i]] = [this.vals[j][i], ' '];
            }
        }
        // 裁去缩小后的多余部分
        this.vals.splice(Math.ceil(this.vals.length * multiple));
        for (j = 0; j < this.vals.length; j++) {
            for (i = 0; i < this.vals[j].length; i++) {
                if (this.vals[j][i] === undefined) {
                    this.vals[j].splice(i);
                    break;
                }
            }
        }
        this.scale_y = multiple;
    } else if (multiple > 1) {
        let colLength = this.vals.length;
        for (i = 0; i < length; i++) {
            for (j = colLength - 1; j >= 0; j--) {
                if (!this.vals[Math.floor(j * multiple)]) {
                    // 开辟新数组空间
                    this.vals[Math.floor(j * multiple)] = [];
                }
                [this.vals[Math.floor(j * multiple)][i], this.vals[j][i]] = [this.vals[j][i], ' '];
            }
        }
        // 填充放大后的未定义像素
        for (j = 0; j < this.vals.length; j++) {
            if (this.vals[j]) {
                for (i = 0; i < this.vals[j].length; i++) {
                    if (this.vals[j][i] === undefined) {
                        this.vals[j].splice(i);
                        break;
                    }
                }
            } else {
                this.vals[j] = [' '];
            }
        }
        this.scale_y = multiple;
    } else {
        this.scale_y = 1;
        return;
    }
};
  • scale, и одновременно масштабировать абсциссу и ординату элемента:
// 元素缩放
this.Element.prototype.scale = function(x, y) {
    this.scaleX(+x);
    this.scaleY(+y);
};

3. Реализуйте класс композицииGroup

// 元素组合
this.Group = function() {
    // 组合标志
    this.isGroup = true;
    // 存放的子元素
    this.elements = [];
    // 组合位置
    this.position = {
        x: 0,
        y: 0
    };
    // 组合层叠优先级
    this.z_index = 0;
};

дляGroupДобавить метод:

  • add, добавление элементов в композицию:
// 组合添加子元素
this.Group.prototype.add = function(ele) {
    if (ele) {
    // 以数组形式添加多个子元素
    if (Object.prototype.toString.call(ele) === '[object Array]') {
        ele.forEach((item) => {
            this.elements.push(item);
            item.group = this;
        });
        return;
    }
    // 添加单个子元素
    this.elements.push(ele);
    ele.group = this;
    }
};
  • remove, удалить всю комбинацию, то есть удалить все элементы, содержащиеся в комбинации:,
// 删除组合
this.Group.prototype.remove = function() {
    this.elements.forEach((ele) => {
        ele.remove();
    })
};

4. Реализуйте класс рендерераRenderer

// 渲染器
this.Renderer = function() {
   this.width = 10;
   this.height = 10;
   this.canvas = [];
};

дляRendererДобавить метод:

  • Pixel, который генерирует пиксели для рендеринга:
// 生成用于渲染的像素点
this.Renderer.prototype.Pixel = function() {
   // 字符值
   this.val = ' ';
   // 样式数组值
   this.style = [];
   // 层叠优先级
   this.z_index = 0;
};	
  • setSize, установите размер рендеринга, то есть откройте пространство холста двумерного массива в соответствии с размером.
// 设置渲染画布的尺寸
this.Renderer.prototype.setSize = function(width, height) {
    this.width = parseInt(width);
    this.height = parseInt(height);
    this.canvas = [];
    for (let j = 0; j < height; j++) {
        this.canvas.push(new Array(width));
            for (let i = 0; i < width; i++) {
                this.canvas[j][i] = new this.Pixel();
            }
    }
};
  • clear, чтобы очистить холст:
// 清除画布
// x:开始清除的横坐标,y:开始清除的纵坐标,width:清除宽度,height:清除长度
this.Renderer.prototype.clear = function(x = 0, y = 0, width, height) {
    width = parseInt(width ? width : this.width);
    height = parseInt(height ? height : this.height);
    for (let j = y; j < y + height && j < this.height; j++) {
        for (let i = x; i < x + width && i < this.width; i++) {
            this.canvas[j][i].val = ' ';
            this.canvas[j][i].style = [];
            this.canvas[j][i].z_index = 0;
        }
    }
    // console清屏
    console.clear();
};
  • print, печатайте содержимое символа на холсте построчно со стилем. В случае большого количества строк печать построчно вызовет заметное мерцание экрана. Почему я не могу распечатать их все сразу? Я пробовал.При использовании console.log со стилем %c перевод строки не может быть помещен в него, как ожидалось, как показано на следующем рисунке:

// 带样式打印字符,逐行打印呈现画布带样式的内容
// noBorder:不显示左右边框(默认显示)
this.Renderer.prototype.print = function(noBorder) {
    let row = '';
    let rowId = 0;
    let style = [];
    let borderRight = noBorder ? '' : 'border-left: 1px solid #ddd';
    let borderLeft = noBorder ? '' : 'border-right: 1px solid #ddd';
    for (let j = 0; j < this.canvas.length; j++) {
        row = noBorder ? '' : '%c ';
        // 每行的唯一id,避免console打印出同样的字符会堆叠显示
        rowId = '%c' + j;
        style = noBorder ? [] : [borderLeft];
        for (let i = 0; i < this.canvas[j].length; i++) {
            row += '%c' + this.canvas[j][i].val;
            style.push(this.canvas[j][i].style.join(';'));
        }
        style.push(`background: #fff; color: #fff;${borderRight}`);
        console.log(row + rowId, ...style);
    }
};
  • printNoStyle, чтобы оптимизировать очевидную ситуацию с экраном-заставкой ранее, чтобы обеспечить метод одноразовой печати нестилизованного содержимого символов.
// 不带样式打印字符,一次打印呈现画布不带样式的内容
// noBorder:不显示左右边框(默认显示)
this.Renderer.prototype.printNoStyle = function(noBorder) {
    let row = '';
    let rows = '';
    let border = noBorder ? '' : '|';
    for (let j = 0; j < this.canvas.length; j++) {
        row = border;
        for (let i = 0; i < this.canvas[j].length; i++) {
            row += this.canvas[j][i].val;
        }
        rows += row + border + '\n';
    }
    console.log(rows);
};
  • render, рассчитайте ситуацию с пикселями после сопоставления элементов сцены с холстом, а затем вызовите метод печати для рендеринга содержимого сцены и представления его на консоли.
// 画布渲染
// scene:用于渲染的场景,noStyle:不带样式(默认带样式),noBorder:不带左右边框(默认带边框)
this.Renderer.prototype.render = function(scene, noStyle, noBorder) {
    // 先清屏
    this.clear();
    // 逐个取出场景中的元素,计算位置后取值替换画布的对应的像素点
    scene.elements.forEach((ele, i) => {
        let style = ele.style.concat();
        let z_index = ele.z_index;
        let positionY = Math.floor(ele.position.y);
        let positionX = Math.floor(ele.position.x);
        if (ele.group) {
            // 从组合里的相对坐标转换为画布上的绝对坐标
            positionY += ele.group.position.y;
            positionX += ele.group.position.x;
            // 叠加上组合的层叠优先级
            z_index += ele.group.z_index;
        }
        for (let y = positionY; y < positionY + ele.vals.length; y++) {
            if (y >= 0 && y < this.height) {
                for (let x = positionX; x < positionX + ele.vals[y - positionY].length && x < this.width; x++) {
                    if (x >= 0 && x < this.width) {
                        // 层叠优先级大的元素会覆盖优先级小的元素
                        if (z_index >= this.canvas[y][x].z_index && ele.vals[y - positionY][x - positionX] && ele.vals[y - positionY][x - positionX].toString().trim() != '') {
                            this.canvas[y][x].val = ele.vals[y - positionY][x - positionX];
                            this.canvas[y][x].style = style.concat();
                            this.canvas[y][x].z_index = z_index;
                        }
                    }
                }
            }
        }
    });
    // 打印样式或无样式判断
    noStyle ? this.printNoStyle(noBorder) : this.print(noBorder);
}

играть на волне

С приведенной выше библиотекой классов намного проще написать консольную версию анимации пинбола:

// 弹球动画
class PinBall {
    constructor(width = 30, height = 10) {
    // 创建场景
    this.scene = new ConsoleCanvas.Scene();
    // 创建渲染器
    this.renderer = new ConsoleCanvas.Renderer();
    // 设置尺寸
    this.renderer.setSize(width, height);
    // 场景元素添加
    this.elementAdd();
    // 开始动画循环
    this.loop();
    }
    elementAdd() {
        // 创建小球元素
        this.ball = new ConsoleCanvas.Element([['●']], ['background: blue', 'color: blue', 'border-radius: 50%']);
        // 在上半区域随机小球起始坐标
        this.ball.position.x = Math.floor(Math.random() * this.renderer.width);
        this.ball.position.y = Math.floor(Math.random() * this.renderer.height / 2);
        this.scene.add(this.ball);
    }
    animation() {
        let gap = 1;
        this.ball.kx = this.ball.kx ? this.ball.kx : 1;
        this.ball.ky = this.ball.ky ? this.ball.ky : 1;
        let x = this.ball.position.x + this.ball.kx * gap;
        let y = this.ball.position.y + this.ball.ky * gap;
        // 触碰边界时回弹
        if (x > this.renderer.width - this.ball.vals[0].length || x < 0) {
            this.ball.kx = -1 * this.ball.kx;
        }
        if (y > this.renderer.height - this.ball.vals.length || y < 0) {
            this.ball.ky = -1 * this.ball.ky;
        }
        this.ball.position.x = this.ball.position.x + (this.ball.kx * gap);
        this.ball.position.y = this.ball.position.y + (this.ball.ky * gap);
    }
    loop() {
        this.renderer.render(this.scene, true);
        this.animation();
        setTimeout(() => {
            this.loop();
        }, 300);
    }
}
let pinBall = new PinBall(30, 10);

Эффект стилизованной версии пинбола следующий.Можно обнаружить, что при стилизованной построчной печати частота обновления действительно ошеломляет.Если временной интервал настроить немного меньше, это ослепит меня. глаза:

Тогда посмотрите дестилизованный вариант, если нестилизованный комбинезон распечатать за один раз, то он не будет таким мерцающим:

Или добавить взаимодействие с клавиатурой?

решитХанойская башняВизуализируется рекурсивный результат задачи.Следующее выполнение 3-х дисков (есть еще 4-й и 5-й диски, слишком долго ставиться):


расширять

Что❓Ты сказал, что есть еще вспышка? Сегодняшние звезды ✨ тоже немного кричащие?

Я вздохнул, повернулся и погладил консоль по голове: "Пожалуйста, верни мне том..."

С тех пор я модифицировалRendererКатегорияprintфункция, которая выводит визуализированный контент в структуру html:

// 输出像素字符到指定dom
// target:目标dom, noStyle: 不显示样式, noBorder: 不显示左右边框
this.Renderer.prototype.print = function(target, noStyle, noBorder) {
    let row = '';
    let style = [];
    let rows = '';
    let border = noBorder ? '' : '<span>|</span>';
    for (let j = 0; j < this.canvas.length; j++) {
        row = border;
        style = [];
        for (let i = 0; i < this.canvas[j].length; i++) {
            row += `<span style='${noStyle?"":this.canvas[j][i].style.join(";")}'>${this.canvas[j][i].val}</span>`;
        }
        rows += row + border + '</br>';
    }
    if (target) {
        target.innerHTML = rows;
    }
};

Модифицированная версия называетсяPixelCanvas.

Тогда и стиль можно легко привести:

Ускорить скорость рендеринга и подняться на пятый этаж на одном дыхании тоже не составляет труда~


Последняя глава

«Кажется, будто я вернулся в эпоху красно-белых машин…»

Я был пристрастием разговаривать сам с собой, но когда я поднял глаза, я увидел непредубежденную улыбку на лице консоли.

实现代码及动画实例代码的地址见下:

https://github.com/youngdro/ConsoleCanvas

Перевод в один клик【В поисках звездного лица (*゚ー゚)v】

Превью онлайн-демонстрации Hanoi Tower

如果要问我为什么想到要写这个东西,以及它能有什么大用途,我怕是会无语凝噎。

生活处处有乐趣,代码亦同。

诚挚希望各位看官能从中找到属于自己的小乐趣~

·

·

·

想看我更多的段子帖,可移步以下地址:

узел гусеничный фонд, самостоятельный и самостоятельный, чтобы понять?

Оригинальный буддийский алгоритм красного конверта, понимаете?