Canvas實(shí)現(xiàn)動態(tài)粒子文字效果的代碼示例
正文
還是先看看效果,其中有幾個(gè)要解決的難點(diǎn),后面有完整代碼。
怎么確定文字位置的粒子坐標(biāo)
怎么讓粒子的位置和文字位置的粒子坐標(biāo)對應(yī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)對應(yīng)
我們現(xiàn)在有所有粒子的位置信息
也有文字位置的粒子坐標(biāo)
,那我們怎么對應(yīng)起來呢。我們可以使用隨機(jī)數(shù)
加上map
使二者對應(yīng)。
有了生成的map映射關(guān)系
,我們就可以確定每一個(gè)粒子要到達(dá)的位置。遍歷所有粒子,存在map映射關(guān)系
的話我們就使用映射到的坐標(biāo),不存在映射關(guān)系我們使用隨機(jī)數(shù)生成。同時(shí),確定水平和豎直方向速度。
讓粒子動起來
起始坐標(biāo)和目標(biāo)坐標(biāo)有了,速度也有了,那不就剩下使用requestAnimationFrame
繪制了嗎,再加上邊界的判斷,最后在所有粒子都到達(dá)指定坐標(biāo)停止動畫就行了。
完整代碼
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 }, []) /** 繪制移動動畫*/ 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)位置,停止動畫 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]) /** 動畫*/ 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)動態(tài)粒子文字效果的代碼示例的詳細(xì)內(nèi)容,更多關(guān)于Canvas動態(tài)粒子文字的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一個(gè)JavaScript防止表單重復(fù)提交的實(shí)例
防止重復(fù)表單提交的方法有很多,本文使用JavaScript來實(shí)現(xiàn)防止表單重復(fù)提交,很簡單,但很實(shí)用,新手朋友們不要錯過2014-10-10值得分享的JavaScript實(shí)現(xiàn)圖片輪播組件
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)圖片輪播組件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11uni-app應(yīng)用配置manifest.json最全最詳細(xì)配置
這篇文章主要給大家介紹了關(guān)于uni-app應(yīng)用配置manifest.json最全最詳細(xì)配置,manifest.json文件是UniApp開發(fā)中用來配置應(yīng)用信息的重要文件,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01javascript 觸發(fā)HTML元素綁定的函數(shù)
只能觸發(fā)函數(shù)的執(zhí)行,并不能完全模擬出實(shí)際的點(diǎn)擊。2010-09-09js將json格式的對象拼接成復(fù)雜的url參數(shù)方法
下面小編就為大家?guī)硪黄猨s將json格式的對象拼接成復(fù)雜的url參數(shù)方法。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-05-05