
大家好,今天我向大家分享一个基于 Vue3+ts+canvas 实现的落叶飘过效果。当然这里并没有做一些基础的知识的科普,我们讲究实现的逻辑。(多多包涵)

可以用在看不见的地方,因为看得见的地方不太好意思用。
首先,我们先创建基础的vue文件结构
创建Leaf.vue,没啥特别的,就一个vue文件,声明了一些函数,调用执行而已(这里最特别的应该就是各位看官老爷了)
html
<template>
<div ref="dropLeafContainer" class="drop-leaf-contaier">
<canvas ref="canvasRef"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, getCurrentInstance, onMounted } from 'vue'
import { Leaf } from './Leaf'
const colorList = [
"#1abc9c",
"#2ecc71",
"#3498db",
"#9b59b6",
"#f1c40f",
"#e67e22",
"#e74c3c",
"#34495e",
];
interface IProps {
width?: number
height?: number
full?: Boolean
}
const props = withDefaults(defineProps<IProps>(), {
height: 400,
width: document.body.clientWidth,
})
let canvasRef: HTMLCanvasElement | null = null
let canvasCtx = null
let leafList: Array<object> = []
// 开始动画
const startAnimate = () => {
// canvasCtx.clearRect(0, 0, canvasRef.width, canvasRef.height)
leafList.forEach((leaf) => {
// leaf.update()
leaf.draw()
})
// requestAnimationFrame(startAnimate)
}
// 初始化canvas
const initCanvas = () => {
canvasRef.height = props.height
canvasRef.width = props.width
canvasCtx = canvasRef?.getContext('2d')
const x = Math.random() * props.width;
const y = 100;
const speedX = Math.random() - 0.5;
const speedY = Math.random() * 2 + 1;
const radiusX = Math.random() * 5 + 5;
const radiusY = Math.random() + 5;
const rotation = (Math.random() * 30) / Math.PI;
const color = colorList[Math.floor(Math.random() * 9)];
const leaf = new Leaf(
canvasCtx,
x,
y,
radiusX,
radiusY,
speedX,
speedY,
color,
0,
rotation,
1,
props.width,
props.height,
);
leafList = [leaf]
startAnimate()
}
onMounted(() => {
const { ctx } = getCurrentInstance()
canvasRef = ctx.$refs.canvasRef
initCanvas()
})
</script>
<style lang="scss" scoped>
.drop-leaf-contaier {
position: absolute;
top: 0px;
z-index: -1;
width: 100%;
}
</style>
其次,创建单个叶子玩玩
相信眼尖的朋友们,应该有看到了,上方我们引用了一个 .Leaf 的文件。猜猜这个文件哪来的。(我猜应该是本地声明的吧)
接下来,我们先创建一个小叶子玩玩
创建叶子
正常逻辑下,我们叶子需要包含以下方法
- draw 绘制一个叶子
- resize 屏幕宽高度变化时,更新宽度高度
- update 更更新叶子的位置
接下来,我们一步一步来,从声明一个对象开始
声明叶子对象
首先上图,让我们看看叶子长啥样

可以五颜六色,但是不要绿色就行
我们看到首先一个叶子,他需要是椭圆的(也不绝对,大佬们弄个奇形怪状的都行),一个这种形状的叶子,需要什么参数捏?
需要 圆角,大小,颜色,透明度等,所谓我们底下就先定义了一个对象,并声明了一些属性,以便于后面使用,
ts
// 创建叶子对象,并声明一些必要参数
class Leaf {
ctx: any // canvas ctx实例
x: number // 叶子的x坐标
y: number // 叶子的y坐标
radiusX: number // 叶子的x圆角
radiusY: number // 叶子的y圆角
speedX: number // 叶子的x速度
speedY: number // 叶子的y速度
color: string // 颜色
rotate: number // 旋转角度
globalAlpha: number // 透明度
canvasHeight: number // 画布高度
canvasWidth: number // 画布宽度
dir: number // 方向 先往左还是先往右
deg: number // 移动角度总和
delDeg: number // 移动角度
constructor(
ctx,
x,
y,
radiusX,
radiusY,
speedX,
speedY,
color,
deg,
rotate,
globalAlpha,
canvasWidth,
canvasHeight,
) {
this.ctx = ctx;
this.x = x;
this.y = y;
this.radiusX = radiusX;
this.radiusY = radiusY;
this.speedX = 1;
this.speedY = speedY;
this.color = color;
this.rotate = rotate;
this.globalAlpha = globalAlpha;
this.canvasHeight = canvasHeight;
this.canvasWidth = canvasWidth;
this.dir = Math.random() - 0.5 > 0 ? 1 : -1;
this.deg = Math.random();
this.delDeg = Math.random() * 0.1;
}
}
处理draw绘制方法
主要是使用canvas的ellipse方法
CanvasRenderingContext2D.ellipse()
是 Canvas 2D API 添加椭圆路径的方法。椭圆的圆心在(x,y)位置,半径分别是radiusX 和 radiusY ,按照anticlockwise (默认顺时针)指定的方向,从 startAngle 开始绘制,到 endAngle 结束。
语法void ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
链接 developer.mozilla.org/zh-CN/docs/...
ts
class Leaf {
...
draw() {
this.ctx.beginPath();
// 设置透明度
this.ctx.globalAlpha = this.globalAlpha;
// 设置填充颜色
this.ctx.fillStyle = this.color;
// 绘制椭圆
this.ctx.ellipse(
this.x,
this.y,
this.radiusX,
this.radiusY,
this.rotate,
0,
2 * Math.PI,
false,
);
// ctx.rotate(this.deg * Math.PI / 180)
this.ctx.fill();
this.ctx.closePath();
}
}
这样我们就绘制了一个椭圆,哦不 叶子 (变绿了 嘿嘿)

处理update方法
针对 叶子的移动方法我们需要考虑一些点
- 边界情况
- 过渡情况
- 如果做飘落效果
我可能考虑不周,目前只想到这些
针对边界的考虑,遵循以下原则
- 如果 y大于画布高度,就重置其所有 random 参数,当成是一个新的叶子来处理,当然颜色是不变的,没必要变对吧
- 如果 x大于小于画布宽度,都进行取反处理
针对过渡的情况,目前是让滚动到底下的元素增加透明度,透明度计算规则
- 1 - 当前的y坐标 / 画布总的高度
如果做飘落效果
- 之前有想过用贝塞尔曲线(不太熟练)
- 退而求其次,依据 Math.sin 函数 让其x位置进行幅度化的更改
ts
class Leaf {
...
update() {
// 如果y的坐标大于画布高度,说明移动到画布外了
if (this.y > this.canvasHeight) {
this.y = Math.random() * -100; // 重置坐标y的值
this.x = Math.random() * this.canvasWidth; // 重置x的值
this.deg = Math.random(); // 移动角度总和
this.delDeg = Math.random() * 0.1; // 移动角度
}
// 如果x坐标超出画布,赋相反方向
if (this.x >= this.canvasWidth) {
this.speedX = -Math.abs(this.speedX);
}
if (this.x <= 0) {
this.speedX = Math.abs(this.speedX);
}
// 增加透明度
this.globalAlpha = 1 - this.y / this.canvasHeight;
// 移动y
this.y += this.speedY;
// 移动x做圆弧飘落效果
this.x = this.dir
? this.x + Math.sin(this.deg)
: this.x - Math.sin(this.deg);
this.deg += this.delDeg;
// 绘制
this.draw();
}
}
这样你就拥有了一个可以下落的树叶,但是目前 vue内容需要调整下
ts
...
// 开始动画
const startAnimate = () => {
canvasCtx.clearRect(0, 0, canvasRef.width, canvasRef.height)
leafList.forEach((leaf) => {
leaf.update()
leaf.draw()
})
requestAnimationFrame(startAnimate)
}
页面效果

处理resize方法
ts
class Leaf {
...
resize(canvasWidth: number, canvasHeight: number) {
this.canvasHeight = canvasHeight;
this.canvasWidth = canvasWidth;
}
}
我们把创建叶子的方法抽离到Leaf文件中
ts
const colorList = [
"#1abc9c",
"#2ecc71",
"#3498db",
"#9b59b6",
"#f1c40f",
"#e67e22",
"#e74c3c",
"#34495e",
];
export const createRandomLeaf = (num: number, ctx: any, canvasWidth: number, canvasHeight: number) => {
const temp = [];
for (let i = 0; i < num; i++) {
const x = Math.random() * canvasWidth;
const y = -Math.random() * 100;
const speedX = Math.random() - 0.5;
const speedY = Math.random() * 2 + 1;
const radiusX = Math.random() * 5 + 5;
const radiusY = Math.random() + 5;
const rotation = (Math.random() * 30) / Math.PI;
const color = colorList[Math.floor(Math.random() * 9)];
const leaf = new Leaf(
ctx,
x,
y,
radiusX,
radiusY,
speedX,
speedY,
color,
0,
rotation,
1,
canvasWidth,
canvasHeight,
);
leaf.draw();
temp.push(leaf);
}
return temp;
};
修改vue文件代码,并做一些相关优化,最终vue代码如下
html
<template>
<div ref="dropLeafContainer" class="drop-leaf-contaier">
<canvas ref="canvasRef"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, getCurrentInstance, onMounted, onUnmounted } from 'vue'
import { createRandomLeaf } from './Leaf'
import { debounce } from '@/utils'
interface IProps {
width?: number
height?: number
full?: Boolean
}
const props = withDefaults(defineProps<IProps>(), {
height: 400,
width: document.body.clientWidth,
})
let canvasRef: HTMLCanvasElement | null = null
let canvasCtx = null
let dropLeafContainer: HTMLDivElement | null = null
let dropLeafContainerObserve: any = null
let leafList: Array<object> = []
const startAnimate = () => {
canvasCtx.clearRect(0, 0, canvasRef.width, canvasRef.height)
leafList.forEach((leaf) => {
leaf.update()
leaf.draw()
})
requestAnimationFrame(startAnimate)
}
const initCanvas = () => {
canvasRef.height = props.height
canvasRef.width = props.width
canvasCtx = canvasRef?.getContext('2d')
leafList = createRandomLeaf(50, canvasCtx, props.width, props.height)
startAnimate()
}
const reszieChange = ([{ contentRect }]) => {
if (leafList.length && contentRect.width) {
canvasRef.width = contentRect.width
leafList.forEach((leaf) => {
leaf.resize(contentRect.width, props.height)
})
}
}
const listenerResize = () => {
dropLeafContainerObserve = new ResizeObserver(debounce(reszieChange, 200))
dropLeafContainerObserve.observe(dropLeafContainer)
}
const unListenerResize = () => {
if (dropLeafContainerObserve && dropLeafContainer) {
dropLeafContainerObserve.unobserve(dropLeafContainer)
dropLeafContainerObserve = null
dropLeafContainer = null
leafList = []
}
}
onMounted(() => {
const { ctx } = getCurrentInstance()
canvasRef = ctx.$refs.canvasRef
dropLeafContainer = ctx.$refs.dropLeafContainer
listenerResize()
initCanvas()
})
onUnmounted(() => {
unListenerResize()
})
</script>
<style lang="scss" scoped>
.drop-leaf-contaier {
position: absolute;
top: 0px;
z-index: -1;
width: 100%;
}
</style>
完整 Leaf.js代码
ts
interface ILeaf {
ctx: any;
}
export class Leaf {
ctx: any // canvas ctx实例
x: number // 叶子的x坐标
y: number // 叶子的y坐标
radiusX: number // 叶子的x圆角
radiusY: number // 叶子的y圆角
speedX: number // 叶子的x速度
speedY: number // 叶子的y速度
color: string // 颜色
rotate: number // 旋转角度
globalAlpha: number // 透明度
canvasHeight: number // 画布高度
canvasWidth: number // 画布宽度
dir: number // 方向 先往左还是先往右
deg: number // 移动角度总和
delDeg: number // 移动角度
constructor(
ctx,
x,
y,
radiusX,
radiusY,
speedX,
speedY,
color,
deg,
rotate,
globalAlpha,
canvasWidth,
canvasHeight,
) {
this.ctx = ctx;
this.x = x;
this.y = y;
this.radiusX = radiusX;
this.radiusY = radiusY;
this.speedX = 1;
this.speedY = speedY;
this.color = color;
this.rotate = rotate;
this.globalAlpha = globalAlpha;
this.canvasHeight = canvasHeight;
this.canvasWidth = canvasWidth;
this.dir = Math.random() - 0.5 > 0 ? 1 : -1;
this.deg = Math.random();
this.delDeg = Math.random() * 0.1;
}
draw() {
this.ctx.beginPath();
this.ctx.globalAlpha = this.globalAlpha;
this.ctx.fillStyle = this.color;
// 绘制椭圆
this.ctx.ellipse(
this.x,
this.y,
this.radiusX,
this.radiusY,
this.rotate,
0,
2 * Math.PI,
false,
);
// ctx.rotate(this.deg * Math.PI / 180)
this.ctx.fill();
this.ctx.closePath();
}
resize(canvasWidth: number, canvasHeight: number) {
this.canvasHeight = canvasHeight;
this.canvasWidth = canvasWidth;
}
update() {
// 如果y的坐标大于画布高度,说明移动到画布外了
if (this.y > this.canvasHeight) {
this.y = Math.random() * -100; // 重置坐标y的值
this.x = Math.random() * this.canvasWidth; // 重置x的值
this.deg = Math.random(); // 移动角度总和
this.delDeg = Math.random() * 0.1; // 移动角度
}
// 如果x坐标超出画布,赋相反方向
if (this.x >= this.canvasWidth) {
this.speedX = -Math.abs(this.speedX);
}
if (this.x <= 0) {
this.speedX = Math.abs(this.speedX);
}
// 增加透明度
this.globalAlpha = 1 - this.y / this.canvasHeight;
// 移动y
this.y += this.speedY;
// 移动x坐圆弧飘落效果
this.x = this.dir
? this.x + Math.sin(this.deg)
: this.x - Math.sin(this.deg);
this.deg += this.delDeg;
// 绘制
this.draw();
}
}
const colorList = [
"#1abc9c",
"#2ecc71",
"#3498db",
"#9b59b6",
"#f1c40f",
"#e67e22",
"#e74c3c",
"#34495e",
];
export const createRandomLeaf = (num: number, ctx: any, canvasWidth: number, canvasHeight: number) => {
const temp = [];
for (let i = 0; i < num; i++) {
const x = Math.random() * canvasWidth;
const y = -Math.random() * 100;
const speedX = Math.random() - 0.5;
const speedY = Math.random() * 2 + 1;
const radiusX = Math.random() * 5 + 5;
const radiusY = Math.random() + 5;
const rotation = (Math.random() * 30) / Math.PI;
const color = colorList[Math.floor(Math.random() * 9)];
const leaf = new Leaf(
ctx,
x,
y,
radiusX,
radiusY,
speedX,
speedY,
color,
0,
rotation,
1,
canvasWidth,
canvasHeight,
);
leaf.draw();
temp.push(leaf);
}
return temp;
};
总结
以上就是完成落叶效果的步骤,有些小细点没有细扣(例如 sin函数等),具体可以百度看看,我怕我讲不清楚,误导了大家。
最终效果

致谢
谢谢各位看官老爷,有啥新奇的想法或者有啥意见,可以评论区讨论
