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
这是为了:
- 提高性能 - 不需要检查每个像素点
- 减少粒子数量 - 避免粒子过于密集
- 保持文字形状 - 即使跳过一些像素也能保持文字轮廓
图解说明
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. 参数控制
- 可以调整粒子数量(影响文字的清晰度)
- 可以改变粒子大小(影响视觉效果)
- 可以选择粒子颜色(改变整体风格)
总的来说,这个效果的核心就是:先用像素画出文字,再用粒子替换这些像素点。这样就实现了用粒子组成文字的效果。
