JavaScript实现文字黑洞特效的代码详解
作者:hy_2095
这篇文章介绍了用 JavaScript 和 HTML5 Canvas 实现文字黑洞特效的项目,包括项目特征,如黑洞特效、引力效果、文字动画和交互设计,以及技术亮点,还阐述了实现逻辑,涵盖项目结构、文字和黑洞对象的实现、动画循环等,并给出了详细的代码示例和解释
简介:
在这篇教程中,我们将通过 JavaScript 和 HTML5 Canvas 实现一个酷炫的“文字黑洞”特效。当用户触摸屏幕时,黑洞会吞噬周围的文字,并随着手指移动;松开手指后,文字会以爆炸效果回到原位。本文将详细介绍如何实现引力效果、动画逻辑以及交互设计,带你一步步完成这个有趣的项目。
项目特征
1. 核心功能
- 黑洞特效:用户触摸屏幕时,生成一个黑洞,吸引周围的文字。
- 引力效果:文字对象会根据黑洞的位置和引力范围被吸引。
- 文字动画:文字围绕黑洞旋转,并在黑洞消失后以爆炸效果回到原位。
- 交互设计:黑洞会跟随用户手指移动,松开手指后黑洞消失。
2. 技术亮点
- HTML5 Canvas:用于绘制文字、黑洞和动画效果。
- JavaScript 物理模拟:实现引力、速度和位置的计算。
- 缓动动画:让文字回到原位时更加自然。
- 触摸事件:支持移动端触摸交互。
实现逻辑
1. 项目结构
- Canvas 初始化:设置画布大小和样式。
- 文字对象:每个字符是一个独立对象,记录位置、速度和状态。
- 黑洞对象:记录黑洞的位置、半径和状态。
- 动画循环:通过
requestAnimationFrame
实现平滑动画。
2. 核心逻辑
(1)文字对象的实现
每个文字对象(Character
类)包含以下属性:
char
:字符内容。origX
和origY
:字符的原始位置。x
和y
:字符的当前位置。vx
和vy
:字符的速度。isCaptured
:是否被黑洞捕获。angle
和angularSpeed
:用于实现围绕黑洞旋转的效果。
代码实现:
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) { // 计算黑洞对字符的引力 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; // 围绕黑洞旋转 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 { // 爆炸效果:从当前位置回到原位 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) { // 缓动回到原位 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)黑洞对象的实现
黑洞对象(BlackHole
类)包含以下属性:
x
和y
:黑洞的位置。radius
:黑洞的半径。targetRadius
:黑洞的目标半径(用于动态调整大小)。active
:黑洞是否处于活动状态。capturedCount
:被吞噬的文字数量。
代码实现:
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) { // 根据吞噬数量调整半径 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)动画循环
通过 requestAnimationFrame
实现动画循环,每一帧更新文字和黑洞的状态并重新绘制。
代码实现:
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++; }); // 更新黑洞吞噬数量 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; // 文本内容 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.`; // 字符类 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) { // 计算黑洞对字符的引力 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; // 围绕黑洞旋转 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 { // 爆炸效果:从当前位置回到原位 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) { // 缓动回到原位,减少弹跳力 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; } } // 黑洞类 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) { // 根据吞噬数量调整半径 this.targetRadius = Math.min(100, 60 + this.capturedCount * 2); // 最大半径为 100 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(); } } } // 生成字符对象 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; }); // 动画循环 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++; }); // 更新黑洞吞噬数量 if (blackHole) blackHole.capturedCount = capturedCount; // 绘制黑洞 if (blackHole) blackHole.draw(); requestAnimationFrame(animate); } animate();
总结
通过这篇教程,我们实现了一个基于 JavaScript 和 Canvas 的文字黑洞特效。核心逻辑包括:
- 引力效果:通过计算字符与黑洞的距离和角度,模拟引力作用。
- 动画逻辑:使用缓动函数和随机速度实现文字回到原位的爆炸效果。
- 交互设计:通过触摸事件实现黑洞的生成和移动。
以上就是使用JavaScript实现文字黑洞特效的代码详解的详细内容,更多关于JavaScript文字黑洞特效的资料请关注脚本之家其它相关文章!