CSS
アニメーション
2026/01/27
2026/1/25
Three.jsを使ったパーティクル表現は、Web上で印象的なビジュアル演出を行うための定番テクニックです。
このデモでは、無数のパーティクルで構成された球体が、ユーザーの入力した文字へとリアルタイムでモーフィングする表現を実装しています。
テキスト入力・ボタン操作によるインタラクション、滑らかな補間アニメーション、AdditiveBlendingによる発光感などを組み合わせ、シンプルながらも没入感のある3D表現を実現しています。
<div id="kumonosu-input-container">
<input type="text" id="kumonosu-text-input" placeholder="Type here..." maxlength="15">
<button id="kumonosu-morph-btn">Morph</button>
</div>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #000;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#kumonosu-input-container {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
z-index: 100;
}
input {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
color: white;
padding: 10px 20px;
font-size: 16px;
outline: none;
width: 250px;
backdrop-filter: blur(5px);
}
button {
background: #6366f1;
border: none;
color: white;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s;
}
button:hover {
background: #4f46e5;
}
canvas {
display: block;
}
let scene, camera, renderer, particles;
const count = 10000; // パーティクル数
// 状態管理
let targetPositions = new Float32Array(count * 3);
let currentPositions = new Float32Array(count * 3);
let spherePositions = new Float32Array(count * 3);
let textPositions = new Float32Array(count * 3);
let morphProgress = 0;
let isTextMode = false;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 6;
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
createParticles();
// イベント設定
window.addEventListener('resize', onWindowResize);
document.getElementById('kumonosu-morph-btn').addEventListener('click', updateTextTarget);
document.getElementById('kumonosu-text-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') updateTextTarget();
});
animate();
}
function createParticles() {
const geometry = new THREE.BufferGeometry();
const colors = new Float32Array(count * 3);
// 1. 球体位置の生成
const phi = Math.PI * (3 - Math.sqrt(5));
for (let i = 0; i < count; i++) {
const y = 1 - (i / (count - 1)) * 2;
const radius = Math.sqrt(1 - y * y);
const theta = phi * i;
const x = Math.cos(theta) * radius;
const z = Math.sin(theta) * radius;
const scale = 2.5;
spherePositions[i * 3] = x * scale;
spherePositions[i * 3 + 1] = y * scale;
spherePositions[i * 3 + 2] = z * scale;
// 初期状態は球体
currentPositions[i * 3] = spherePositions[i * 3];
currentPositions[i * 3 + 1] = spherePositions[i * 3 + 1];
currentPositions[i * 3 + 2] = spherePositions[i * 3 + 2];
// 色の初期設定
const color = new THREE.Color();
color.setHSL(0.6 + (y * 0.1), 0.8, 0.6);
colors[i * 3] = color.r;
colors[i * 3 + 1] = color.g;
colors[i * 3 + 2] = color.b;
}
geometry.setAttribute('position', new THREE.BufferAttribute(currentPositions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.02,
vertexColors: true,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
particles = new THREE.Points(geometry, material);
scene.add(particles);
// 初期のターゲットは球体
targetPositions.set(spherePositions);
}
function updateTextTarget() {
const text = document.getElementById('kumonosu-text-input').value;
if (!text) {
isTextMode = false;
targetPositions.set(spherePositions);
return;
}
// オフスクリーンキャンバスでテキストを描画して座標を取得
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 100;
ctx.fillStyle = 'white';
ctx.font = 'bold 60px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 200, 50);
const imageData = ctx.getImageData(0, 0, 400, 100).data;
const points = [];
// 白いピクセル(テキスト部分)を抽出
for (let y = 0; y < 100; y += 1) {
for (let x = 0; x < 400; x += 1) {
const alpha = imageData[(y * 400 + x) * 4 + 3];
if (alpha > 128) {
// 座標を3D空間用に変換 (-5 to 5 程度)
points.push({
x: (x - 200) / 30,
y: -(y - 50) / 30,
z: (Math.random() - 0.5) * 0.5 // 厚み
});
}
}
}
// パーティクル数に合わせてテキスト座標を割り当て
for (let i = 0; i < count; i++) {
const p = points[i % points.length];
textPositions[i * 3] = p.x;
textPositions[i * 3 + 1] = p.y;
textPositions[i * 3 + 2] = p.z;
}
isTextMode = true;
targetPositions.set(textPositions);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const positions = particles.geometry.attributes.position.array;
// 各パーティクルをターゲットに向けて移動(イージング)
for (let i = 0; i < count * 3; i++) {
positions[i] += (targetPositions[i] - positions[i]) * 0.08;
}
particles.geometry.attributes.position.needsUpdate = true;
// テキストモードでない時は回転させる
if (!isTextMode) {
particles.rotation.y += 0.005;
particles.rotation.x += 0.002;
} else {
// テキストモード時は正面を向かせる(ゆっくり戻る)
particles.rotation.y *= 0.95;
particles.rotation.x *= 0.95;
}
renderer.render(scene, camera);
}
window.onload = init;
https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
本デモは Three.js を用いた3Dパーティクル表現で、初期状態では黄金比(フィボナッチスフィア)に基づいて均一に配置された球体を表示します。
テキスト入力時には、Canvas上に描画した文字のピクセル情報を取得し、それを3D座標へ変換することで文字形状を生成します。パーティクルは補間処理によって目標位置へ滑らかに移動し、自然なモーフィングアニメーションを実現しています。テキスト表示中は自動回転を抑え、形状を正面から見せる仕様です。
コード内の数値や設定を変更することで、見た目や挙動を柔軟に調整できます。
count を変更すると密度や負荷を調整可能font 設定でフォント・サイズを変更可能パーティクル数が多いため、端末性能によっては描画負荷が高くなる場合があります。特にスマートフォンや低スペック環境では、数を減らす調整が推奨されます。また、フォント指定によっては文字の形状が崩れることがあるため、日本語フォントを使う場合は事前確認が必要です。