自我介绍
大家好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。
前言
Canvas技术可以说是web技术花样最多的了。
根据mdn,它是由HTML5<canvas>标签和Canvas API所构成。主要聚焦于2d图形,当然也可以结合webgl作3d图形。
那么它可以做什么呢?
- 协同文档(也有基于svg的)
- 游戏(系兄弟就来砍我)
- 可视化图表(echarts、antV)
- 网页的动画(苹果官网某些部分)
- 图片编辑(在线ps)
- 实时视频处理
下面来学习一下吧
边上手边认识API
搭建环境
新建一个canvas画布元素
<template>
<canvas id="canvas" width="500" height="500">
<div> 你的浏览器不支持Canvas </div>
</canvas>
</template>
注意: 这里别同时用css和width/height来设置画布大小,否则会出现缩放,导致后续画图的时候坐标对应不上。
canvas上下文
这里的上下文可以理解为生成一个画笔
const canvas = document.querySelector('#canvas')
// 拿到渲染上下文
const ctx = canvas.getContext('2d')
可以看到拥有一些基本信息,方法在原型CanvasRenderingContext2D上。
画一条直线吧
画画肯定需要告诉它你需要画在什么地方吧? 就像初中物理课上的坐标系一样,需要一个标准去描述你的画笔🖌️应该在什么位置。 Canvas的坐标系跟position布局的差不多啊
那么开始画画吧
// 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
ctx.beginPath()
// 起点,类比于下笔
ctx.moveTo(100,50)
// 经过的路径点
ctx.lineTo(100,100)
// 经过的路径点
ctx.lineTo(50,100)
// 有路径之后,需要通过线条来绘制图形轮廓。
ctx.stroke()
从起点(100,50)-->路径点1(100,100)-->路径点2(50,100)三个点组成半个正方形。
没错,画完是这样子的
这里是我自己加了背景色。
三角形
同样的可以画个三角形
在刚才的基础之上,使用closePath()方法即可
。。。刚才的代码
闭合路径,使得图形绘制命令又重新指向到上下文中。
ctx.closePath()
// 注意需要在填充线条之前闭合路径
ctx.stroke()
如果需要颜色的话可以使用fill()去填充。注意,比如刚刚未closePath()时也可以填充,此时会自动填充
正方形
rect(x,y,width,height) 生成矩形路径 (注意了,路径就需要stroke()绘制)
也可以这样
strokeRect(x,y,width,height) = rect(x,y,width,height) + stroke()
矩形擦除(清除画布)
clearRect(x,y,width,height) 擦除范围内的矩形
矩形路径填充
fillReact(x,y,width,height) = rect(x,y,width,height) + stroke()(可有可无) + fill()
圆形
arc(x,y,r,startAngle,endAngle,anticlockwise)
r -- 当然是 半径啦
x,y -- 当然是组成 圆心 坐标点啦
startAngle 、endAngle -- 开始结束角度 从x正半轴到y正半轴 (弧度制)
anticlockwise -- boolean 是否逆时针 ,默认为flse
ctx.arc(100,150,30,0,Math.PI/2,true)
ctx.fill()
ctx.arc(100,150,30,0,Math.PI/2,false)
ctx.fill()
贝塞尔曲线
贝塞尔曲线就是PhotoShop上面到钢笔工具那个 它有两种
2次贝塞尔曲线 1个控制点 1个结束点
quadraticCurveTo(cp1x, cp1y, x, y)cp1x,cp1y为一个控制点,x,y为结束点
3次贝塞尔曲线 2个控制点 1个结束点
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。
图中蓝点为起始点 ,红点为控制点
有看官可能会问,那起点呢?可别忘了我们的moveTo(x,y)啊
一般来说用photoshop话会比较直观,靠想象太难了
文字
对没错,文字同样可以在画布上显示
同样的fill和stroke两兄弟搭配
fillText(text,x,y,[, maxWidth]) maxWidth最大宽度 ,超过会被横向缩放
strokeText(text,x,y,[, maxWidth])
ctx.font = "bold 48px serif";
文本对齐全
ctx.textAlign = 'left'|'right'|'center'
文字方向
ctx.direction = 'ltr'|'rtl'|'inherit'
图片
drawImage(image, dx, dy)
drawImage(image, dx, dy, dWidth, dHeight)
// 前四代表裁剪的部分
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
// 示例
const img = new Image()
img.src = '../xxx.png'
ctx.drawImage(img,200,200,100,100)
可以看到,这方法可以重载为三种方法,简单区分就是 dx,dy是根据目标画布的坐标点,sx,sy代表image 的矩形(裁剪)选择框 基于(左上角) 坐标点
style
通过fill stroke 两兄弟搭配style来设置上下文的颜色
ctx.strokeStyle ='blue'
ctx.fillStyle ='blue'
ctx.fillStyle = color;
ctx.fillStyle = gradient;
ctx.fillStyle = pattern;
同时可以设置lineCaps末端颜色 lineWidth路径宽度 setLineDash([实线长度,间隔长度])
保存和重置
设置了上下文的颜色啊,字体大小之类的会影响我们后续要画的内容,因此需要一个重置的步骤
但这样不太优雅,因此可以使用save()和restore()方法去存档和恢复
save() restore()
注意:这里的restore可以恢复到上次保存的状态(这里只包括颜色,线宽之类的)
变形
- 移动坐标系原点 translate(x,y)
- 旋转坐标系 rotate(angle)
- 伸缩 scale(x,y) 水平/垂直伸缩因子 注意: save 和restore方法也是可以恢复这些状态的
实战
实战部分分为两个小例子
签名版
利用mouseMove mouseDown mouseUp 3个事件,结合刚刚路径和线条方法可以实现一个具有清除/保存功能的签名版
苹果官网小动画
可以看到MacbookPro的M2芯片这里的动画有个滚动时才会触发的动画
是不是很神奇?
通过观察代码可以发现,这是用Canvas来绘制帧动画实现的
思路就是scroll滚动时候触发对应帧的动画渲染,渲染则是通过canvas2d 上下文的drawImage方法
下面来实现一下
构建DOM页面结构
// 设定一个高度,模拟苹果官网
<body style="height: 3000px;">
<div id="app">
<div>
这是苹果官网的其他内容
</div>
<div id="container">
<canvas id="pro" width="308" height="308">
<div> 你的浏览器不支持Canvas!</div>
</canvas>
<canvas id="max" width="308" height="308">
<div> 你的浏览器不支持Canvas!</div>
</canvas>
</div>
</div>
</body>
设定一下style
body {
margin: 0;
padding: 20px;
}
#container {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
position: relative;
top: 500px;
padding: 5%;
background-image: url('./03_source/m2_bg__e4dkdscoyaaa_small_2x.jpg');
}
#container canvas {
margin-bottom: 20px;
}
利用gasp库实现滚动悬浮的效果
// 利用gsap这个库控制滚动到经过盒子时,盒子悬浮在视口上
// 当滚动条滚动超过盒子的范围时,取消悬浮效果
// 注册ScrollTrigger插件
gsap.registerPlugin(ScrollTrigger);
gsap.to("#container", {
opacity: 1,
scrollTrigger: {
trigger: "#container",
duration: 2,
start: `top ${TOP}px`, // 从盒子顶部15px处开始固定
scrub: true, // 表示动画可以重复执行改成false表示只执行一次
// markers: true, // 绘制开始位置和结束位置的线条 (开发的时候可以打开更直观)
pin: true, // 动画执行期间,页面不进行滚动,动画执行结束后
},
});
接下来的思路就是
拿到渲染上下文 --> 封装好渲染函数 --> 在scroll事件的回调中实现关键逻辑
下面看看回调当中的关键逻辑
// 记录下初次滑动到悬停位置
let firstScrollTop
const handleScroll = ()=>{
const top = container.getBoundingClientRect().top // 容器距离视口的高度
const height = container.getBoundingClientRect().height // 432px 容器的高度
// 此时容器开始滚动停滞
if(top === TOP){
// 拿到此时滚动的距离
const scrollTop = document.body.scrollTop
// 记录下初次停滞时候的滚动距离
if(firstScrollTop == undefined){
firstScrollTop = document.body.scrollTop
}
// 求出当前滚动和初次触发停滞时候的差值
// 比上容器本身的高度获得百分比(当滚动的长度超过容器高度时,容器不再停滞)
const percent = (scrollTop - firstScrollTop) / height
// 百分比和需要渲染的帧相乘,得出当前这个滚动距离应该渲染的帧(即图片)
const renderIndex = Math.ceil(percent * TOTAL_PICS)
// 借助window.requestAnimationFrame来渲染
requestAnimationFrame(()=>{
renderImg(renderIndex)
})
}
}
其余函数
渲染对应的帧
const renderImg = (index)=>{
if(index == undefined ) return
if(IMG_MAP.pro[index]){
proCtx.clearRect(0,0,CANVAS_HEIGHT,CANVAS_HEIGHT)
proCtx.drawImage(IMG_MAP.pro[index],0,0)
}
if(IMG_MAP.max[index]){
maxCtx.clearRect(0,0,CANVAS_HEIGHT,CANVAS_HEIGHT)
maxCtx.drawImage(IMG_MAP.max[index],0,0)
}
}
// canvas 容器的宽高
const CANVAS_HEIGHT = 308
const TOTAL_PICS = 52 // 总共的img、
// 存放Image对象
const IMG_MAP = {
max: new Array(TOTAL_PICS).fill(null),
pro: new Array(TOTAL_PICS).fill(null),
}
// 从静态资源当中引入图片,并且包装成Image对象
const loadImages = ()=>{
return new Promise((resolve,reject)=>{
let count = 0
for(let i = 0 ;i <=TOTAL_PICS ; i ++ ){
const n = i < 10 ? `0${i}` : `${i}`
const pro = new Image()
pro.src = `./03_source/m2pro/small_00${n}.jpg`
const max = new Image()
max.src = `./03_source/m2max/small_00${n}.jpg`
// 异步加载需要控制下
pro.onload = ()=>{
IMG_MAP.pro[i] = pro
count ++
if(count == 2*(TOTAL_PICS+1)){
resolve()
}
}
max.onload = ()=>{
IMG_MAP.max[i] = max
count ++
if(count == 2*(TOTAL_PICS+1)){
resolve()
}
}
// 处理错误
pro.onerror = max.onerror = ()=>{
reject()
}
}
})
}
具体的实现代码可以参考我的github