持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情
前言
本篇带大家用canvas来画一个蜘蛛侠~ :)
首先,找一张背景透明的蜘蛛侠图片,并借助工具将其转化成base64图片数据。
简单的HTML和CSS代码
<canvas id="spider-man"></canvas>
复制代码
没错,html值需要一个canvas就可以了,相应地,css也很简单:
body {
padding: 0;
margin: 0;
display: flex;
place-items: center;
place-content: center;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.9);
}
#spider-man {
width: 960px;
height: 484px;
border: 1px solid white;
}
复制代码
css代码有一点需要注意:canvas的size大小要与图片的size保持一致!
将图片画至画布上
- 首先加载图片资源,并在图片加载完毕之后执行render函数:
const imageSource = 'iVBORw0KGgoAAAANSUhEUgAAA8AAAAHk...' //前文转换好的base64图片数据
// 加载图片资源
const loadImage = () => {
const img = new Image()
img.src = `data:image/png;base64,${imageSource}`
img.addEventListener('load', () => {
render(img)
})
}
loadImage()
复制代码
render的实现也很简单,就是获取画布的上下文对象,然后调用drawImage
api绘制图片:
const CANVAS_WIDTH = 960 // 这里对应图片的size
const CANVAS_HEIGHT = 484
// 绘制图片
const renderImage = (
ctx: CanvasRenderingContext2D,
image: HTMLImageElement
) => {
ctx.drawImage(image, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
}
const render = (image: HTMLImageElement) => {
const $canvas: HTMLCanvasElement = document.querySelector('#image-canvas')
$canvas.width = CANVAS_WIDTH
$canvas.height = CANVAS_HEIGHT
const ctx: CanvasRenderingContext2D = $canvas.getContext('2d')
renderImage(ctx, image)
}
复制代码
好了,图片绘制上了,但我们也不仅仅是绘制一张图片这么简单!
瀑布粒子效果
天上掉下万千粒子,现在开始让画布“动”起来~
class
先实现一个粒子对象:
class Particle {
private ctx: CanvasRenderingContext2D
public x: number // 粒子中心x坐标
public y: number // 粒子中心y坐标
public size: number // 粒子半径
public color: string // 粒子颜色
constructor(
ctx: CanvasRenderingContext2D,
) {
this.ctx = ctx
this.x = Math.random() * CANVAS_WIDTH
this.y = Math.random() * 2.5
this.color = 'white'
this.size = Math.random() * 1.5 + 1.25
}
public draw() { // 画出来
const { ctx, x, y, size, color } = this
ctx.beginPath()
ctx.fillStyle = color
ctx.arc(x, y, size, 0, Math.PI * 2)
ctx.fill()
}
}
// 创造5000个粒子先
const createParticle = (ctx: CanvasRenderingContext2D): Particle[] => {
const particles: Particle[] = []
const amount = 5000
for (let i = 0; i < amount; i++) {
particles.push(new Particle(ctx))
}
return particles
}
// 绘制粒子
const renderParticel = (
particles: Particle[]
) => {
particles.forEach((p) => p.draw())
}
// 加入到主render函数中
const render = (image: HTMLImageElement) => {
// ...
// 省略其他,只展示渲染粒子代码
const particles = createParticle(ctx)
renderParticel(particles)
}
复制代码
看出效果了吗?与第一张图相比,顶部多了一层白色色块,这些就是我们创建的粒子,只是现在它们都集中在了顶部,一动不动!
我们让它动起来,从顶部倾泻下来,像瀑布一般~
// 给Particle对象增加一些属性
class Particle {
// ... 省略其他属性(见上文代码),新增以下属性
private velocity: number // Y方向移动速度
// ... 省略其他代码
private update() { // 更新粒子的属性信息
this.y += 2.5 + this.velocity
if (this.y > CANVAS_HEIGHT) { // 超出画布区域时,重置回画布顶部
this.y = 0
this.x = Math.random() * CANVAS_WIDTH
}
}
public draw() { // 画出来
// ... 省略其他代码
this.update() // 更新粒子的坐标信息,下次绘制的时候产生移动效果
}
}
复制代码
canvas做动画的核心其实就是重复绘制。我们知道,canvas绘制的内容是静态的,但只要我们每一帧都绘制一次,并且每次绘制改变一下绘制对象的坐标,这样,画布看起来就是具有动画效果的。
const render = (image: HTMLImageElement) => {
const $canvas: HTMLCanvasElement = document.querySelector('#image-canvas')
$canvas.width = CANVAS_WIDTH
$canvas.height = CANVAS_HEIGHT
const ctx: CanvasRenderingContext2D = $canvas.getContext('2d')
const particles = createParticle(ctx)
const animate = () => {
renderImage(ctx, image)
renderParticel(particles)
requestAnimationFrame(animate) // 借助requestAnimationFrame重复绘制
}
animate()
}
复制代码
可以看到,粒子已经自顶部倾泻下来了,周而复始。但是...我们发现画布的背景变白色了,给画布加个黑色背景试试看:
const render = (image: HTMLImageElement) => {
// ...省略其他代码
const animate = () => {
ctx.fillStyle = 'rgba(0, 0, 0)'
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
// ...省略其他代码
}
animate()
}
复制代码
完美!我们再设置一个透明度,这个透明度的作用在于使我们能看见上(上上上...)一帧的画布内容:
const render = (image: HTMLImageElement) => {
// ...省略其他代码
// ...省略其他代码
const animate = () => {
ctx.globalAlpha = 0.05 // 设置画布透明度
// ...省略其他代码
}
animate()
}
复制代码
粒子转化为图片
当前我们的龙舟还是一张图片,粒子虽然有瀑布效果了,但龙舟还是静态的,我们需要把这两个结合起来。因此,我们利用getImageData
获取画布上图片的像素信息。
let pixels: number[][] = [] // 存储图片像素信息
// 将每个像素的rgb信息转换为亮度
const calculateReleativeBrightness = (
r: number,
g: number,
b: number
): number => {
return Math.sqrt(r * r * 0.299 + g * g * 0.587 + b * b * 0.114) / 100
}
// 获取像素信息
const getPixels = (ctx: CanvasRenderingContext2D) => {
const data = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT).data
for (let j = 0; j < CANVAS_HEIGHT; j++) {
const row = []
for (let i = 0; i < CANVAS_WIDTH; i++) {
const red = data[j * 4 * CANVAS_WIDTH + i * 4]
const green = data[j * 4 * CANVAS_WIDTH + (i * 4 + 1)]
const blue = data[j * 4 * CANVAS_WIDTH + (i * 4 + 2)]
const brightness = calculateReleativeBrightness(red, green, blue)
row.push([brightness])
}
pixels.push(row)
}
}
复制代码
而在render
函数里面,我们获取完图片的像素信息之后,需要将图片从画布中清除:
const render = (image: HTMLImageElement) => {
const $canvas: HTMLCanvasElement = document.querySelector('#image-canvas')
$canvas.width = CANVAS_WIDTH
$canvas.height = CANVAS_HEIGHT
const ctx: CanvasRenderingContext2D = $canvas.getContext('2d')
const particles = createParticle(ctx)
// 绘制图片
renderImage(ctx, image)
// 获取图片像素信息
getPixels(ctx)
// 清除画布(图片)
clearCanvas(ctx)
ctx.globalAlpha = 0.05
const animate = () => { // 这里不再绘制图片了
ctx.fillStyle = 'rgba(0, 0, 0)'
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
renderParticel(particles)
requestAnimationFrame(animate)
}
animate()
}
复制代码
还不够,我们有了每个像素点的(亮度)信息,要怎么用到粒子对象上呢?结合亮度控制粒子的移动速率:
// 给Particle对象增加一些属性
class Particle {
// ... 省略其他属性(见上文代码),新增以下属性
private speed: number // 控制速率
public posX: number // 粒子对应pixels所在位置的x坐标
public posY: number // 粒子对应pixels所在位置的y坐标
// ... 省略其他代码
private update() { // 更新粒子的属性信息
this.posX = Math.floor(this.x)
this.posY = Math.floor(this.y)
const pixel = pixels[this.posY][this.posX] // 取出当前粒子对应的像素点
this.speed = pixel[0]
this.y += 2.5 - this.speed + this.velocity
// ... 省略其他代码
}
}
复制代码
我们看到,粒子落在龙舟图像的部分已经慢下来了
目前还是黑白的,我们把颜色带上:
// 获取像素信息
const getPixels = (ctx: CanvasRenderingContext2D) => {
const data = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT).data
for (let j = 0; j < CANVAS_HEIGHT; j++) {
const row = []
for (let i = 0; i < CANVAS_WIDTH; i++) {
// ...省略其他代码
const brightness = calculateReleativeBrightness(red, green, blue)
const color = `rgb(${red}, ${green}, ${blue})`
row.push([brightness, color])
}
pixels.push(row)
}
}
复制代码
class Particle {
// ... 省略其他代码
private update() { // 更新粒子的属性信息
// ... 省略其他代码
const pixel = pixels[this.posY][this.posX] // 取出当前粒子对应的像素点
this.speed = pixel[0]
this.color = pixel[1]
// ... 省略其他代码
}
}
复制代码