[Холст такой забавный] Начиная с Матрицы

JavaScript

предисловие

Автор некоторое время назад изучал технические знания, связанные с Canvas.Ссылаясь на некоторые информационные статьи в Интернете, я научился использовать простые математические и физические знания для достижения некоторых интересных анимационных эффектов.Недавно я просто пролистал предыдущий код. , так что на этот раз эти практики кода реорганизованы и собраны в документ. Самостоятельно консолидируя и просматривая, вы можете общаться и учиться со всеми. так как [Холст это весело] В первой статье цикла автор все же начинает с самой классической Матрицы, и шаг за шагом выполняя кодовую практику, я проведу вас в волшебный мир анимации Canvas.

Код загружен наGithub, вы можете вытащить его и запустить напрямую, сохранив следующие приготовления.

визуализация

Готов к работе

Поскольку предыдущий код относительно старый, на этот раз я планирую использоватьReactВосстановить его снова или использовать более часто используемый в настоящее времяcreate-react-appScaffold для сборки проекта, найдите соответствующий путь к проекту локально, а затем выполните команду инициализации проекта:

npm install -g create-react-app  
create-react-app react-canvas

Учитывая, что на более позднем этапе может быть ряд анимационных эффектов, ради красивого интерфейса и удобного управления здесь используется React прямо и просто.Ant DesignДля управления меню анимации для легкого переключения на разные анимации используйтеreact-router-domдля управления маршрутизацией, используяloadableЧтобы реализовать загрузку маршрутов по требованию:

npm install --save antd react-router-dom @loadable/component

// 以下依赖遵循antd官网的高级配置,使用babel-plugin-import实现组件代码和样式的按需加载
npm install --save-dev react-app-rewired customize-cra babel-plugin-import

Модификация после завершения установкиpackage.jsonдокумент:

/* package.json */
"scripts": {
-   "start": "react-scripts start",
+   "start": "react-app-rewired start",
-   "build": "react-scripts build",
+   "build": "react-app-rewired build",
-   "test": "react-scripts test",
+   "test": "react-app-rewired test",
-   "eject": "react-scripts eject",
+   "eject": "react-app-rewired eject",
}

Затем создайтеconfig-overrides.jsДля изменения конфигурации по умолчанию:

+ const { override, fixBabelImports } = require('customize-cra');

+ module.exports = override(
+   fixBabelImports('import', {
+     libraryName: 'antd',
+     libraryDirectory: 'es',
+     style: 'css',
+   }),
+ );

На данный момент структура каталогов проекта выглядит следующим образом:

├── node_modules
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   └── serviceWorker.js
├── .gitignore
├── config-overrides.js
├── package.json
├── package-lock.json
└── README.md

srcВ каталоге есть какие-то файлы, которые не очень нужны в текущем проекте, их можно удалить, а потомsrcСоздано в каталогеrouterКаталог используется для хранения маршрутов проекта,viewsКаталог используется для хранения страниц по разным маршрутам через antd.LayoutКомпоненты для достижения макета страницы, модифицированный код выглядит следующим образом:

// src -> router -> index.js
import loadable from '@loadable/component';

const routes = [
  {
        path: '/hacker',
        name: '黑客帝国',
        component: loadable(() => import(/* webpackChunkName: 'hacker' */ '../views/Hacker')),
    }
];

export default routes;
// src -> views -> Hacker.js
function Hacker() {

    const canvasRef = useRef(null);

    return (
        <canvas ref={canvasRef} style={{background: '#000'}}/>
    );
}

export default Hacker;
// src -> App.js
import React, {useState} from 'react';
import {Redirect, Route, NavLink, Switch, withRouter} from 'react-router-dom';
import {Layout, Menu, Icon} from 'antd';
import routes from './router';
import './App.css';

const {Header, Sider, Content} = Layout;

function App({location}) {
    const [collapsed, setCollapsed] = useState(false);
    const toggle = () => setCollapsed(!collapsed);
    return (
        <Layout>
            <Sider trigger={null} collapsible collapsed={collapsed}>
                <div className="title">Canvas真好玩</div>
                <Menu theme="dark" mode="inline"
                      defaultSelectedKeys={[location.pathname.length === 1 ? routes[0].path : location.pathname]}>
                    {
                        routes.map(route =>
                            <Menu.Item
                                key={route.path}>
                                <NavLink
                                    to={route.path}
                                    style={{color: 'rgba(255,255,255,.65)'}}
                                    activeStyle={{color: '#fff'}}
                                >
                                    {route.name}
                                </NavLink>
                            </Menu.Item>)
                    }
                </Menu>
            </Sider>
            <Layout>
                <Header style={{background: '#fff', padding: 0}}>
                    <Icon
                        className="trigger"
                        type={collapsed ? 'menu-unfold' : 'menu-fold'}
                        onClick={toggle}
                    />
                </Header>
                <Content
                    style={{
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                        margin: '24px 16px',
                        padding: 24,
                        background: '#fff',
                        minHeight: 280,
                    }}
                >
                    <Switch>
                        {
                            routes.map((route, i) =>
                                <Route
                                    path={route.path}
                                    exact={route.exact}
                                    render={props => <route.component {...props} router={route.routes}/>}
                                    key={i}
                                />
                            )
                        }
                        <Redirect from="/" to="/hacker" exact={true}/>
                    </Switch>
                </Content>
            </Layout>
        </Layout>
    );
}

export default withRouter(App);
// src -> index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router} from 'react-router-dom';
import './index.css';
import App from './App';

ReactDOM.render(
    <Router>
        <App/>
    </Router>,
    document.getElementById('root'));
// src -> App.css
#root {
    height: 100%;
}

.ant-layout {
    height: 100%;
}

.title {
    padding: 16px 0;
    text-align: center;
    color: #fff;
    font-size: 24px;
    background-color: rgba(0, 0, 0, .2);
}

.trigger {
    font-size: 18px;
    line-height: 64px;
    padding: 0 24px;
    cursor: pointer;
    transition: color 0.3s;
}

.trigger:hover {
    color: #1890ff;
}

.logo {
    height: 32px;
    background: rgba(255, 255, 255, 0.2);
    margin: 16px;
}

До сих пор была написана основная структура кода нашего проекта. Вот эффект страницы, который я завершен до сих пор:

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

выполнить

Прежде чем приступить к написанию кода, давайте проанализируем детали реализации Матрицы.В приведенном выше эффекте анимации мы можем знать, что анимация на самом делеЭффект смещения расстояния сверху вниз, достигаемый различными английскими буквами, цифрами и специальными символами., поэтому мы будем поддерживать в коде коллекцию для хранения всех возможных текстов. Во-вторых, мы видим, что эффект падения текста на самом деле разделен на несколько столбцов.Конечно, количество столбцов будет динамически рассчитываться в соответствии с шириной контейнера Canvas. Чтобы получить анимацию, мы можем использовать функцию браузера.requestAnimationFrameДля поддержания плавности 60 кадров в секунду я считаю, что большинству фронтенд-персонала этот Api не чужд, но вот следующие два момента:

  1. Чтобы продолжить обновление следующего кадра анимации перед следующей перерисовкой браузера, сама функция обратного вызова должна снова вызвать requestAnimationFrame().
  2. Чтобы повысить производительность и время работы от батареи, в большинстве браузеров, когда requestAnimationFrame() выполняется на фоновой вкладке или в скрытом iframe, вызовы requestAnimationFrame() приостанавливаются для повышения производительности и времени работы от батареи.

С помощью этого API анимации мы можем очистить текущее состояние контейнера Canvas в каждом кадре и в то же время вычислить новые координаты каждого текста и отрисовки, мы можем определить начальную переменную для смещения по оси Y каждого столбца текста как 1, то есть представляет собой размер единицы шрифта.Каждый раз, когда текст выпадает на размер шрифта, к этой начальной переменной прибавляем 1, чтобы при вычислении координат текста в следующий раз это значение можно было умножить на размер шрифта чтобы получить ось Y. Координаты, это визуально достигает эффекта падения текста. Здесь следует упомянуть, что,Система координат Canvas немного отличается от декартовой системы координат в области науки.Используется система координат окна по умолчанию, то есть координата начала координат находится в верхнем левом углу окна, а положительное значение вдоль направление оси X является положительным, а направление вниз по оси Y является положительным значением, при последующем расчете текстовых координат, здесь нужно обратить внимание на разницу.На самом деле в системе координат окна есть и отрицательные значения, но они убегают за пределы экрана, и мы их вообще не замечаем .
Декартова система координат:

Система координат окна:
Другие точки знаний и базовые API о Canvas не являются предметом этой серии. Заинтересованные студенты могут проверить соответствующую информацию в Интернете. В Canvas не так много API для рисования, и порог обучения не высок, поэтому его легко освоить. Основываясь на приведенном выше анализе, мы пытаемся улучшитьHacker.jsкод в:

function Hacker() {

    const canvasRef = useRef(null);

    useEffect(() => {

        // 获取当前的canvas元素
        const canvas = canvasRef.current;

        // 获取canvas上下文,2d表示建立一个二维渲染上下文,当然也有基于WebGL的三维渲染上下文,在本系列中暂不考虑
        const context = canvas.getContext('2d');

        // 临时保存canvas的宽高信息,问了简便固定800 x 600
        const w = canvas.width = 800;
        const h = canvas.height = 600;

        // 文字颜色
        const textColor = '#33ff33';

        // 保存所有可能出现的文字
        const words = "0123456789qwertyuiopasdfghjklzxcvbnm,./;'[]QWERTYUIOP{}ASDFGHJHJKL:ZXCVBBNM<>?";

        // 将文字拆分进一个数组
        const wordsArr = words.split('');

        // 这里假设每个文字的字体大小为16px
        const font_size = 16;

        // 根据字体大小动态计算文字列数
        const columns = w / font_size;

        // 根据上面的分析,我们创建一个数组保存每列中的文字当前在Y轴上偏移了几个字体单位
        const dropUnits = [];

        // 初始化dropUnits,默认值从1开始,而不是0,因为canvas的fillText方法默认是从文字的左下角开始绘制
        for (let i = 0; i < columns; i++) {
            dropUnits[i] = 1;
        }

        // 设置上下文的填充色和字体大小
        context.fillStyle = textColor;
        context.font = `${font_size}px arial`;

        function draw() {

            // 核心,
            // 这里开始循环每一列,
            // 为每一列创建随机文字,
            // 同时根据当前列已经下落了几个字体大小来设置文字坐标(坐标原点为canvas容器的左上角)
            for (let i = 0, len = dropUnits.length; i < len; i++) {
                const text = wordsArr[Math.floor(Math.random() * wordsArr.length)];
                const x = i * font_size;
                const y = dropUnits[i] * font_size;
                context.fillText(text, x, y);

                // 当文字已经超出高度边界的时候,需要重置当前列下落的字体单位
                if (y > h) {
                    dropUnits[i] = 0;
                }

                dropUnits[i]++;
            }
        }

        // 循环执行动画
        (function frame() {
            // 此处需要再次调用requestAnimationFrame,注意并不是同步递归
            window.requestAnimationFrame(frame);

            // 在绘制下一帧的文字之前需要清空当前状态下的所有文字,避免文字被覆盖
            context.clearRect(0, 0, w, h);
            draw();
        }());
    }, []);

    return (
        <canvas ref={canvasRef} style={{background: '#000'}}/>
    );
}

После добавления приведенного выше кода давайте посмотрим на текущий эффект:

Это не то, что мы ожидали. Давайте проанализируем причину проблемы. В приведенной выше реализации кодаdrawФункция используется для отрисовки текста.Если будет обнаружено, что текст в данный момент вышел за рамки контейнера, он будет сброшенdropUnitsЗначение в массиве равно 0, тогда следствие,dropUnitsКаждый элемент в массиве равен 0, поэтому начальные координаты оси Y каждого столбца текста всегда одинаковы, что приводит к описанному выше эффекту. Итак, нам просто нужно найти способ сместить начальные координаты оси Y, тогда будет достигнут ожидаемый эффект.Конечно, это смещение также является случайным, поэтому легко придумать использованиеMath.randomМетод добавляет оценку случайных чисел, для достижения которой мы вносим небольшие изменения в приведенный выше код:

- if (y > h) {
+ if (y > h && Math.random() > 0.98) { // 此处增加随机数判断,只有满足条件后才进行重置
    dropUnits[i] = 0;
}

Я нарисовал простую картинку, чтобы помочь понять процесс.Два квадрата на картинке представляют два слова, а логическое значение представляет результат условия if в приведенном выше коде:

На приведенном выше рисунке хорошо видно, что после добавления случайного числа координаты текста по оси Y меняются. Измененный эффект выглядит следующим образом:
Мы приближаемся к желаемому эффекту, но эффект выглядит немного неуклюжим, потому что мы используем Canvas.clearRectМетод очищает холст Canvas, поэтому текст моментально появится в следующей координатной точке, формируя этот мерцающий эффект, похожий на светофоры на дороге.Перед переключением цветов предыдущие цвета очищаются, а затем переключаются моментально. Здесь мы меняем образ мышления, мы не используемclearRectметод очистки холста, но в каждом кадре используйтеfillRectМетод заполняет холст светлым цветом фона для достижения эффекта градиента Давайте немного изменим код:

// 文字颜色
const textColor = '#33ff33';
+ // 填充背景色
+ const bgColor = 'rgba(0, 0, 0, .1)';

- // 设置上下文的填充色和字体大小
- context.fillStyle = textColor;
- context.font = font_size + 'px arial';
function draw() {
    // 将上述两行代码放到此函数中,因为这里需要重新设置fillStyle
    + context.fillStyle = textColor;
    + context.font = font_size + 'px arial';
}

// 循环执行动画
(function frame() {
    ...
    - // 在绘制下一帧的文字之前需要清空当前状态下的所有文字,避免文字被覆盖
    - context.clearRect(0, 0, w, h);
    
    + // 在绘制下一帧的文字之前给画布填充背景色
    + context.fillStyle = bgColor;
    + context.fillRect(0, 0, w, h);
    ...
}());

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

Суммировать

Эта статья в основном предназначена для того, чтобы поделиться с вами использованием Canvas для достижения классного эффекта Матрицы. Конечно, это только начало этой серии. В дальнейшем мы будем сочетать простые математические и физические знания, чтобы достичь большего. интересные анимационные эффекты.Обсуждайте и учитесь друг у друга.

общаться с

Сегодня я поделюсь ею здесь.Если вам интересна анимация Canvas, вы можете подписаться на нашу официальную учетную запись, чтобы общаться и учиться вместе.

Статья обновлена ​​доБлог на гитхабеЕсли статью все еще может чувствовать, не стесняйтесь!

Один из ваших лайков заслуживает моих дополнительных усилий!

Вырастая перед лицом невзгод, только постоянно учась, вы можете стать лучшей версией себя и поощрять себя!