筆者以前有一段時間一直在學習Canvas相關的技術知識點,經過參考網上的一些資料文章,學着利用簡單的數學和物理知識點實現了一些比較有趣的動畫效果,最近恰好翻看到之前的代碼,因此此次將這些代碼實踐從新梳理一遍後整理成文,本身鞏固複習的同時,能夠和你們一塊兒交流學習。做爲【Canvas真好玩】系列的第一篇文章,筆者仍是從最經典的黑客帝國開始,在一步一步進行代碼具體實踐的同時,帶領你們進入神奇的Canvas動畫的世界。javascript
代碼已上傳至Github,能夠拉下來後直接運行,省掉下面的準備工做環節。css
由於以前的代碼比較久遠,此次打算使用React來重構一遍,仍是使用目前使用頻率比較高的create-react-app
腳手架來搭建項目,在本地找到合適的項目路徑,而後執行項目初始化命令:html
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
文件:java
/* 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
用於修改默認配置:node
+ const { override, fixBabelImports } = require('customize-cra'); + module.exports = override( + fixBabelImports('import', { + libraryName: 'antd', + libraryDirectory: 'es', + style: 'css', + }), + );
到目前爲止,項目的目錄結構以下:react
├── 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
組件來實現頁面佈局,修改後的代碼以下:webpack
// 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; }
至此,咱們項目的基本代碼結構就已經書寫完畢,這裏先貼一張我目前已經完成的頁面效果:
其實也沒有那麼好看,主要是爲了方便管理菜單,接下來咱們就來一步一步分析實現頁面中炫酷的黑客帝國效果吧。git
在代碼實踐以前,咱們先來分析一下黑客帝國的實現細節,在上面的動畫效果中,咱們能夠知道,動畫其實就是由各類英文字母,數字以及特殊符號實現的一個從上到下的距離偏移效果,因此咱們在代碼中會維護一個集合用於存放全部可能出現的文字。其次,咱們能夠看出,文字的下墜效果實際上是分紅了多列的,固然列數會根據Canvas容器的寬度來動態計算。爲了實現動畫,咱們這裏能夠藉助瀏覽器的requestAnimationFrame
來保持每秒60幀的流暢度,相信大部分前端人員對這個Api已經不陌生了,不過這裏須要注意如下兩點:github
- 若想在瀏覽器下次重繪以前繼續更新下一幀動畫,那麼回調函數自身必須再次調用requestAnimationFrame()
- 爲了提升性能和電池壽命,所以在大多數瀏覽器裏,當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!
你的一個點贊,值得讓我付出更多的努力!
逆境中成長,只有不斷地學習,才能成爲更好的本身,與君共勉!