JavaScript实现文字黑洞特效的代码详解

 更新时间:2025年03月03日 09:32:22   作者:hy_2095  
这篇文章介绍了用 JavaScript 和 HTML5 Canvas 实现文字黑洞特效的项目,包括项目特征,如黑洞特效、引力效果、文字动画和交互设计,以及技术亮点,还阐述了实现逻辑,涵盖项目结构、文字和黑洞对象的实现、动画循环等,并给出了详细的代码示例和解释

简介:

在这篇教程中,我们将通过 JavaScript 和 HTML5 Canvas 实现一个酷炫的“文字黑洞”特效。当用户触摸屏幕时,黑洞会吞噬周围的文字,并随着手指移动;松开手指后,文字会以爆炸效果回到原位。本文将详细介绍如何实现引力效果、动画逻辑以及交互设计,带你一步步完成这个有趣的项目。

项目特征

1. 核心功能

  • 黑洞特效:用户触摸屏幕时,生成一个黑洞,吸引周围的文字。
  • 引力效果:文字对象会根据黑洞的位置和引力范围被吸引。
  • 文字动画:文字围绕黑洞旋转,并在黑洞消失后以爆炸效果回到原位。
  • 交互设计:黑洞会跟随用户手指移动,松开手指后黑洞消失。

2. 技术亮点

  • HTML5 Canvas:用于绘制文字、黑洞和动画效果。
  • JavaScript 物理模拟:实现引力、速度和位置的计算。
  • 缓动动画:让文字回到原位时更加自然。
  • 触摸事件:支持移动端触摸交互。

实现逻辑

1. 项目结构

  • Canvas 初始化:设置画布大小和样式。
  • 文字对象:每个字符是一个独立对象,记录位置、速度和状态。
  • 黑洞对象:记录黑洞的位置、半径和状态。
  • 动画循环:通过 requestAnimationFrame 实现平滑动画。

2. 核心逻辑

(1)文字对象的实现

每个文字对象(Character 类)包含以下属性:

  • char:字符内容。
  • origXorigY:字符的原始位置。
  • xy:字符的当前位置。
  • vxvy:字符的速度。
  • isCaptured:是否被黑洞捕获。
  • angleangularSpeed:用于实现围绕黑洞旋转的效果。

代码实现:

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 类)包含以下属性:

  • xy:黑洞的位置。
  • 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文字黑洞特效的资料请关注脚本之家其它相关文章!

相关文章

  • JavaScript限定复选框的选择个数示例代码

    JavaScript限定复选框的选择个数示例代码

    有10个复选框,用户最多只能勾选3个,否则就灰掉所有复选框,具体实现思路及代码如下,感兴趣的朋友可以参考下,希望对大家有所帮助
    2013-08-08
  • 使用canvas实现仿新浪微博头像截取上传功能

    使用canvas实现仿新浪微博头像截取上传功能

    用户提供图像大小尺寸不合适,如何用截取上传呢?接下来小编教大家使用使用canvas实现仿新浪微博头像截取上传功能解决问题,需要的朋友一起学习吧。
    2015-09-09
  • JS 非图片动态loading效果实现代码

    JS 非图片动态loading效果实现代码

    功能说明:譬如在按某个button时,显示消息"Loading”,然后每隔一秒后后面加上".",至一定数量的"."时如:"Loading...",再重置此消息为"Loading",继续动态显示,直至按钮事件处理完成。
    2010-04-04
  • js 关键词高亮(根据ID/tag高亮关键字)案例介绍

    js 关键词高亮(根据ID/tag高亮关键字)案例介绍

    关键词高亮在开发中会带来很多的方便,关键词高亮包括:根据ID高亮关键字/根据Tag名高亮关键字等等,感兴趣的朋友可以了解下,希望本文对你有所帮助
    2013-01-01
  • js中关于base64编码的问题

    js中关于base64编码的问题

    这篇文章主要介绍了js中关于base64编码的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • js实现iframe自动自适应高度的方法

    js实现iframe自动自适应高度的方法

    这篇文章主要介绍了js实现iframe自动自适应高度的方法,涉及javascript操作iframe框架的技巧,非常具有实用价值,需要的朋友可以参考下
    2015-02-02
  • JavaScript实现九宫格抽奖功能的示例代码

    JavaScript实现九宫格抽奖功能的示例代码

    这篇文章主要为大家介绍了如何利用JavaScript实现九宫格抽奖功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02
  • js实现鼠标经过表格行变色的方法

    js实现鼠标经过表格行变色的方法

    这篇文章主要介绍了js实现鼠标经过表格行变色的方法,涉及javascript表格节点样式及鼠标事件的相关操作技巧,需要的朋友可以参考下
    2015-05-05
  • 挺实用的20个JavaScript简化写法代码技巧

    挺实用的20个JavaScript简化写法代码技巧

    掌握一些JavaScript的精简书写方式,有助增强代码的阅读性,提升代码质量,任何一种编程语言的简写小技巧都是为了帮助你写出更简洁、更完善的代码,让你用更少的编码实现你的需求
    2023-08-08
  • JS使用oumousemove和oumouseout动态改变图片显示的方法

    JS使用oumousemove和oumouseout动态改变图片显示的方法

    这篇文章主要介绍了JS使用oumousemove和oumouseout动态改变图片显示的方法,涉及javascript鼠标事件及图片操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-03-03

最新评论