Canvas實(shí)現(xiàn)動(dòng)態(tài)粒子文字效果的代碼示例
正文
還是先看看效果,其中有幾個(gè)要解決的難點(diǎn),后面有完整代碼。
怎么確定文字位置的粒子坐標(biāo)怎么讓粒子的位置和文字位置的粒子坐標(biāo)對(duì)應(yīng)怎么讓粒子動(dòng)起來

事先準(zhǔn)備
我們要準(zhǔn)備幾個(gè)方便自己寫代碼的函數(shù),隨機(jī)數(shù)、隨機(jī)顏色、和繪制粒子

初始化粒子
首先得有粒子,我們使用隨機(jī)數(shù)和隨機(jī)顏色生成粒子,并且記錄坐標(biāo)信息。

怎么確定文字位置的粒子坐標(biāo)
原理:用一種特殊顏色(如紅色)在畫布的一塊區(qū)域填充文字,然后使用getImageData方法獲取這一塊區(qū)域每一個(gè)單位像素顏色,如果這個(gè)單位像素是標(biāo)記的特殊顏色就記錄其坐標(biāo)。
這里是取5個(gè)像素為一個(gè)單位,是為了有顆粒感。

怎么讓粒子的位置和文字位置的粒子坐標(biāo)對(duì)應(yīng)
我們現(xiàn)在有所有粒子的位置信息也有文字位置的粒子坐標(biāo),那我們?cè)趺磳?duì)應(yīng)起來呢。我們可以使用隨機(jī)數(shù)加上map使二者對(duì)應(yīng)。

有了生成的map映射關(guān)系,我們就可以確定每一個(gè)粒子要到達(dá)的位置。遍歷所有粒子,存在map映射關(guān)系的話我們就使用映射到的坐標(biāo),不存在映射關(guān)系我們使用隨機(jī)數(shù)生成。同時(shí),確定水平和豎直方向速度。

讓粒子動(dòng)起來
起始坐標(biāo)和目標(biāo)坐標(biāo)有了,速度也有了,那不就剩下使用requestAnimationFrame繪制了嗎,再加上邊界的判斷,最后在所有粒子都到達(dá)指定坐標(biāo)停止動(dòng)畫就行了。

完整代碼
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() {
/** 隨機(jī)文字*/
const sentenceList = ['Hello World', 'Canvas', '掘金你好', '前端']
const frameDom = useRef<any>(null);
/** 粒子總數(shù)*/
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());
/** 生成隨機(jī)數(shù)*/
const createRandomNum = useCallback((min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
}, [])
/** 生成隨機(jī)顏色*/
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
/** 繪制點(diǎn)*/
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();
}, [])
/** 是否全部到達(dá) */
const isAllArrive = useCallback(() => {
let isTrue = true
for (let i = 0; i < allDot.current.length; i++) {
if (!allDot.current[i].isArrive) {
isTrue = false
}
}
return isTrue
}, [])
/** 繪制移動(dòng)動(dòng)畫*/
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)
//全部粒子到達(dá)目標(biāo)位置,停止動(dòng)畫
if (isAllArrive()) {
cancelAnimationFrame(moveAnimation.current)
}
}, [width, height, isAllArrive])
/** 設(shè)置文字坐標(biāo)信息*/
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;
//組成記錄文字點(diǎn)的信息
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])
/** 設(shè)置點(diǎn)到達(dá)坐標(biāo)*/
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])
/** 動(dòng)畫*/
const onScatter = useCallback(() => {
setLiteralCoordinate()
createMap()
setArrivalCoordinate()
drawMove()
}, [height, width, drawMove, setLiteralCoordinate, setArrivalCoordinate])
/** 創(chuàng)建映射關(guān)系*/
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>
</>
)
}結(jié)語
感興趣的可以去試試
以上就是Canvas實(shí)現(xiàn)動(dòng)態(tài)粒子文字效果的代碼示例的詳細(xì)內(nèi)容,更多關(guān)于Canvas動(dòng)態(tài)粒子文字的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript中call,apply,bind的區(qū)別詳解
這篇文章主要介紹了javascript中call,apply,bind的區(qū)別詳解,幫助大家更好的理解和使用JavaScript,感興趣的朋友可以了解下2020-12-12
Layui表格行內(nèi)動(dòng)態(tài)編輯數(shù)據(jù)
本文主要介紹經(jīng)典前端框架 layui 中的動(dòng)態(tài)表格數(shù)據(jù)操作,結(jié)合 JQuery 動(dòng)態(tài)編輯單元格中的數(shù)據(jù),具有一定的參考價(jià)值,感興趣的可以了解一下2021-08-08
微信小程序自定義菜單切換欄tabbar組件代碼實(shí)例
這篇文章主要介紹了微信小程序自定義菜單切換欄tabbar組件代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
簡(jiǎn)單談?wù)刯avascript代碼復(fù)用模式
這篇文章主要簡(jiǎn)單談?wù)刯avascript代碼復(fù)用模式,主要詳細(xì)介紹了類式繼承模式中的默認(rèn)模式,希望大家能夠喜歡。2015-01-01
維護(hù)loading加載狀態(tài)的幾個(gè)方法小結(jié)
在項(xiàng)目開發(fā)中,當(dāng)頁面請(qǐng)求接口時(shí),組件局部或者頁面全局顯示loading加載遮罩可謂是司空見慣了,下面來討論一下如何優(yōu)雅的使用loading狀態(tài),文中通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09
JavaScript圖像放大鏡效果實(shí)現(xiàn)方法詳解
這篇文章主要介紹了JavaScript圖像放大鏡效果實(shí)現(xiàn)方法詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
靜態(tài)的動(dòng)態(tài)續(xù)篇之來點(diǎn)XML
靜態(tài)的動(dòng)態(tài)續(xù)篇之來點(diǎn)XML...2006-12-12

