笔者以前有一段时间一直在学习Canvas相关的技术知识点,经过参考网上的一些资料文章,学着利用简单的数学和物理知识点实现了一些比较有趣的动画效果,最近恰好翻看到之前的代码,因此此次将这些代码实践从新梳理一遍后整理成文,本身巩固复习的同时,能够和你们一块儿交流学习。做为【Canvas真好玩】系列的第一篇文章,笔者仍是从最经典的黑客帝国开始,在一步一步进行代码具体实践的同时,带领你们进入神奇的Canvas动画的世界。javascript
代码已上传至 Github,能够拉下来后直接运行,省掉下面的准备工做环节。
由于以前的代码比较久远,此次打算使用React来重构一遍,仍是使用目前使用频率比较高的create-react-app
脚手架来搭建项目,在本地找到合适的项目路径,而后执行项目初始化命令:css
npm install -g create-react-app create-react-app react-canvas
考虑到后期可能会有一系列的动画效果,因此为了界面美观以及方便管理,这里直接简单使用下React Ant Design来管理动画菜单方便切换到不一样的动画,使用react-router-dom
来控制路由,同时使用loadable
来对路由实现按需加载:html
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
用于修改默认配置:java
+ const { override, fixBabelImports } = require('customize-cra'); + module.exports = override( + fixBabelImports('import', { + libraryName: 'antd', + libraryDirectory: 'es', + style: 'css', + }), + );
到目前为止,项目的目录结构以下:node
├── 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
组件来实现页面布局,修改后的代码以下:react
// 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; }
至此,咱们项目的基本代码结构就已经书写完毕,这里先贴一张我目前已经完成的页面效果:
其实也没有那么好看,主要是为了方便管理菜单,接下来咱们就来一步一步分析实现页面中炫酷的黑客帝国效果吧。webpack
在代码实践以前,咱们先来分析一下黑客帝国的实现细节,在上面的动画效果中,咱们能够知道,动画其实就是由各类英文字母,数字以及特殊符号实现的一个从上到下的距离偏移效果,因此咱们在代码中会维护一个集合用于存放全部可能出现的文字。其次,咱们能够看出,文字的下坠效果实际上是分红了多列的,固然列数会根据Canvas容器的宽度来动态计算。为了实现动画,咱们这里能够借助浏览器的requestAnimationFrame
来保持每秒60帧的流畅度,相信大部分前端人员对这个Api已经不陌生了,不过这里须要注意如下两点:git
- 若想在浏览器下次重绘以前继续更新下一帧动画,那么回调函数自身必须再次调用requestAnimationFrame()
- 为了提升性能和电池寿命,所以在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的iframe里时,requestAnimationFrame() 会被暂停调用以提高性能和电池寿命
经过这个动画Api咱们就能够在每帧的时间内清空当前的Canvas容器状态,同时计算每一个文字的新坐标并进行绘制,咱们能够为每列文字的Y轴偏移定义一个初始变量为1,即表示一个字体单位的大小,每次当文字下落一个字体大小的时候,将这个初始变量加1,这样在下次计算文字坐标的时候,就能够将这个值乘以字体大小从而得出Y轴的坐标,这样在视觉上就达到了一个文字的下坠效果。这里须要提一下的是,Canvas的坐标系统和理科领域的笛卡尔坐标系有点不太同样,采用默认的窗口坐标系统,即原点坐标位于窗口的左上角,沿X轴方向向右为正值,沿Y轴方向向下为正值,在后续计算文字坐标的时候须要注意这里的区别,其实窗口坐标系统中也是有负值的,只是跑到了屏幕以外,咱们通常没有注意到而已。
笛卡尔坐标系:
窗口坐标系:
关于Canvas其余的知识点和基础API不是本系列的重点,感兴趣的同窗能够自行网上查阅下相关资料,Canvas的绘图API也不是不少,学习门槛不高,很好掌握。基于以上的分析,咱们尝试完善一下Hacker.js
中的代码:github
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!
你的一个点赞,值得让我付出更多的努力!
逆境中成长,只有不断地学习,才能成为更好的本身,与君共勉!