粒子形成文字

javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <!-- 页面基本设置 -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>静态粒子组成文字效果</title>
    <!-- 页面样式设置 -->
    <style>
        /* 页面基本样式 */
        body {
            margin: 0; /* 页面边距为0 */
            padding: 0; /* 页面内边距为0 */
            background: #000; /* 背景颜色为黑色 */
            overflow: hidden; /* 隐藏滚动条 */
            display: flex; /* 使用弹性布局 */
            justify-content: center; /* 水平居中 */
            align-items: center; /* 垂直居中 */
            height: 100vh; /* 高度为视窗高度 */
            font-family: Arial, sans-serif; /* 字体设置 */
        }
        /* 画布样式 */
        canvas {
            display: block; /* 画布显示为块级元素 */
            background: #000; /* 画布背景为黑色 */
        }
        /* 控制面板样式 */
        .controls {
            position: absolute; /* 绝对定位 */
            top: 20px; /* 距离顶部20像素 */
            left: 20px; /* 距离左边20像素 */
            color: white; /* 文字颜色为白色 */
            z-index: 100; /* 层级设置 */
        }
        /* 输入框和按钮样式 */
        input, button {
            margin: 5px; /* 外边距5像素 */
            padding: 5px; /* 内边距5像素 */
        }
    </style>
</head>
<body>
    <!-- 控制面板 -->
    <div class="controls">
        <!-- 文字输入框 -->
        <input type="text" id="textInput" value="123" maxlength="10">
        <!-- 更新文字按钮 -->
        <button onclick="updateText()">更新文字</button>
        <!-- 粒子数量设置 -->
        <div>
            <label>粒子数量: <input type="number" id="particleCount" value="2000" min="500" max="5000" step="500"></label>
        </div>
        <!-- 粒子大小设置 -->
        <div>
            <label>粒子大小: <input type="number" id="particleSize" value="2" min="1" max="5" step="0.5"></label>
        </div>
        <!-- 粒子颜色设置 -->
        <div>
            <label>颜色: <input type="color" id="particleColor" value="#00ffff"></label>
        </div>
    </div>
    <!-- 画布元素 -->
    <canvas id="canvas"></canvas>

    <script>
        // 获取画布元素
        const canvas = document.getElementById('canvas');
        // 获取2D绘图上下文
        const ctx = canvas.getContext('2d');
        
        // 设置画布大小函数
        function resizeCanvas() {
            canvas.width = window.innerWidth;  // 画布宽度设置为窗口宽度
            canvas.height = window.innerHeight; // 画布高度设置为窗口高度
        }
        
        // 监听窗口大小变化事件,自动调整画布大小
        window.addEventListener('resize', resizeCanvas);
        // 初始设置画布大小
        resizeCanvas();
        
        // 粒子类定义
        class Particle {
            // 构造函数,创建粒子时调用
            constructor(x, y, size, color) {
                this.x = x;        // 粒子的X坐标
                this.y = y;        // 粒子的Y坐标
                this.size = size;  // 粒子大小
                this.color = color; // 粒子颜色
            }
            
            // 绘制粒子方法
            draw() {
                ctx.fillStyle = this.color; // 设置填充颜色
                ctx.beginPath(); // 开始绘制路径
                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); // 绘制圆形
                ctx.fill(); // 填充圆形
            }
        }
        
        // 存储粒子的数组
        let particles = [];
        
        // 创建文字粒子函数
        function createTextParticles() {
            // 获取用户输入的文字
            const text = document.getElementById('textInput').value;
            // 如果没有文字则返回
            if (!text) return;
            
            // 清空画布并绘制文字
            ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空画布
            ctx.font = 'bold 120px Arial'; // 设置字体样式
            ctx.textAlign = 'center'; // 文字水平居中对齐
            ctx.textBaseline = 'middle'; // 文字垂直居中对齐
            ctx.fillStyle = 'white'; // 设置文字颜色为白色
            ctx.fillText(text, canvas.width / 2, canvas.height / 2); // 在画布中心绘制文字
            
            // 获取文字像素数据
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 获取画布上的像素数据
            const data = imageData.data; // 获取像素数据数组
            
            // 清空粒子数组
            particles = [];
            
            // 获取粒子数量和大小
            const particleCount = parseInt(document.getElementById('particleCount').value); // 获取粒子数量
            const particleSize = parseFloat(document.getElementById('particleSize').value); // 获取粒子大小
            const particleColor = document.getElementById('particleColor').value; // 获取粒子颜色
            
            // 获取文字中的所有点
            const textPoints = []; // 存储文字像素点的数组
            // 遍历画布上的每个像素点
            for (let y = 0; y < canvas.height; y += 2) { // Y轴,每隔2像素检查一次
                for (let x = 0; x < canvas.width; x += 2) { // X轴,每隔2像素检查一次
                    const index = (y * canvas.width + x) * 4; // 计算像素在数组中的索引
                    // 检查该像素点是否属于文字(透明度大于128)
                    if (data[index + 3] > 128) { // 检查透明度
                        textPoints.push({x, y}); // 将坐标添加到数组中
                    }
                }
            }
            
            // 随机选择一部分像素点作为粒子位置
            const selectedPoints = []; // 存储选中的点
            // 从所有文字点中随机选择指定数量的点
            for (let i = 0; i < Math.min(particleCount, textPoints.length); i++) {
                const randomIndex = Math.floor(Math.random() * textPoints.length); // 随机索引
                selectedPoints.push(textPoints[randomIndex]); // 添加到选中的点数组
            }
            
            // 创建粒子
            for (let i = 0; i < selectedPoints.length; i++) {
                const point = selectedPoints[i]; // 获取选中的点
                // 创建粒子对象并添加到粒子数组
                particles.push(new Particle(point.x, point.y, particleSize, particleColor));
            }
            
            // 清空画布,准备绘制粒子
            ctx.clearRect(0, 0, canvas.width, canvas.height);
        }
        
        // 更新文字函数
        function updateText() {
            createTextParticles(); // 创建文字粒子
            drawParticles(); // 绘制粒子
        }
        
        // 绘制粒子函数
        function drawParticles() {
            // 清空画布,设置背景为黑色
            ctx.fillStyle = 'rgba(0, 0, 0, 1)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // 绘制所有粒子
            for (let i = 0; i < particles.length; i++) {
                particles[i].draw(); // 调用粒子的绘制方法
            }
        }
        
        // 初始化
        createTextParticles(); // 创建文字粒子
        drawParticles(); // 绘制粒子
        
        // 鼠标移动事件监听器,实现互动效果
        canvas.addEventListener('mousemove', (e) => {
            // 获取鼠标相对于画布的坐标
            const rect = canvas.getBoundingClientRect(); // 获取画布位置信息
            const mouseX = e.clientX - rect.left; // 鼠标X坐标
            const mouseY = e.clientY - rect.top; // 鼠标Y坐标
            
            // 清空画布
            ctx.fillStyle = 'rgba(0, 0, 0, 1)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // 遍历所有粒子
            for (let i = 0; i < particles.length; i++) {
                // 计算粒子与鼠标之间的距离
                const dx = particles[i].x - mouseX; // X方向距离
                const dy = particles[i].y - mouseY; // Y方向距离
                const distance = Math.sqrt(dx * dx + dy * dy); // 计算实际距离
                
                // 如果粒子距离鼠标小于100像素
                if (distance < 100) {
                    // 让粒子远离鼠标
                    const angle = Math.atan2(dy, dx); // 计算角度
                    const force = (100 - distance) / 50; // 计算推力
                    const newX = particles[i].x + Math.cos(angle) * force; // 新的X坐标
                    const newY = particles[i].y + Math.sin(angle) * force; // 新的Y坐标
                    
                    // 绘制粒子在新位置
                    ctx.fillStyle = particles[i].color; // 设置颜色
                    ctx.beginPath(); // 开始绘制路径
                    ctx.arc(newX, newY, particles[i].size, 0, Math.PI * 2); // 绘制圆形
                    ctx.fill(); // 填充圆形
                } else {
                    // 绘制粒子在原始位置
                    particles[i].draw(); // 调用粒子的绘制方法
                }
            }
        });
        
        // 鼠标离开画布时的事件处理
        canvas.addEventListener('mouseleave', () => {
            drawParticles(); // 重新绘制静态粒子
        });
    </script>
</body>
</html>

让我用通俗易懂的话来解释这个粒子组成文字的实现思路:

实现思路

1. 基本原理

想象一下,我们要用很多小点(粒子)来"拼"出"123"这三个字。这就像用很多小星星拼出图案一样。

2. 实现步骤

第一步:画出文字轮廓

  • 我们先在画布上用白色画出"俞超群"这三个字
  • 这时文字是实心的,就像一个模板

第二步:提取文字位置

  • 我们检查画布上的每一个像素点
  • 如果这个点是白色(文字部分),就记住它的坐标
  • 这样我们就得到了所有属于文字的坐标点

第三步:创建粒子

  • 从所有属于文字的坐标点中,随机选择一部分(比如2000个点)
  • 在这些坐标位置上,放上我们想要的粒子(小圆点)
  • 每个粒子都有自己的颜色、大小

第四步:显示粒子

  • 把所有粒子按照它们在文字中的位置画出来
  • 这样就形成了用粒子组成的文字

3. 代码实现的关键点

粒子类(Particle)

javascript 复制代码
class Particle {
    constructor(x, y, size, color) {
        this.x = x;        // 粒子的X坐标
        this.y = y;        // 粒子的Y坐标
        this.size = size;  // 粒子大小
        this.color = color; // 粒子颜色
    }
}

获取文字像素

javascript 复制代码
// 先画出文字
ctx.fillText(text, canvas.width / 2, canvas.height / 2);

// 获取所有像素信息
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// 检查每个像素是否属于文字
if (data[index + 3] > 128) { // 透明度大于128的就是文字部分
    // 记录这个点的坐标
}

让我详细解释这段代码的工作原理:

javascript 复制代码
  for (let y = 0; y < canvas.height; y += 2) { // Y轴,每隔2像素检查一次
                for (let x = 0; x < canvas.width; x += 2) { // X轴,每隔2像素检查一次
                    const index = (y * canvas.width + x) * 4; // 计算像素在数组中的索引
                    // 检查该像素点是否属于文字(透明度大于128)
                    if (data[index + 3] > 128) { // 检查透明度
                        textPoints.push({x, y}); // 将坐标添加到数组中
                    }
                }
            }

画布像素数据的存储方式

当你获取画布的像素数据时,Canvas API会返回一个一维数组,但实际上是按二维网格存储的。每个像素由4个值表示:

  • data[index + 0] - 红色值 (Red)
  • data[index + 1] - 绿色值 (Green)
  • data[index + 2] - 蓝色值 (Blue)
  • data[index + 3] - 透明度值 (Alpha)

为什么用这个公式计算索引

ini 复制代码
const index = (y * canvas.width + x) * 4;

让我用一个具体例子来说明:

假设画布是 5x5 像素的(实际很小,便于理解):

rust 复制代码
坐标: (x, y) -> 索引位置
(0,0) -> 索引 0
(1,0) -> 索引 4  
(2,0) -> 索引 8
(0,1) -> 索引 20 (5*4)
(1,1) -> 索引 24 (5*4+4)

计算过程:

  • (y * canvas.width + x) 计算的是第几个像素
  • 乘以4是因为每个像素占4个数组位置

举例说明:

  • 如果在第2行第3列 (x=3, y=2),画布宽度为100
  • 第2行第3列是第 (2 * 100 + 3) = 203 个像素
  • 在数组中的起始位置是 203 * 4 = 812
  • 所以这个像素的颜色数据在数组的:
    • data[812] - 红色值
    • data[813] - 绿色值
    • data[814] - 蓝色值
    • data[815] - 透明度值

为什么要加3

kotlin 复制代码
if (data[index + 3] > 128) // 检查透明度
  • index 是当前像素的起始位置
  • index + 3 就是透明度值的位置
  • 透明度值范围是 0-255:
    • 0 = 完全透明
    • 255 = 完全不透明
  • 128 是一个中间值,大于128就认为是文字部分

为什么每隔2像素检查

makefile 复制代码
y += 2 和 x += 2

这是为了:

  1. 提高性能 - 不需要检查每个像素点
  2. 减少粒子数量 - 避免粒子过于密集
  3. 保持文字形状 - 即使跳过一些像素也能保持文字轮廓

图解说明

ini 复制代码
实际像素位置:    数组中的存储方式:
[0,0][0,1][0,2]   [R,G,B,A,R,G,B,A,R,G,B,A,...]
[1,0][1,1][1,2]   
[2,0][2,1][2,2]   

所以 (y * width + x) * 4 就是找到指定坐标的像素在数组中的起始位置。

4. 互动功能

  • 当鼠标移动时,靠近鼠标的一些粒子会暂时"躲开"
  • 鼠标离开后,所有粒子回到原来的位置
  • 这样就实现了简单的互动效果

5. 参数控制

  • 可以调整粒子数量(影响文字的清晰度)
  • 可以改变粒子大小(影响视觉效果)
  • 可以选择粒子颜色(改变整体风格)

总的来说,这个效果的核心就是:先用像素画出文字,再用粒子替换这些像素点。这样就实现了用粒子组成文字的效果。

相关推荐
Kayshen1 小时前
春节期间我们开源了一个 AI-Native 的矢量设计工具,对标 Pencil.dev,让 AI Agent 直接画 UI
前端·aigc·agent
没想好d1 小时前
通用管理后台组件库-6-头部导航组件
前端
linux_cfan1 小时前
打造智慧校园视听新基建:高校与在线教育平台 Web 视频播放器选型指南 (2026版)
前端·学习·音视频·教育电商
JYeontu1 小时前
实现一个超萌的柯基交互输入框
前端
天蓝色的鱼鱼2 小时前
Vite 8:从“混动”到“纯电”,构建性能提升10倍+
前端·vite
dreams_dream2 小时前
XSS类型
前端·xss
wuhen_n2 小时前
副作用的概念与effect基础:Vue3响应式系统的核心
前端·javascript·vue.js
张3蜂2 小时前
Vue.js-知识体系
前端·javascript·vue.js
Cache技术分享2 小时前
333. Java Stream API - 按年份找出合作最多的作者对:避免 Optional.orElseThrow() 的风险
前端·后端