持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
基本思路
- 搭建静态页面
- 在完成静态页面的基础上,为每一个需要添加动画的元素添加上对应的动画效果
搭建静态页面
绘制太阳
window.onload = () => {
const canvas = document.getElementById('canvas')
// 对图片资源进行缓存,避免频繁重复加载图片资源
let sun
let moon
let earth
const ctx = canvas.getContext('2d')
function init() {
// 背景虽然是不动的
// 但是每次canvas执行动画的时候,都需要清除画布,并重新绘制 (因为canvas没有图层的概念)
// 所以每次都需要重新绘制对应的背景
if (!sun) {
sun = new Image()
sun.crossOrigin = ''
sun.src = 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/409d9c94f04e4eabb55f9d522a05fd6c~tplv-k3u1fbpfcp-watermark.image?'
}
if (!earth) {
// 对于静态资源,绝大多数情况下,会被存放在CDN上
// 而对于跨域资源,浏览器因为同源策略,会认为其会污染画布(对画布的操作变得不在安全),所以会阻止该图片的渲染
// 如果的确有跨域图片需要渲染,那么需要满足如下两个条件
// 1. 图片开启Access-Control-Allow-Origin响应头
// 2. 设置图片的crossOrigin属性为anonymous
// - 对于img, video, script这类元素默认执行支持CORS(Cross-Origin Resource Sharing)(跨域资源共享)
// - 对应的属性为crossOrigin
// - crossOrigin可以设置如下值
// - anonymous 元素在请求跨域资源的时候 不需要凭证 只要crossOrigin的属性值不是use-credentials
// 那么对应的值就会被解析为anonymous, 例如 空字符串,'abc’等
// - use-credentials 元素在请求跨域资源的时候 需要凭证(cookie, token, 证书等)
// 服务器会对我们的凭证进行校对,如果凭证校对正确,那么就返回对应的资源,如果校验失败,则拒绝请求
// 总结:
// 1. crossOrigin不设置的时候 表示只要是跨域资源 就不允许加载跨域资源
// 2. crossOrigin的值是 anonymous表示的是 向服务器请求资源的时候 不需要携带对应的凭证
// 3. crossOrigin的值是 use-credentials 表示的是 向服务器请求资源的同时 需要携带上对应的凭证进行鉴权操作
earth = new Image()
earth.crossOrigin = ''
earth.src = 'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e8175edef0f24cfd8985c65f59a156ba~tplv-k3u1fbpfcp-watermark.image?'
}
if (!moon) {
moon = new Image()
moon.crossOrigin = ''
moon.src = 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e97c5487e284fce87884216e2ccfd5d~tplv-k3u1fbpfcp-watermark.image?'
}
}
function drawCricle() {
ctx.save()
// ctx的位移 默认单位是px,并不支持%
ctx.translate(150, 150)
ctx.beginPath()
ctx.arc(0, 0, 100, 0, Math.PI * 2)
ctx.strokeStyle = 'rgba(0, 153, 255, 0.4)'
ctx.stroke()
ctx.restore()
}
function drawBg() {
// 在每次对canvas进行操作的时候,尤其是修改了canvas的状态或修改了canvas的坐标轴(也就是对canvas进行形变操作)的时候
// 应该先将canvas原始状态进行保存,以方便canvas坐标轴的恢复
ctx.save()
// 对于不存在的图片,canvas会静默失败
// 所以在刚刚开始执行requestAnimationFrame方法的时候,图片可能没有加载完全,所以此时界面上没有任何内容
// 等到图片加载完全后,requestAnimationFrame方法就可以正确绘制出所需要的图形
// 而图片加载的过程,本质上是很快的,所以不会出现刚刚开始没有图形,过一段时间再出现图形,而造成的闪屏效果
// 因此 这里在加载图片的时候,并不需要通过onload去监听图片是否已经加载完成
ctx.drawImage(sun, 0, 0)
// 绘制圆环
drawCricle()
ctx.restore()
}
// 开启动画的执行
requestAnimationFrame(function fn() {
ctx.save()
ctx.clearRect(0, 0, 300, 300)
init()
drawBg()
ctx.restore()
requestAnimationFrame(fn)
})
}
此时界面中存在的坐标轴如下
绘制地球
function drawCricle() {
ctx.save()
ctx.translate(150, 150)
ctx.beginPath()
ctx.arc(0, 0, 100, 0, Math.PI * 2)
ctx.strokeStyle = 'rgba(0, 153, 255, 0.4)'
ctx.stroke()
// 之所以在这里绘制地球 是因为地球的坐标轴需要在圆环对应的坐标轴上进行二次位移
drawEarth()
ctx.restore()
}
function drawShadow() {
ctx.save()
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'
// 1. 绘图的时候 基准点位于左上角 所以需要对遮罩层的位置进行微调
// 2. 因为图片背景默认是黑色,而遮罩层的颜色也是半透明的黑色
// 所以在这里直接绘制矩形即可,不需要绘制半圆
ctx.fillRect(0, -12, 24, 24)
// 绘制地球的阴影,即面向太阳的部分高亮,背向太阳的部分阴暗
// 基本思路: 绘制一个遮罩层
drawShadow()
ctx.restore()
}
function drawEarth() {
ctx.save()
ctx.translate(100, 0)
ctx.drawImage(earth, 0, 0)
ctx.restore()
}
但是此时,我们会发现对应的地球和太阳的对齐方式会存在问题,地球的位置明显靠下
这是因为,在绘制图片的时候,默认坐标轴的基准点是左上角,所以会导致实际绘制的图片靠下
所以需要在drawImage的时候,对图片的位置进行微调
ctx.drawImage(earth, -12, -12)
绘制月亮
function drawMoon() {
ctx.save()
ctx.translate(0, 28)
// 因为默认基准点在左上角,所以对坐标轴进行微调
ctx.drawImage(moon, -3.5, -3.5)
ctx.restore()
}
function drawEarth() {
ctx.save()
ctx.translate(100, 0)
ctx.drawImage(earth, -12, -12)
drawShadow()
// 绘制月亮,月亮是围绕地球进行旋转的
// 所以其坐标轴依旧是相对于地球
drawMoon()
ctx.restore()
}
添加动画
页面的静态结构依旧搭建完成,此时就可以对需要变化的部分添加对应的动效
地球
- 地球绕太阳旋转
- 一周的执行时间为60s
function drawCricle(second) {
ctx.save()
ctx.translate(150, 150)
// 地球绕太阳转,那么本质上旋转的是太阳对应的那个坐标轴
// 旋转一圈是 2* Math.PI, 旋转一圈用时60s
// 所以 每秒需要转动的角度为 Math.PI * 2 / 60
// 则第一秒 旋转 Math.PI * 2 / 60 * 1
// 第二秒 旋转 Math.PI * 2 / 60 * 2
// 。。。
// 第n秒 旋转 Math.PI * 2 / 60 * n
ctx.rotate(Math.PI * 2 / 60 * second)
ctx.beginPath()
ctx.arc(0, 0, 100, 0, Math.PI * 2)
ctx.strokeStyle = 'rgba(0, 153, 255, 0.4)'
ctx.stroke()
drawEarth()
ctx.restore()
}
但是此时旋转会发现,地球的旋转动画十分的卡顿,因为此时地球的旋转角度是每秒变化一次,此时对应的动画看上去就会非常的卡顿
所以为了可以流畅的执行对应的动画,我们需要让太阳对应的坐标轴的旋转角度的变化达到毫秒级别
function drawCricle(second, millisecond) {
ctx.save()
ctx.translate(150, 150)
// 地球绕太阳转,那么本质上旋转的是太阳对应的那个坐标轴
// 这样就可以保证地球的高亮面永远朝向太阳
// 旋转一圈是 2* Math.PI, 旋转一圈用时60s
// 所以 每秒需要转动的角度为 Math.PI * 2 / 60
// 则第一秒 旋转 Math.PI * 2 / 60 * 1
// 第二秒 旋转 Math.PI * 2 / 60 * 2
// 。。。
// 第n秒 旋转 Math.PI * 2 / 60 * n
// 类似于秒,对应的每毫秒 坐标轴旋转的角度为
// Math.PI * 2 / 60 / 1000 * i
ctx.rotate(Math.PI * 2 / 60 * second + Math.PI * 2 / 60 / 1000 * millisecond)
ctx.beginPath()
ctx.arc(0, 0, 100, 0, Math.PI * 2)
ctx.strokeStyle = 'rgba(0, 153, 255, 0.4)'
ctx.stroke()
drawEarth()
ctx.restore()
}
月球
月球的动画的执行思路和地球的动画对应的执行思路是一模一样的
function drawMoon(second, millisecond) {
ctx.save()
// 月球是绕着地球转的,所以实际转动的坐标轴是地球的坐标轴
// 但是对应的坐标轴的渲染不能在drawEarth中完成
// 因为对于地球而言存在对应的高亮面,所以在绘制遮罩层的时候,地球的坐标轴是不能发生改变的
// 所以对应的坐标轴的渲染应该在drawMoon中完成,因为drawMoon保证了对应的状态,且drawMoon函数执行完毕后,会恢复初始坐标轴
// 此外,旋转的坐标轴是地球对应的坐标轴,不是月球对应的坐标轴
// 所以需要在坐标轴改变之前完成对应的旋转
ctx.rotate(Math.PI * 2 / 10 * second + Math.PI * 2 / 10 / 1000 * millisecond)
ctx.translate(0, 28)
ctx.drawImage(moon, -3.5, -3.5)
ctx.restore()
}
function drawEarth(second, millisecond) {
ctx.save()
ctx.translate(100, 0)
ctx.drawImage(earth, -12, -12)
drawShadow()
drawMoon(second, millisecond)
ctx.restore()
}