前言

上篇主要是理论的概述,本篇会多些实践,来讲讲canvas的基础用法,并包含一些基础三角函数的应用,推荐没有canvas基础的朋友阅读,熟悉的朋友可以跳过。
本人能力有限,欢迎牛人共同讨论,批评指正。

一起来画画吧

canvas的API有很多,如果一一列举30分钟你是绝对看不完的,而且怎么流水账还不如自己去看文档呢(笑),本教程的思路是用实例一步一步从无到有讲解基础用法。
canvas相关文档

准备工作

  1. 布置画布:通过添加<canvas>标签,添加canvas元素;
  2. 获取画布:通过<canvas>标签的id,获得canvas对象;
  3. 获得画笔:通过canvas对象的getContext("2d")方法,获得2D环境。
<canvas id="canvas" width="400" height="400"></canvas>
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');

画个箭头

首先我们来画个红边黄底的箭头,使用面向对象的代码组织方式,全部代码如下。
类名为Arrow。它拥有x轴坐标、y轴坐标、底的颜色color、旋转弧度rotation四个属性。
实例方法是draw(),它需要一个context对象作为参数,就是准备工作里的context,它就相当于是画笔,这里其实是类似依赖注入的过程,将canvas的画笔交给实例的draw()方法,实例用这个画笔去画出箭头,绘画过程见代码注释。特别注意以下几点:

  • beginPath()方法调用后moveTo()和lineTo移动坐标是相对与beginPath()时画笔的坐标的,可以理解成画笔自带一个坐标系,它可以旋转和在画布上移动,绘制工作的坐标都是属于这个坐标系的;
  • beginPath()是绘制设置状态的起始点,它之后代码设置的绘制状态的作用域结束于绘制方法stroke()、fill()或者closePath();
  • save()的作用是保存笔的状态,因为一个画布的笔只有一支,会在不同对象中传递,为了不污染后续的画就应该先保存,画完再restore()还原;
  • <canvas>本身是透明的,可以使用CSS给它个背景,例子中普遍使用白色背景。
/**
* 箭头类
* @class Representing a arrow.
*/
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "Arrow" }] */
class Arrow {
/**
* Create a arrow.
*/
constructor() {
this.x = 0;
this.y = 0;
this.color = '#ffff00';
this.rotation = 0;
}
/**
* Draw the arrow.
* @param {Object} _context - The canvas context.
*/
draw(_context) {
const context = _context;
// 会先保存画笔状态
context.save();
// 移动画笔
context.translate(this.x, this.y);
// 旋转画笔
context.rotate(this.rotation);
// 设置线条宽度
context.lineWidth = 2;
// 设置线条颜色
context.strokeStyle = '#ff0000';
// 设置填充颜色
context.fillStyle = this.color;
// 开始路径
context.beginPath();
// 将笔移动到相对位置
context.moveTo(-50, -25);
// 画线到相对位置
context.lineTo(0, -25);
context.lineTo(0, -50);
context.lineTo(50, 0);
context.lineTo(0, 50);
context.lineTo(0, 25);
context.lineTo(-50, 25);
context.lineTo(-50, -25);
// 闭合路径
context.closePath();
// 填充路径包围区
context.fill();
// 绘制路径
context.stroke();
// 载入保存的笔信息
context.restore();
}
}

同理我们还可以画点其他的,比如一个圆ball.js,稍微多些参数,慢慢理解。
成品的效果可以先看这个(稍微剧透):一个会跟踪鼠标位置的箭头

加入循环动起来

现在我们已经掌握了画画的基本功,并且可以画箭头arrow.js和圆ball.js,然而这样只是静止画,接下来我们需要一个循环,不断的执行擦除和重画的工作才能实现帧动画。
下面这段代码的中绘图函数drawFrame被立即执行,并递归调用自身,你将会在大部分例子中看到。
循环原理上一篇已经说明,不再重复。这里要说明的是clearRect(),这个函数接受一个矩形坐标,也就是(x轴坐标,y轴坐标,矩形宽度,矩形高度),用于清除矩形区域内的画。
例子里直接是清除了整个画布,但这不是绝对的,刷不刷新,是局部刷新还是全部刷新,都需要灵活处理。
这里有个不刷新的例子:鼠标画图工具

(function drawFrame() {
// 类似setTimeout的操作
window.requestAnimationFrame(drawFrame, canvas);
// 将画布擦干净
context.clearRect(0, 0, canvas.width, canvas.height);
// ...继续你的作画
}());

给它点动力

现在画面已经是在不断的重绘,但为什么还是静止的呢?因为每一次刷新都没有改变要画的内容。
那我们就给它一个目标吧,这样它才能动起来,比如就让箭头始终指向鼠标。
下面是核心代码,主要目的就是求出每帧arrow的旋转角度,这里使用的工具类mouse会实时返回鼠标的x,y轴坐标,封装原理上一篇已经讲过,根据这鼠标的坐标和arrow的坐标,即可得到鼠标的相对于arrow的距离dx和dy,如下图:

而arrow的旋转角度即可以通过dx和dy使用反正切函数得到,这里需要注意几点:

  • 仔细看上面代码中arrow的绘制过程,可知其原点是在中心位置的,所以刚好旋转角度就是画笔的旋转角度;
  • dx和dy是鼠标相对与arrow的坐标,所以图中把坐标系挪动箭头中心是没毛病的;
  • 用atan2,而不是atan,是因为tan值本来就可能是重复的,比如-1/2和1/(-2)两个都是-0.5,无法区分象限,而atan2就可以区分开。

完整实例:一个会跟踪鼠标位置的箭头

window.onload = function () {
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const mouse = utils.captureMouse(canvas);
const arrow = new Arrow(); arrow.x = canvas.width / 2;
arrow.y = canvas.height / 2; (function drawFrame() {
window.requestAnimationFrame(drawFrame, canvas);
context.clearRect(0, 0, canvas.width, canvas.height);
const dx = mouse.x - arrow.x;
const dy = mouse.y - arrow.y; arrow.rotation = Math.atan2(dy, dx);
arrow.draw(context);
}());
};

三角函数

上下运动

终于顺利过渡到三角函数的话题(笑)。三角函数不止有反正切一个应用,下面再看一个例子。
下面是一个ball在上下运动的核心代码,重点就是ball的y轴坐标改变,就是这句:

ball.y = clientY + Math.sin(angle) * range;

利用Math.sin(angle)的取值范围是-1到1,并且会随着angle增大而反复,使ball在一定范围上下运动。
完整例子:一个上下运动的球(可调参数版)

window.onload = function () {
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const ball = new Ball();
let angle = 0;
// 运动中心
const clientY = 200;
// 范围
const range = 50;
// 速度
const speed = 0.05; ball.x = canvas.width / 2;
ball.y = canvas.height / 2; (function drawFrame() {
window.requestAnimationFrame(drawFrame, canvas);
context.clearRect(0, 0, canvas.width, canvas.height); ball.y = clientY + Math.sin(angle) * range;
angle += speed;
ball.draw(context);
}());
};

向前运动

只是上下运动不过瘾,那就让圆前进吧,其实就是每帧改变x轴的位置。
核心代码如下,相比前面的上下运动,多了x轴的速度,每帧移动一点就形成了波浪前进的效果。
完整实例:一个波浪运动的球

window.onload = function () {
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const ball = new Ball();
let angle = 0;
const centerY = 200;
const range = 50;
const xspeed = 1;
const yspeed = 0.05; ball.x = 0;
(function drawFrame() {
window.requestAnimationFrame(drawFrame, canvas);
context.clearRect(0, 0, canvas.width, canvas.height);
ball.x += xspeed;
ball.y = centerY + Math.sin(angle) * range;
angle += yspeed;
ball.draw(context);
}());
};

其他示例

其他的应用就不一一讲解,罗列出来一些:

【30分钟学完】canvas动画|游戏基础(2):从零开始画画的更多相关文章

  1. 【30分钟学完】canvas动画|游戏基础(1):理论先行

    前言 本文虽说是基础教程,但这是相对动画/游戏领域来说,在前端领域算是中级教程了,不适合前端小白或萌新.阅读前请确保自己对前端三大件(JavaScript+CSS+HTML)的基础已经十分熟悉,而且有 ...

  2. 【30分钟学完】canvas动画|游戏基础(7):动量守恒与多物体碰撞

    前言 一路沿着本系列教程学习的朋友可能会发现,前面教程中都尽量避免提及质量的概念,很多运动概念也时刻提醒大家这不是真实的物体运动.因为真实的物体运动其实跟质量都是密不可分的,而且质量的引入自然必须提及 ...

  3. 【30分钟学完】canvas动画|游戏基础(4):边界与碰撞

    前言 本系列前几篇中常出现物体跑到画布外的情况,本篇就是为了解决这个问题. 阅读本篇前请先打好前面的基础. 本人能力有限,欢迎牛人共同讨论,批评指正. 越界检测 假定物体是个圆形,如图其圆心坐标即是物 ...

  4. 【30分钟学完】canvas动画|游戏基础(6):坐标旋转探究

    前言 本篇主要讲坐标旋转及其应用,这是编程动画必不可少的技术. 阅读本篇前请先打好前面的基础. 本人能力有限,欢迎牛人共同讨论,批评指正. 坐标旋转 模拟场景:已知一个中心点(centerX,cent ...

  5. 【30分钟学完】canvas动画|游戏基础(extra1):颜色那些事

    前言 本篇主要讲解关于计算机颜色系统的概念,后续结合一些canvas的应用.因为是"你不知道也没关系"的边缘知识,所以作为本系列教程的扩展,没有兴趣的同学可以跳过. 开始我们万紫千 ...

  6. 【30分钟学完】canvas动画|游戏基础(extra1-1):美图我也行

    前言 本文是接续系列教程的extra1,主要是介绍颜色系统在canvas中的应用. 本来是与extra1一起成文的,因为segmentfault莫名其妙的字数限制bug只能分割放送了. canvas操 ...

  7. 【30分钟学完】canvas动画|游戏基础(5):重力加速度与模拟摩擦力

    前言 解决运动和碰撞问题后,我们为了让运动环境更加自然,需要加入一些环境因子,比如常见的重力加速度和模拟摩擦力. 阅读本篇前请先打好前面的基础. 本人能力有限,欢迎牛人共同讨论,批评指正. 重力加速度 ...

  8. 30分钟学玩转RabbitMQ

    最近在学习RabbitMQ,在网上找了不少资料发现都特高端.动辄集群部署,分布式架构什么的,对于一个初学者实在不够友好.心想求人不如求自己,为什么不自己整理一套资料呢?于是<30分钟学玩转Rab ...

  9. 3分钟学完Python,直接从入门到精通

    作为帅气小编,我已经把python一些模块的甩在这儿了qwq,只要你拿到这些干货,包你玩转python,直接冲向"大佬"的段位,如果已经学了C或者C++或者说如果你需要你的一段关键 ...

随机推荐

  1. 为企业服务器配置RAID0、raid1、 raid10、raid5、raid6、等常见RAID

    RAID卡操作手册先从开机启动时如何进入管理界面开始介绍: 1)当机器开启后,显示器出现阵列卡检测信息时,会提示用户是否要进入管理界面对阵列卡进行操作,此时按下Ctrl + H 即可,如下图 2)按下 ...

  2. Oracle 修改数据文件路径的方法

      1. 关闭数据库,然后启动至mount状态 sqlplus / as sysdba shutdown immediate startup mount 2. 修改物理文件: 我这边将: c:\cwd ...

  3. linux的进程间通信概述

    一 进程间通信 1.1. linux内核提供多种进程间通信机制 a. 无名管道和有名管道 b. SystemV IPC:信号量.消息队列.共享内存 c. Socket域套接字 d. 信号 1.2. 无 ...

  4. JProfiler> ERROR: Invalid license key. Aborting.

    用IDEA+Tomcat的方式打开JProfiler,出现错误 1,Event Log 出错 16:10 Application Server was not connected before run ...

  5. python数据结构:pandas(3)

    一.pandas数据操作: 1.处理缺失数据 (1)判断是否存在缺失值 ser_obj.isnull(),df_obj.isnull() (2)dropna:丢弃缺失数据 (3)fillna:填充缺失 ...

  6. k3 cloud的单据存储在业务对象表中

    k3 cloud的单据存储在业务对象表中,表名为T_META_OBJECTTYPE,查询表名和对应的表: select FNAME,FBASEOBJECTID from T_META_OBJECTTY ...

  7. 关于“如何只用2GB内存从20亿,40亿,80亿个整数中找到出现次数最多的数?”的一种思路

    小弟不才,只懂一些c#的皮毛,有一些想法, int32值范围大概在-20亿——20亿,按hashtable一个keyvalue占8B的设定来说,最大可以存储大约2.5亿个 数字-次数对. 那么,可以将 ...

  8. 前端开发HTML&css入门——CSS&选择器练习

    CSS 层叠样式表 (Cascading Style Sheets)css可以用来为网页创建样式表,通过样式表可以对网页进行装饰.所谓层叠,可以将整个网页想象成是一层一层的结构,层次高的将会覆盖层次低 ...

  9. 45. Jump Game II (JAVA)

    Given an array of non-negative integers, you are initially positioned at the first index of the arra ...

  10. 磁盘IO性能优化-实践

    RAID卡缓存策略调整 原因详解 操作实例 I/O 调度算法 文件系统journal 磁盘挂载参数 操作实例 性能数据对比 RAID卡缓存策略调整 可以将RAID卡缓存策略由No Write Cach ...