前言

笔者之前有一段时间一直在学习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真好玩】从黑客帝国开始的更多相关文章

  1. H5 canvas 解决合成图模糊(canvas真机下scale不起作用,无法缩放显示的问题)

    在解决canvas合成图片模糊的问题想必我们已经了解了 window.devicePixelRatio      window接口的devicePixelRatio返回当前显示设备的物理像素分辨率与C ...

  2. Linux真好玩阿,不过我家电脑不行,运行不够流畅

    不过呢....能编程就行了....   哎,总说不用QT不用QT,最后还是没办法,用QT了   连个界面都看不到的话,感觉太差了....   我还不会用QT呢....好好学习....争取像MFC那样习 ...

  3. 车大棒浅谈for循环+canvas实现黑客帝国矩形阵

    背景: 一日在网上闲逛的之时,突然看到一个利用JQ插件实现canvas实现的电影黑客帝国的小Demo.觉得创意不错,就下载下来研究一下. 网上浏览jQuery的写法 $(document).ready ...

  4. 小程序canvas生成海报保存至手机相册

    小程序canvas画图保存至手机相册 (1)可直接展示生成的海报 .因手机分辨率不同可能导致生成的海报会有细微差别,这里隐藏canvas海报,页面正常设置海报样式保存时保存隐藏的canvas海报 (2 ...

  5. 简单的运动学,用canvas写弹力球

    声明:本文为原创文章,如需转载,请注明来源WAxes,谢谢! 跟之前的随笔一样,因为本人仍是菜鸟一只,所以用到的技术比较简单,不适合大神观看...... 学canvas学了有一个多礼拜了,觉得canv ...

  6. 后HTML5时代

    十二年前,无论多么复杂的布局,在我们神奇的table面前,都不是问题:十年前,阿捷的一本<网站重构>,为我们开启了新的篇章:八年前,我们研究yahoo.com,惊叹它在IE5下都表现得如此 ...

  7. /etc/xinetd.conf 和 /etc/xinetd.d/*【新网络服务配置】

    http://blog.csdn.net/kelven2004/article/details/1701930 xinetd 是 inetd 的安全加强版,它内置了自己的 TCP wrapper, 可 ...

  8. Atitit 基于dom的游戏引擎

    Atitit 基于dom的游戏引擎 1. 添加sprite控件(cocos,createjs,dom)1 1.1.1. Cocos1 1.1.2. createjs1 1.1.3. Dom模式2 1. ...

  9. 偶然的发现(与Code无关)

    最近做后台用户注册, 在考虑不使用验证码, 百度搜了一下看了看一些相关技术, 发现了个小说——[万恶的验证码], 看了挺搞笑分享一下:原文链接 万恶的验证码 前言: 传说中,它是最为邪恶的吸血鬼,它是 ...

随机推荐

  1. display——table-cell属性

    display的table和table-cell一般情况下用的不多,所以很少有人去关注它,但他们两个联手起来会给你惊喜! 当两个或者两个以上标签一起使用显示在同一行时,以前常用的是float.posi ...

  2. Java中冒泡排序法的代码实现方法之一

    主要运用双层for循环嵌套,进行冒泡排序 public class BubbleSortTest { public static void main(String[] args) { int[] ar ...

  3. 计算几何基础算法几何C++实现

    This file is implementation of Common Common Computational Geometry Algorithms.Please please pay att ...

  4. 彻底修改eclipse中项目的名称

    需要四个步骤: 一.右键工程:Refactor->Rename,或选中工程按F2,修改名称 二.修改项目目录下:.project文件 三.项目右键属性 --> Web Project Se ...

  5. postman全局变量设置

    1.点击小齿轮进入到变量添加页面,点击Globals添加全局变量 2.输入变量名称和变量值 3.接口中设置变量

  6. Python_散点图绘制

    为了可视化一些数据分布,需要以散点图的形式呈现 引入绘图工具 import matplotlib.pyplot as plt from matplotlib.font_manager import F ...

  7. shark恒破解笔记6-摆脱NAG

    1.打开软件后,发现是未注册,然后点击关闭按钮,会弹出窗口 我们的目的就是为了能够去掉这个弹窗. 2.对这个程序进行查壳,没有什么发现 3.载入OD里面,F9运行起来,随后切换到程序主界面点击关闭按钮 ...

  8. 本月16日SpringBoot2.2发布,有哪些变化先知晓

    本月(2019年10月16日)Spring Boot 2.2已经正式发布了!在此篇文章中,将给大家介绍一下2.2版为大家带来了哪些重要的新变化.笔者用心书写,希望阅读完成之后转发关注,你的支持是我不竭 ...

  9. 开普勒云平台:如何配置gitlab与Jenkins

    一.Kplcloud是什么? kplcloud是一个基于Kubernetes的轻量级PaaS平台,通过可视化的界面对应用进行管理,降低应用容器化的对度,从而减少应用容器化的时间成本. Kplcloud ...

  10. 算法<初级> - 第一章 排序相关问题

    算法 - 第一章 时间复杂度: Big O 时间/空间复杂度计算一样,都是跟输入数据源的大小有关 n->∞ O(logn) 每次只使用数据源的一半,logn同理 最优解 先满足时间复杂度的情况最 ...