Vue3+不正规ts实现不正规的落叶飘落效果

大家好,今天我向大家分享一个基于 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)位置,半径分别是radiusXradiusY ,按照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函数等),具体可以百度看看,我怕我讲不清楚,误导了大家。

最终效果

致谢

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

相关推荐
Larcher16 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐28 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭40 分钟前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu2 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花2 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋2 小时前
场景模拟:基础路由配置
前端
六月的可乐2 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程