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函数等),具体可以百度看看,我怕我讲不清楚,误导了大家。

最终效果

致谢

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

相关推荐
moxiaoran57533 小时前
uni-app萌宠案例学习笔记--页面布局和CSS样式设置
前端·css·uni-app
CrissChan4 小时前
Pycharm 函数注释
java·前端·pycharm
小小小小宇4 小时前
Vue.nextTick()笔记
前端
小约翰仓鼠6 小时前
vue3子组件获取并修改父组件的值
前端·javascript·vue.js
Lin Hsüeh-ch'in6 小时前
Vue 学习路线图(从零到实战)
前端·vue.js·学习
烛阴6 小时前
bignumber.js深度解析:驾驭任意精度计算的终极武器
前端·javascript·后端
计蒙不吃鱼6 小时前
一篇文章实现Android图片拼接并保存至相册
android·java·前端
全职计算机毕业设计6 小时前
基于Java Web的校园失物招领平台设计与实现
java·开发语言·前端
啊~哈7 小时前
vue3+elementplus表格表头加图标及文字提示
前端·javascript·vue.js