three/文字爆裂效果
皮蛋众妙真君.html
ini
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 文字粒子动画</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
}
#loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-family: sans-serif;
font-size: 24px;
background-color: rgba(0, 0, 0, 0.7);
z-index: 100;
}
</style>
</head>
<body>
<div id="loading-overlay">
<div id="loading-text">正在生成文字粒子...</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
let scene, camera, renderer;
let textParticles, stars;
let mouse = new THREE.Vector2(-100, -100);
const clock = new THREE.Clock();
const explosionDelay = 2.0; // 粒子组成文字后停留2秒
let hasExploded = false;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 100;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
createStars();
createTextParticles();
window.addEventListener('resize', onWindowResize);
window.addEventListener('mousemove', onMouseMove);
}
function createStars() {
const starGeometry = new THREE.BufferGeometry();
const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.5, transparent: true, opacity: 0.8 });
const starVertices = [];
for (let i = 0; i < 10000; i++) {
const x = (Math.random() - 0.5) * 2000;
const y = (Math.random() - 0.5) * 2000;
const z = (Math.random() - 0.5) * 2000;
starVertices.push(x, y, z);
}
starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3));
stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
}
// 核心改动:使用 Canvas 生成文字并采样粒子
function createTextParticles() {
const loadingOverlay = document.getElementById('loading-overlay');
const loadingText = document.getElementById('loading-text');
try {
// 1. 创建 Canvas 并绘制文字
const text = '皮蛋众妙真君';
const fontSize = 120;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `bold ${fontSize}px "Microsoft YaHei", "SimHei", sans-serif`;
const metrics = context.measureText(text);
const textWidth = metrics.width;
canvas.width = textWidth;
canvas.height = fontSize * 1.2;
context.font = `bold ${fontSize}px "Microsoft YaHei", "SimHei", sans-serif`;
context.fillStyle = '#ffffff';
context.fillText(text, 0, fontSize);
// 2. 从 Canvas 采样像素数据
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const particlePositions = [];
const particleDensity = 4; // 采样密度,数值越小粒子越多
for (let y = 0; y < canvas.height; y += particleDensity) {
for (let x = 0; x < canvas.width; x += particleDensity) {
const index = (y * canvas.width + x) * 4;
const alpha = data[index + 3];
if (alpha > 128) {
// 将 Canvas 坐标转换为居中的 3D 坐标
const pX = (x - canvas.width / 2) * 0.2;
const pY = -(y - canvas.height / 2) * 0.2;
particlePositions.push(new THREE.Vector3(pX, pY, 0));
}
}
}
if (particlePositions.length === 0) throw new Error('未能从Canvas生成粒子');
// 3. 创建粒子系统
const particleCount = particlePositions.length;
const particleGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const velocities = new Float32Array(particleCount * 3);
const initialPositions = new Float32Array(particleCount * 3); // 存储初始位置
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
const pos = particlePositions[i];
positions[i3] = pos.x;
positions[i3 + 1] = pos.y;
positions[i3 + 2] = pos.z;
initialPositions[i3] = pos.x;
initialPositions[i3 + 1] = pos.y;
initialPositions[i3 + 2] = pos.z;
const color = new THREE.Color();
color.setHSL(Math.random(), 1.0, 0.5);
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
// 初始速度为0
velocities[i3] = 0;
velocities[i3 + 1] = 0;
velocities[i3 + 2] = 0;
}
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
particleGeometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
particleGeometry.setAttribute('initialPosition', new THREE.BufferAttribute(initialPositions, 3));
const particleMaterial = new THREE.PointsMaterial({
size: 1.0,
vertexColors: true,
blending: THREE.AdditiveBlending,
transparent: true,
});
textParticles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(textParticles);
loadingOverlay.style.display = 'none';
} catch (error) {
console.error("创建文字粒子失败:", error);
loadingText.innerText = '创建文字粒子失败';
}
}
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
if (stars) {
stars.rotation.y += 0.0001;
}
if (textParticles) {
if (elapsedTime > explosionDelay && !hasExploded) {
// 触发爆裂
const velocities = textParticles.geometry.attributes.velocity;
for (let i = 0; i < velocities.count; i++) {
const i3 = i * 3;
velocities.array[i3] = (Math.random() - 0.5) * 4;
velocities.array[i3+1] = (Math.random() - 0.5) * 4;
velocities.array[i3+2] = (Math.random() - 0.5) * 4;
}
velocities.needsUpdate = true;
hasExploded = true;
}
if (hasExploded) {
// 爆裂后的持续动画
const positions = textParticles.geometry.attributes.position;
const velocities = textParticles.geometry.attributes.velocity;
for (let i = 0; i < positions.count; i++) {
const i3 = i * 3;
const px = positions.getX(i);
const py = positions.getY(i);
const dx = mouse.x - px;
const dy = mouse.y - py;
const distSq = dx * dx + dy * dy;
const mouseForceRadius = 30;
if (distSq < mouseForceRadius * mouseForceRadius) {
const dist = Math.sqrt(distSq);
const force = (mouseForceRadius - dist) / mouseForceRadius;
velocities.array[i3] -= dx / dist * force * 0.5;
velocities.array[i3 + 1] -= dy / dist * force * 0.5;
}
// 粒子随机扰动
velocities.array[i3] += (Math.random() - 0.5) * 0.01;
velocities.array[i3 + 1] += (Math.random() - 0.5) * 0.01;
velocities.array[i3 + 2] += (Math.random() - 0.5) * 0.01;
// 更新位置
positions.array[i3] += velocities.array[i3];
positions.array[i3 + 1] += velocities.array[i3 + 1];
positions.array[i3 + 2] += velocities.array[i3 + 2];
// 速度衰减
velocities.array[i3] *= 0.97;
velocities.array[i3 + 1] *= 0.97;
velocities.array[i3 + 2] *= 0.97;
}
positions.needsUpdate = true;
}
}
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
const vector = new THREE.Vector3(mouse.x, mouse.y, 0.5);
vector.unproject(camera);
const dir = vector.sub(camera.position).normalize();
const distance = -camera.position.z / dir.z;
const pos = camera.position.clone().add(dir.multiplyScalar(distance));
mouse.x = pos.x;
mouse.y = pos.y;
}
init();
animate();
</script>
</body>
</html>