前言
前两天在写了Canvas实现数字雨和放大镜效果和Canvas实现苹果充电盒动效后,还想试一下更炫酷的效果,经过挑选感觉动态粒子文字效果
不错,马上开始研究。
正文
还是先看看效果,其中有几个要解决的难点,后面有完整代码。
怎么确定文字位置的粒子坐标
怎么让粒子的位置和文字位置的粒子坐标对应
怎么让粒子动起来
事先准备
我们要准备几个方便自己写代码的函数,随机数、随机颜色、和绘制粒子
初始化粒子
首先得有粒子,我们使用随机数和随机颜色生成粒子,并且记录坐标信息。
怎么确定文字位置的粒子坐标
原理:用一种特殊颜色(如红色)在画布的一块区域填充文字,然后使用getImageData方法获取这一块区域每一个单位像素颜色,如果这个单位像素是标记的特殊颜色就记录其坐标。
这里是取5个像素为一个单位,是为了有颗粒感。
怎么让粒子的位置和文字位置的粒子坐标对应
我们现在有所有粒子的位置信息
也有文字位置的粒子坐标
,那我们怎么对应起来呢。我们可以使用随机数
加上map
使二者对应。
有了生成的map映射关系
,我们就可以确定每一个粒子要到达的位置。遍历所有粒子,存在map映射关系
的话我们就使用映射到的坐标,不存在映射关系我们使用随机数生成。同时,确定水平和竖直方向速度。
让粒子动起来
起始坐标和目标坐标有了,速度也有了,那不就剩下使用requestAnimationFrame
绘制了吗,再加上边界的判断,最后在所有粒子都到达指定坐标停止动画就行了。
完整代码
tsx
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import './Test.scss'
interface DotItem {
x: number,
y: number,
toX: number,
toY: number,
speedX: number,
speedY: number,
color: string,
isArrive: boolean,
}
export default function Index() {
/** 随机文字*/
const sentenceList = ['Hello World', 'Canvas', '掘金你好', '前端']
const frameDom = useRef<any>(null);
/** 粒子总数*/
const dotTotal = useRef(1200)
const canvasDom = useRef<any>(null);
const canvasCtx = useRef<any>(null);
const [height, setHeight] = useState(0)
const [width, setWidth] = useState(0)
/** 粒子信息列表*/
const allDot = useRef<DotItem[]>([])
/** 文字粒子信息*/
const textCoordinateList = useRef<{
x: number,
y: number
}[]>([])
const moveAnimation = useRef<any>(null)
/** 文字粒子和粒子全部信息的映射 */
let map = useRef(new Map());
/** 生成随机数*/
const createRandomNum = useCallback((min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
}, [])
/** 生成随机颜色*/
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
/** 绘制点*/
const pointPlot = useCallback((x: number, y: number, color: string) => {
canvasCtx.current.beginPath()
canvasCtx.current.strokeStyle = color;
canvasCtx.current.arc(x, y, 1, 0, 2 * Math.PI);
canvasCtx.current.stroke();
}, [])
/** 是否全部到达 */
const isAllArrive = useCallback(() => {
let isTrue = true
for (let i = 0; i < allDot.current.length; i++) {
if (!allDot.current[i].isArrive) {
isTrue = false
}
}
return isTrue
}, [])
/** 绘制移动动画*/
const drawMove = useCallback(() => {
canvasCtx.current.clearRect(0, 0, width, height)
for (let i = 0; i < allDot.current.length; i++) {
let { x: currentX, y: currentY, toX, toY, speedX, speedY } = allDot.current[i]
let x = 0;
let y = 0;
x = currentX + speedX
y = currentY + speedY
//边界判断
if (speedX < 0 && x < toX ||
speedX > 0 && x > toX
) {
x = toX
allDot.current[i] = {
...allDot.current[i],
isArrive: true,
}
}
if (speedY < 0 && y < toY ||
speedY > 0 && y > toY
) {
y = toY;
allDot.current[i] = {
...allDot.current[i],
isArrive: true,
}
}
pointPlot(x, y, allDot.current[i].color)
allDot.current[i] = {
...allDot.current[i],
x, y,
}
}
moveAnimation.current = requestAnimationFrame(drawMove)
//全部粒子到达目标位置,停止动画
if (isAllArrive()) {
cancelAnimationFrame(moveAnimation.current)
}
}, [width, height, isAllArrive])
/** 设置文字坐标信息*/
const setLiteralCoordinate = useCallback(() => {
let index = createRandomNum(0, sentenceList.length - 1);
let text = sentenceList[index]
canvasCtx.current.font = "120px Arial"
canvasCtx.current.fillStyle = "red"
let textWidth = canvasCtx.current.measureText(text).width;
canvasCtx.current.fillText(text, width / 2 - textWidth / 2, height / 2)
let startX = width / 2 - textWidth / 2
let endX = startX + textWidth;
let startY = height / 2 - 120;
let endY = height / 2 + 30;
//组成记录文字点的信息
textCoordinateList.current = [];
for (let i = startX; i <= endX; i += 5) {
for (let j = startY; j <= endY; j += 5) {
let imageData = canvasCtx.current.getImageData(i, j, 2, 2);
let data = imageData.data
if (data[0] == 255 && data[1] == 0 && data[2] == 0) {
textCoordinateList.current.push({
x: i,
y: j,
})
}
}
}
}, [width, height])
/** 设置点到达坐标*/
const setArrivalCoordinate = useCallback(() => {
for (let i = 0; i < allDot.current.length; i++) {
let x = 0;
let y = 0;
if (map.current.has(i)) {
x = textCoordinateList.current[map.current.get(i)].x;
y = textCoordinateList.current[map.current.get(i)].y;
} else {
x = createRandomNum(0, width)
y = createRandomNum(0, height)
}
allDot.current[i] = {
...allDot.current[i],
toX: x,
toY: y,
speedX: ((x - allDot.current[i].x) / 2000 * 17),
speedY: ((y - allDot.current[i].y) / 2000 * 17),
isArrive: false,
}
}
}, [width, height])
/** 动画*/
const onScatter = useCallback(() => {
setLiteralCoordinate()
createMap()
setArrivalCoordinate()
drawMove()
}, [height, width, drawMove, setLiteralCoordinate, setArrivalCoordinate])
/** 创建映射关系*/
const createMap = useCallback(() => {
map.current.clear()
var numbers = [];
for (var i = 0; i < allDot.current.length; i++) {
numbers.push(i);
}
var randomNumbers = [];
for (var j = 0; j < textCoordinateList.current.length; j++) {
var randomIndex = createRandomNum(0, numbers.length - 1)
randomNumbers.push(numbers[randomIndex]);
map.current.set(numbers[randomIndex], j);
numbers.splice(randomIndex, 1);
}
}, [])
/** 视口大小变化*/
const onReSize = useCallback(() => {
let { height, width } = frameDom.current.getBoundingClientRect();
setHeight(height)
setWidth(width)
}, [])
/** 初始化*/
useEffect(() => {
if (canvasDom.current === null) {
return
}
canvasCtx.current = canvasDom.current.getContext('2d')
/** 初始化*/
let { height, width } = frameDom.current.getBoundingClientRect();
setHeight(height)
setWidth(width)
}, [])
useEffect(() => {
requestAnimationFrame(() => {
for (let i = 0; i < dotTotal.current; i++) {
let x = createRandomNum(0, width)
let y = createRandomNum(0, height)
let color = getRandomColor()
pointPlot(x, y, color)
allDot.current[i] = {
x, y, color,
toX: 0,
toY: 0,
speedX: 0,
speedY: 0,
isArrive: false,
}
}
onScatter()
})
}, [onScatter, height, width])
useEffect(() => {
window.addEventListener('resize', onReSize)
return () => {
window.removeEventListener('resize', onReSize)
}
}, [onReSize])
return (
<>
<div
ref={frameDom}
onClick={onScatter}
style={{
position: 'relative',
height: '100vh',
width: '100%',
backgroundColor: "black"
}}>
<canvas
style={{
position: 'absolute',
top: 0,
left: 0,
zIndex: 2,
}}
ref={canvasDom} width={width} height={height}></canvas>
</div>
</>
)
}
结语
感兴趣的可以去试试