1. 程式人生 > >【Canvas真好玩】從黑客帝國開始

【Canvas真好玩】從黑客帝國開始

前言

筆者之前有一段時間一直在學習Canvas相關的技術知識點,通過參考網上的一些資料文章,學著利用簡單的數學和物理知識點實現了一些比較有趣的動畫效果,最近剛好翻看到以前的程式碼,所以這次將這些程式碼實踐重新梳理一遍後整理成文,自己鞏固複習的同時,可以和大家一起交流學習。作為【Canvas真好玩】系列的第一篇文章,筆者還是從最經典的黑客帝國開始,在一步一步進行程式碼具體實踐的同時,帶領大家進入神奇的Canvas動畫的世界。

程式碼已上傳至Github,可以拉下來後直接執行,省掉下面的準備工作環節。

效果圖

準備工作

因為之前的程式碼比較久遠,這次打算使用React來重構一遍,還是使用目前使用頻率比較高的create-react-app

腳手架來搭建專案,在本地找到合適的專案路徑,然後執行專案初始化命令:

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軸方向向下為正值,在後續計算文字座標的時候需要注意這裡的區別,其實視窗座標系統中也是有負值的,只是跑到了螢幕之外,我們一般沒有注意到而已。
笛卡爾座標系:

視窗座標系:

關於Canvas其他的知識點和基礎API不是本系列的重點,感興趣的同學可以自行網上查閱下相關資料,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的動畫比較感興趣,可以關注咱們的公眾號,一起交流學習。

文章已同步更新至Github部落格,若覺文章尚可,歡迎前往star!

你的一個點贊,值得讓我付出更多的努力!

逆境中成長,只有不斷地學習,才能成為更好的自己,與君共勉!

相關推薦

Canvas好玩黑客帝國開始

前言 筆者之前有一段時間一直在學習Canvas相關的技術知識點,通過參考網上的一些資料文章,學著利用簡單的數學和物理知識點實現了一些比較有趣的動畫效果,最近剛好翻看到以前的程式碼,所以這次將這些程式碼實踐重新梳理一遍後整理成文,自己鞏固複習的同時,可以和大家一起交流學習。作為【Canvas真好玩】系列的第一篇

Leetcode周賽contest-81開始。(一般是10個contest寫一篇文章)

Contest 81 (2018年11月8日,週四,凌晨) 連結:https://leetcode.com/contest/weekly-contest-81 比賽情況記錄:結果:3/4, ranking: 440/2797。這次題目似乎比較簡單,因為我比賽的時候前三題全做出來了(1:12:39),然後第

Leetcode周賽contest-41開始。(一般是10個contest寫一篇文章)

Contest 41 ()(題號) Contest 42 ()(題號) Contest 43 ()(題號) Contest 44 (2018年12月6日,週四上午)(題號) 連結:https://leetcode.com/contest/leetcode-weekly-contest-44 比賽情況

原創黑客帝國》劇情詳細解析(未完)

本文是對整個《黑客帝國》劇情的完整分析和介紹,希望廣大黑客帝國迷能夠喜歡。由於本人能力有限加上黑客帝國的情節過於複雜,文中分析如有錯誤的地方歡迎大家指出,如有不完整的地方也希望大家補充。本文參考以下資料:《黑客帝國》動畫版(Animatrix),《黑客帝國》1、2、3一、二次

SSH學習筆記配置Struts1環境到簡單實例

swa void tro 介紹 -s exceptio art error con 以下我將從一個簡單點的計算器實例,介紹struts1的環境配置,以及其重要的兩個核心類:ActionForm和Action 簡單計算器實現思路: 1.提供一個輸入界面,

開源分享:入門到精通ASP.NET MVC+EF6+Bootstrap這裏開始,一起搭框架(1)開篇介紹

strong src 擁有 ckeditor 開發 技術分享 mdi 控制 https 框架簡介 這幾年一直在做ASP.NET開發,幾年前做項目都是老老實實一行行的寫代碼,後來發現那些高手基本都會有自己積累起來的代碼庫,現在稱之為開發框架,基礎代碼不用再去堆,

許曉笛開始運行EOS系統

EOS 系統 nodeos keos 復習一下上次文章的內容,EOS 系統主要有三個應用程序:nodeos: EOS 系統的核心進程,也就是所謂的“節點”。cleos:本地的命令行工具,通過命令行與真人用戶交互,並與節點(nodeos)和錢包(keosd)通信。是用戶或者開發者與節點進程交互的橋梁

ASP.NET Core向 Web API 提交純文本內容談起

文本 .text prot 實例 out 示例 問題 img anr 前些時日,老周在升級“華南閑腎回收登記平臺”時,為了擴展業務,尤其是允許其他開發人員在其他平臺向本系統提交有關腎的介紹資料,於是就為該系統增加了幾個 Web API。 其中,有關

mysql學習二——架構到基本配置講解

前言 雖然不是DBA,但是瞭解mysql的一些基本知識對於我們提高自身水平和提高書寫sql語句效能有幫助! 內容 1.配置檔案: 檔名稱 作用 二進位制日誌log-bin 用於主

網際網路那些事——3Q大戰到3B大戰

          相報何時了,網際網路大戰從網際網路出現幾乎就一天沒消停過,國內的網際網路巨頭們為了搶佔各個行業的市場,都不得不使出自己的看家本領。奇虎360更是拿出不贏官司不罷休的決心,跟這家打完跟那家打,但至今還

React 實戰教程0到1 構建 github star管理工具

前言 在日常使用github中,除了利用git進行專案版本控制之外,最多的用處就是遊覽各式的專案,在看到一些有趣或者有用的專案之後,我們通常就會順手star,目的是日後再看。但是當我們star了許多專案之後,回過頭想找一個的專案就會發現,很難在短時間內找到它,官方也並沒有提供很好的管理我們的star專案的功

幹貨 | 虛擬貨幣錢包 BIP32、BIP39、BIP44 到 Ethereum HD Wallet

建議 比較 以及 master www issue count use strip 虛擬貨幣錢包 錢包顧名思義是存放$$$。但在虛擬貨幣世界有點不一樣,我的帳戶資訊(像是我有多少錢)是儲存在區塊鏈上,實際存在錢包中的是我的帳戶對應的 key。有了這把 key 我就可以在虛

前端仔教你一步步實現人人對戰五子棋小遊戲canvas詳細版

線上地址--gobang online pc上使用谷歌瀏覽器比較友好@~@ 程式碼倉庫--gobang tutorial 歡迎對此倉庫進行擴充套件或star啦 @~@ 前置知識點: 阮生的es6教程和MDN的canvas教程 以上,兵馬未動,糧草先行。看官可以先體驗下小遊戲並且粗略瞭解

Go 原始碼分析 sort.go 看排序演算法的工程實踐

go version go1.11 darwin/amd64file: src/sort/sort.go 排序演算法有很多種類,比如快排、堆排、插入排序等。各種排序演算法各有其優劣性,在實際生產過程中用到的排序演算法(或者說 Sort 函式)通常是由幾種排序演算法組

Flutter入門教程零構建電商應用(一)

在這個系列中,我們將學習如何使用google的移動開發框架flutter建立一個電商應用。本文是flutter框架系列教程的第一部分,將學習如何安裝Flutter開發環境並建立第一個Flutter應用,並學習Flutter應用開發中的核心概念,例如widget、狀態等。 本系列教程包含如

C++extern關鍵字開始談C語言多檔案程式設計

extern 關鍵字 我們知道,C語言程式碼是由上到下依次執行的,不管是變數還是函式,原則上都要先定義再使用,否則就會報錯。但在實際開發中,經常會在函式或變數定義之前就使用它們,這個時候就需要提前宣告。 所謂宣告(Declaration),就是告訴編譯器我要使用這個變數或函

劍指offer上往下列印二叉樹,層次遍歷二叉樹python

題目描述 從上往下打印出二叉樹的每個節點,同層節點從左至右列印。 採用佇列的思想,出佇列則列印,然後左節點右節點分別入佇列 注意如果需要兩個不同的列表,一定不能用list = result = []這樣

11.18總結SAML出發在重定向中發現的XSS漏洞

總算回家了,完全沒想到這次要外出一個月,今天開始恢復更新。 前幾天忘記在哪裡看到了這個write up的中文翻譯了,當時也沒看,今天打算寫總結的時候剛好發現了這篇write-up,決定就是這篇了。 這個在uber發現的漏洞實現上是由logout時重定向引起的反射型XSS,是作者在分析uber的SAML功

舊文章搬運XP到Win7看Windows物件管理的變化(概述)

原文發表於百度空間,2010-08-01========================================================================== 今天花了一點時間把Windows物件管理的變化看了一下,重點放在了Win7這個新的系統上。事實上,在物件管理這一部分上

SpringCloud Eureka原始碼Eureka Client發起註冊請求到Eureka Server處理的整個服務註冊過程(上)

目錄 Eureka Client啟動並呼叫Eureka Server的註冊介面 Spring Cloud Eureka的自動配置 @EnableDiscoveryClient EurekaDiscoveryClientConfiguration