CSSとJSで作る 3Dパーティクル文字モーフィング

アニメーション

CSSとJSで作る 3Dパーティクル文字モーフィング

投稿日2026/01/27

更新日2026/1/25

Three.jsを使ったパーティクル表現は、Web上で印象的なビジュアル演出を行うための定番テクニックです。
このデモでは、無数のパーティクルで構成された球体が、ユーザーの入力した文字へとリアルタイムでモーフィングする表現を実装しています。

テキスト入力・ボタン操作によるインタラクション、滑らかな補間アニメーション、AdditiveBlendingによる発光感などを組み合わせ、シンプルながらも没入感のある3D表現を実現しています。

Preview プレビュー

Code コード

<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

Explanation 詳しい説明

仕様

本デモは Three.js を用いた3Dパーティクル表現で、初期状態では黄金比(フィボナッチスフィア)に基づいて均一に配置された球体を表示します。
テキスト入力時には、Canvas上に描画した文字のピクセル情報を取得し、それを3D座標へ変換することで文字形状を生成します。パーティクルは補間処理によって目標位置へ滑らかに移動し、自然なモーフィングアニメーションを実現しています。テキスト表示中は自動回転を抑え、形状を正面から見せる仕様です。

  • 使用ライブラリ:Three.js(CDN)
  • 表現方法:Points + BufferGeometry
  • 動作環境:WebGL対応ブラウザ

カスタマイズ

コード内の数値や設定を変更することで、見た目や挙動を柔軟に調整できます。

  • パーティクル数
    count を変更すると密度や負荷を調整可能
  • 文字の見た目
    Canvasの font 設定でフォント・サイズを変更可能
  • モーフィング速度
    補間係数を調整すると変形スピードが変化
  • 全体サイズ
    球体スケールや座標変換倍率で大きさを調整可能

注意点

パーティクル数が多いため、端末性能によっては描画負荷が高くなる場合があります。特にスマートフォンや低スペック環境では、数を減らす調整が推奨されます。また、フォント指定によっては文字の形状が崩れることがあるため、日本語フォントを使う場合は事前確認が必要です。

  • 高負荷になりやすい(GPU性能に依存)
  • モバイル環境では調整推奨
  • Three.jsのバージョン差異に注意