javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript文字黑洞特效

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

作者:hy_2095

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

简介:

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

项目特征

1. 核心功能

2. 技术亮点

实现逻辑

1. 项目结构

2. 核心逻辑

(1)文字对象的实现

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

代码实现:

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

代码实现:

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

您可能感兴趣的文章:
阅读全文