JavaScript實(shí)現(xiàn)文字黑洞特效的代碼詳解
簡(jiǎn)介:
在這篇教程中,我們將通過(guò) JavaScript 和 HTML5 Canvas 實(shí)現(xiàn)一個(gè)酷炫的“文字黑洞”特效。當(dāng)用戶(hù)觸摸屏幕時(shí),黑洞會(huì)吞噬周?chē)奈淖郑㈦S著手指移動(dòng);松開(kāi)手指后,文字會(huì)以爆炸效果回到原位。本文將詳細(xì)介紹如何實(shí)現(xiàn)引力效果、動(dòng)畫(huà)邏輯以及交互設(shè)計(jì),帶你一步步完成這個(gè)有趣的項(xiàng)目。
項(xiàng)目特征
1. 核心功能
- 黑洞特效:用戶(hù)觸摸屏幕時(shí),生成一個(gè)黑洞,吸引周?chē)奈淖帧?/li>
- 引力效果:文字對(duì)象會(huì)根據(jù)黑洞的位置和引力范圍被吸引。
- 文字動(dòng)畫(huà):文字圍繞黑洞旋轉(zhuǎn),并在黑洞消失后以爆炸效果回到原位。
- 交互設(shè)計(jì):黑洞會(huì)跟隨用戶(hù)手指移動(dòng),松開(kāi)手指后黑洞消失。
2. 技術(shù)亮點(diǎn)
- HTML5 Canvas:用于繪制文字、黑洞和動(dòng)畫(huà)效果。
- JavaScript 物理模擬:實(shí)現(xiàn)引力、速度和位置的計(jì)算。
- 緩動(dòng)動(dòng)畫(huà):讓文字回到原位時(shí)更加自然。
- 觸摸事件:支持移動(dòng)端觸摸交互。
實(shí)現(xiàn)邏輯
1. 項(xiàng)目結(jié)構(gòu)
- Canvas 初始化:設(shè)置畫(huà)布大小和樣式。
- 文字對(duì)象:每個(gè)字符是一個(gè)獨(dú)立對(duì)象,記錄位置、速度和狀態(tài)。
- 黑洞對(duì)象:記錄黑洞的位置、半徑和狀態(tài)。
- 動(dòng)畫(huà)循環(huán):通過(guò)
requestAnimationFrame實(shí)現(xiàn)平滑動(dòng)畫(huà)。
2. 核心邏輯
(1)文字對(duì)象的實(shí)現(xiàn)
每個(gè)文字對(duì)象(Character 類(lèi))包含以下屬性:
char:字符內(nèi)容。origX和origY:字符的原始位置。x和y:字符的當(dāng)前位置。vx和vy:字符的速度。isCaptured:是否被黑洞捕獲。angle和angularSpeed:用于實(shí)現(xiàn)圍繞黑洞旋轉(zhuǎn)的效果。
代碼實(shí)現(xiàn):
class Character {
constructor(char, x, y) {
this.char = char;
this.origX = x;
this.origY = y;
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.isCaptured = false;
this.angle = Math.random() * Math.PI * 2;
this.angularSpeed = (Math.random() - 0.5) * 0.1;
}
draw() {
ctx.fillStyle = '#000';
ctx.fillText(this.char, this.x, this.y);
}
update(blackHole) {
if (blackHole && blackHole.active) {
// 計(jì)算黑洞對(duì)字符的引力
const dx = blackHole.x - this.x;
const dy = blackHole.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < blackHole.radius * 2) {
const force = (blackHole.radius * 2 - dist) / (blackHole.radius * 2);
const angle = Math.atan2(dy, dx);
if (dist < blackHole.radius) {
this.isCaptured = true;
// 圍繞黑洞旋轉(zhuǎn)
this.angle += this.angularSpeed;
this.x = blackHole.x + Math.cos(this.angle) * blackHole.radius * 0.8;
this.y = blackHole.y + Math.sin(this.angle) * blackHole.radius * 0.8;
} else {
this.vx += Math.cos(angle) * force * 1.5;
this.vy += Math.sin(angle) * force * 1.5;
}
} else {
this.isCaptured = false;
}
} else {
// 爆炸效果:從當(dāng)前位置回到原位
if (this.isCaptured) {
this.isCaptured = false;
this.vx = (Math.random() - 0.5) * 10;
this.vy = (Math.random() - 0.5) * 10;
}
const dx = this.origX - this.x;
const dy = this.origY - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
// 緩動(dòng)回到原位
const easing = 0.1;
this.vx += dx * easing;
this.vy += dy * easing;
} else {
this.x = this.origX;
this.y = this.origY;
this.vx = 0;
this.vy = 0;
}
}
// 更新位置
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.95;
this.vy *= 0.95;
}
}
(2)黑洞對(duì)象的實(shí)現(xiàn)
黑洞對(duì)象(BlackHole 類(lèi))包含以下屬性:
x和y:黑洞的位置。radius:黑洞的半徑。targetRadius:黑洞的目標(biāo)半徑(用于動(dòng)態(tài)調(diào)整大小)。active:黑洞是否處于活動(dòng)狀態(tài)。capturedCount:被吞噬的文字?jǐn)?shù)量。
代碼實(shí)現(xiàn):
class BlackHole {
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.targetRadius = radius;
this.active = false;
this.capturedCount = 0;
}
draw() {
if (this.active) {
// 根據(jù)吞噬數(shù)量調(diào)整半徑
this.targetRadius = Math.min(100, 60 + this.capturedCount * 2);
this.radius += (this.targetRadius - this.radius) * 0.1;
// 繪制光暈效果
const gradient1 = ctx.createRadialGradient(
this.x, this.y, 0,
this.x, this.y, this.radius * 1.5
);
gradient1.addColorStop(0, 'hsla(45, 100%, 70%, 0.8)');
gradient1.addColorStop(1, 'hsla(45, 100%, 50%, 0)');
ctx.fillStyle = gradient1;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius * 1.5, 0, Math.PI * 2);
ctx.fill();
// 繪制黑洞核心
const gradient2 = ctx.createRadialGradient(
this.x, this.y, 0,
this.x, this.y, this.radius
);
gradient2.addColorStop(0, 'hsl(45, 100%, 50%)');
gradient2.addColorStop(1, 'hsla(45, 100%, 50%, 0)');
ctx.fillStyle = gradient2;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
}
}
(3)動(dòng)畫(huà)循環(huán)
通過(guò) requestAnimationFrame 實(shí)現(xiàn)動(dòng)畫(huà)循環(huán),每一幀更新文字和黑洞的狀態(tài)并重新繪制。
代碼實(shí)現(xiàn):
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新和繪制字符
let capturedCount = 0;
characters.forEach(char => {
char.update(blackHole);
char.draw();
if (char.isCaptured) capturedCount++;
});
// 更新黑洞吞噬數(shù)量
if (blackHole) blackHole.capturedCount = capturedCount;
// 繪制黑洞
if (blackHole) blackHole.draw();
requestAnimationFrame(animate);
}
animate();
html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
* { margin: 0; padding: 0; }
canvas { touch-action: none; display: block; }
</style>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
index.js:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
// 固定 Canvas 尺寸
canvas.width = 320;
canvas.height = 720;
// 文本內(nèi)容
const textContent = `dealing with fonts with multiple weights and styles is easier to assign a different font family to each of them and just use different font families, in the example's code we do show how to load different weights with the same family name, but be aware that one font file has one family, one weight and one style. Loading the font Lato doesn't grant you access to all variants of the fonts, but just one. there is one file per variant.`;
// 字符類(lèi)
class Character {
constructor(char, x, y) {
this.char = char; // 字符內(nèi)容
this.origX = x; // 原始位置
this.origY = y;
this.x = x;
this.y = y;
this.vx = 0; // 速度
this.vy = 0;
this.isCaptured = false; // 是否被黑洞捕獲
this.angle = Math.random() * Math.PI * 2; // 初始旋轉(zhuǎn)角度
this.angularSpeed = (Math.random() - 0.5) * 0.1; // 旋轉(zhuǎn)速度
}
draw() {
ctx.fillStyle = '#000';
ctx.fillText(this.char, this.x, this.y);
}
update(blackHole) {
if (blackHole && blackHole.active) {
// 計(jì)算黑洞對(duì)字符的引力
const dx = blackHole.x - this.x;
const dy = blackHole.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < blackHole.radius * 2) { // 引力范圍稍大一些
const force = (blackHole.radius * 2 - dist) / (blackHole.radius * 2); // 引力強(qiáng)度
const angle = Math.atan2(dy, dx);
if (dist < blackHole.radius) {
this.isCaptured = true;
// 圍繞黑洞旋轉(zhuǎn)
this.angle += this.angularSpeed;
this.x = blackHole.x + Math.cos(this.angle) * blackHole.radius * 0.8;
this.y = blackHole.y + Math.sin(this.angle) * blackHole.radius * 0.8;
} else {
this.vx += Math.cos(angle) * force * 1.5; // 增加引力強(qiáng)度
this.vy += Math.sin(angle) * force * 1.5;
}
} else {
this.isCaptured = false;
}
} else {
// 爆炸效果:從當(dāng)前位置回到原位
if (this.isCaptured) {
this.isCaptured = false;
// 賦予隨機(jī)速度模擬爆炸
this.vx = (Math.random() - 0.5) * 10;
this.vy = (Math.random() - 0.5) * 10;
}
const dx = this.origX - this.x;
const dy = this.origY - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
// 緩動(dòng)回到原位,減少?gòu)椞?
const easing = 0.1; // 緩動(dòng)系數(shù),控制返回速度
this.vx += dx * easing;
this.vy += dy * easing;
} else {
this.x = this.origX;
this.y = this.origY;
this.vx = 0;
this.vy = 0;
}
}
// 更新位置
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.95; // 速度衰減
this.vy *= 0.95;
}
}
// 黑洞類(lèi)
class BlackHole {
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.targetRadius = radius; // 目標(biāo)半徑
this.active = false;
this.capturedCount = 0; // 吞噬的文字對(duì)象數(shù)量
}
draw() {
if (this.active) {
// 根據(jù)吞噬數(shù)量調(diào)整半徑
this.targetRadius = Math.min(100, 60 + this.capturedCount * 2); // 最大半徑為 100
this.radius += (this.targetRadius - this.radius) * 0.1; // 緩動(dòng)調(diào)整半徑
// 繪制光暈效果
const gradient1 = ctx.createRadialGradient(
this.x, this.y, 0,
this.x, this.y, this.radius * 1.5
);
gradient1.addColorStop(0, 'hsla(45, 100%, 70%, 0.8)'); // 光暈內(nèi)圈
gradient1.addColorStop(1, 'hsla(45, 100%, 50%, 0)'); // 光暈外圈
ctx.fillStyle = gradient1;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius * 1.5, 0, Math.PI * 2);
ctx.fill();
// 繪制黑洞核心
const gradient2 = ctx.createRadialGradient(
this.x, this.y, 0,
this.x, this.y, this.radius
);
gradient2.addColorStop(0, 'hsl(45, 100%, 50%)'); // 核心顏色
gradient2.addColorStop(1, 'hsla(45, 100%, 50%, 0)'); // 漸變透明
ctx.fillStyle = gradient2;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
}
}
// 生成字符對(duì)象
const characters = [];
const fontSize = 14;
const lineHeight = 20;
const maxWidth = canvas.width - 20; // 留出邊距
ctx.font = `${fontSize}px Arial`;
let x = 10, y = 30; // 初始位置
for (const char of textContent) {
if (x + ctx.measureText(char).width > maxWidth || char === '\n') {
x = 10;
y += lineHeight;
}
if (char !== '\n') {
characters.push(new Character(char, x, y));
x += ctx.measureText(char).width;
}
}
let blackHole = null;
// 觸摸事件處理
canvas.addEventListener('touchstart', (e) => {
const { clientX, clientY } = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
blackHole = new BlackHole(x, y, 60); // 黑洞初始半徑 60
blackHole.active = true;
});
canvas.addEventListener('touchmove', (e) => {
if (blackHole) {
const { clientX, clientY } = e.touches[0];
const rect = canvas.getBoundingClientRect();
blackHole.x = clientX - rect.left;
blackHole.y = clientY - rect.top;
}
});
canvas.addEventListener('touchend', () => {
if (blackHole) blackHole.active = false;
});
// 動(dòng)畫(huà)循環(huán)
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新和繪制字符
let capturedCount = 0;
characters.forEach(char => {
char.update(blackHole);
char.draw();
if (char.isCaptured) capturedCount++;
});
// 更新黑洞吞噬數(shù)量
if (blackHole) blackHole.capturedCount = capturedCount;
// 繪制黑洞
if (blackHole) blackHole.draw();
requestAnimationFrame(animate);
}
animate();
總結(jié)
通過(guò)這篇教程,我們實(shí)現(xiàn)了一個(gè)基于 JavaScript 和 Canvas 的文字黑洞特效。核心邏輯包括:
- 引力效果:通過(guò)計(jì)算字符與黑洞的距離和角度,模擬引力作用。
- 動(dòng)畫(huà)邏輯:使用緩動(dòng)函數(shù)和隨機(jī)速度實(shí)現(xiàn)文字回到原位的爆炸效果。
- 交互設(shè)計(jì):通過(guò)觸摸事件實(shí)現(xiàn)黑洞的生成和移動(dòng)。
以上就是使用JavaScript實(shí)現(xiàn)文字黑洞特效的代碼詳解的詳細(xì)內(nèi)容,更多關(guān)于JavaScript文字黑洞特效的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
前端本地?cái)?shù)據(jù)存儲(chǔ)的幾種常見(jiàn)方式總結(jié)
在前端開(kāi)發(fā)中,本地?cái)?shù)據(jù)存儲(chǔ)是實(shí)現(xiàn)客戶(hù)端數(shù)據(jù)持久化的關(guān)鍵技術(shù),以下是幾種常見(jiàn)的前端本地?cái)?shù)據(jù)存儲(chǔ)方式,通過(guò)代碼示例講解的非常詳細(xì),需要的朋友可以參考下2025-01-01
javascript 使用 NodeList需要注意的問(wèn)題
理解NodeList及其近親NamedNodeMap和HTMLCollection,是從整體上透徹理解DOM的關(guān)鍵所在,這三個(gè)集合都是“動(dòng)態(tài)的”,換句話(huà)說(shuō),每當(dāng)文檔結(jié)構(gòu)發(fā)生變化時(shí),他們都會(huì)得到更新。2013-03-03
JScript實(shí)現(xiàn)表格的簡(jiǎn)單操作
這篇文章主要為大家詳細(xì)介紹了JScript實(shí)現(xiàn)簡(jiǎn)單的表格操作,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
為什么JavaScript中0.1 + 0.2 != 0.3
這篇文章主要給大家介紹了關(guān)于為什么JavaScript中0.1 + 0.2 != 0.3的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12

