アニメーション

Three.jsで作る、入力テキストが3Dメタル文字に変わるジェネレーター

投稿日2026/05/21

更新日2026/5/12

テキストをただ表示するだけでなく、金属のような質感で立体的に浮かび上がらせたい――そんな演出を、Three.jsひとつで実現するのがこのデモです。

入力欄にテキストを打ち込んで「Morph」ボタンを押すと、1文字ずつスケールアニメーションしながら3Dメタル文字が出現します。

メタリックな質感はCanvasで生成したブラシテクスチャとMeshStandardMaterialの金属パラメータで表現しており、複数のライトによる陰影がリアルさを引き立てます。

OrbitControlsで自由に回転・ズームして、あらゆる角度から文字を眺めることができます。

Code コード

<div class="kumonosu-ui-container">
	<input type="text" id="kumonosu-morphInput" class="kumonosu-text-input" placeholder="Type here..." maxlength="20" value="HAPPY">
	<button id="kumonosu-morphBtn" class="kumonosu-morph-button">Morph</button>
	<div class="kumonosu-disclaimer">※日本語には対応していません</div>
</div>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
body {
	margin: 0;
	overflow: hidden;
	background-color: #050505;
	font-family: 'Inter', sans-serif;
}
.kumonosu-ui-container {
	position: fixed;
	bottom: 40px;
	left: 50%;
	transform: translateX(-50%);
	display: flex;
	gap: 10px;
	z-index: 100;
	padding: 10px;
	border-radius: 14px;
	backdrop-filter: blur(10px);
	width: auto;
	max-width: 90vw;
	box-sizing: border-box;
}
.kumonosu-text-input {
	width: 300px;
	background: #181818;
	border: 1px solid #333;
	border-radius: 8px;
	color: #fff;
	padding: 12px 16px;
	font-size: 16px;
	outline: none;
	transition: border-color 0.2s;
	flex-shrink: 1;
	min-width: 0;
}
.kumonosu-text-input:focus {
	border-color: #555;
}
.kumonosu-text-input::placeholder {
	color: #555;
}
.kumonosu-disclaimer {
	position: absolute;
	bottom: -22px;
	left: 14px;
	font-size: 10px;
	color: #666;
	letter-spacing: 0.02em;
	pointer-events: none;
	white-space: nowrap;
}
.kumonosu-morph-button {
	background: #6366f1;
	color: white;
	border: none;
	border-radius: 8px;
	padding: 0 20px;
	font-size: 15px;
	font-weight: 700;
	cursor: pointer;
	transition: transform 0.1s, background 0.2s;
	display: flex;
	align-items: center;
	justify-content: center;
	flex-shrink: 0;
}
.kumonosu-morph-button:hover {
	background: #4f46e5;
}
.kumonosu-morph-button:active {
	transform: scale(0.95);
}
@media (max-width: 600px) {
	.kumonosu-ui-container {
		bottom: 30px;
		width: 92vw;
		padding: 8px;
	}
	.kumonosu-text-input {
		width: 100%;
		padding: 10px 12px;
		font-size: 14px;
	}
	.kumonosu-morph-button {
		padding: 0 15px;
		font-size: 14px;
	}
	.kumonosu-disclaimer {
		bottom: -18px;
		font-size: 9px;
	}
}
import * as THREE from 'https://esm.sh/three@0.150.0';
import {
	OrbitControls
} from 'https://esm.sh/three@0.150.0/examples/jsm/controls/OrbitControls.js';
import {
	TextGeometry
} from 'https://esm.sh/three@0.150.0/examples/jsm/geometries/TextGeometry.js';
import {
	FontLoader
} from 'https://esm.sh/three@0.150.0/examples/jsm/loaders/FontLoader.js';
// --- シーン設定 ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

function adjustCamera() {
	camera.aspect = window.innerWidth / window.innerHeight;
	if (window.innerWidth < 600) {
		camera.position.set(0, 0.8, 7.5);
	} else {
		camera.position.set(-1.8, 0.8, 4.5);
	}
	camera.updateProjectionMatrix();
}
adjustCamera();
const renderer = new THREE.WebGLRenderer({
	antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// --- ライティング ---
const ambientLight = new THREE.AmbientLight(0x404050, 0.6);
const mainLight = new THREE.DirectionalLight(0xffffff, 1.5);
mainLight.position.set(5, 5, 5);
mainLight.castShadow = true;
const backLight = new THREE.PointLight(0xffaa88, 1.2);
backLight.position.set(-3, 2, -4);
const rimLight = new THREE.PointLight(0xffffff, 0.8);
rimLight.position.set(0, 4, -2);
const frontLight = new THREE.PointLight(0xaaccff, 0.5);
frontLight.position.set(0, 0, 4);
const bottomLight = new THREE.PointLight(0x443322, 0.4);
bottomLight.position.set(0, -3, 0);
[ambientLight, mainLight, backLight, rimLight, frontLight, bottomLight].forEach(l => scene.add(l));
const FONT_URL = 'https://threejs.org/examples/fonts/helvetiker_bold.typeface.json';
// --- メタルテクスチャ ---
function createMetalMap() {
	const canvas = document.createElement('canvas');
	canvas.width = 512;
	canvas.height = 512;
	const ctx = canvas.getContext('2d');
	ctx.fillStyle = '#c0c0c0';
	ctx.fillRect(0, 0, 512, 512);
	for (let i = 0; i < 3000; i++) {
		ctx.fillStyle = `rgba(255,255,255,${Math.random() * 0.04})`;
		ctx.fillRect(Math.random() * 512, Math.random() * 512, 1, 20);
	}
	const tex = new THREE.CanvasTexture(canvas);
	tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
	return tex;
}
let currentFont = null;
const metalMap = createMetalMap();
let textGroup = new THREE.Group();
scene.add(textGroup);
async function rebuildText(newText) {
	if (!currentFont || !newText) return;
	const oldMeshes = [...textGroup.children];
	oldMeshes.forEach((m) => {
		let s = 1;
		const fade = () => {
			s -= 0.15;
			m.scale.set(s, s, s);
			if (s > 0) requestAnimationFrame(fade);
			else {
				if (m.geometry) m.geometry.dispose();
				if (m.material) m.material.dispose();
				textGroup.remove(m);
			}
		};
		fade();
	});
	const chars = newText.toUpperCase().split('');
	const meshes = [];
	let totalX = 0;
	const spacing = 0.02; // 文字間隔
	chars.forEach((char) => {
		if (char === ' ') {
			totalX += 0.3;
			return;
		}
		const geo = new TextGeometry(char, {
			font: currentFont,
			size: 0.45,
			height: 0.2,
			curveSegments: 12,
			bevelEnabled: true,
			bevelThickness: 0.04,
			bevelSize: 0.03,
			bevelSegments: 5
		});
		geo.computeBoundingBox();
		const mat = new THREE.MeshStandardMaterial({
			color: 0xc0c0c0,
			map: metalMap,
			metalness: 0.9,
			roughness: 0.2
		});
		const mesh = new THREE.Mesh(geo, mat);
		mesh.castShadow = true;
		mesh.userData.offsetX = geo.boundingBox.min.x;
		mesh.userData.width = geo.boundingBox.max.x - geo.boundingBox.min.x;
		mesh.userData.posX = totalX;
		totalX += mesh.userData.width + spacing;
		meshes.push(mesh);
	});
	const centerOffset = (totalX - spacing) / 2;
	meshes.forEach((m, i) => {
		m.position.x = m.userData.posX - centerOffset - m.userData.offsetX;
		m.scale.set(0, 0, 0);
		textGroup.add(m);
		setTimeout(() => {
			let s = 0;
			const anim = () => {
				s += 0.12;
				m.scale.set(s, s, s);
				if (s < 1) requestAnimationFrame(anim);
				else m.scale.set(1, 1, 1);
			};
			anim();
		}, i * 40);
	});
}
const input = document.getElementById('kumonosu-morphInput');
const btn = document.getElementById('kumonosu-morphBtn');
const handleMorph = () => {
	const val = input.value.trim();
	if (val) rebuildText(val);
};
btn.addEventListener('click', handleMorph);
input.addEventListener('keypress', (e) => {
	if (e.key === 'Enter') handleMorph();
});
new FontLoader().load(FONT_URL, f => {
	currentFont = f;
	rebuildText("HAPPY");
});
window.addEventListener('resize', () => {
	adjustCamera();
	renderer.setSize(window.innerWidth, window.innerHeight);
});

function animate() {
	requestAnimationFrame(animate);
	controls.update();
	renderer.render(scene, camera);
}
animate();

Explanation 詳しい説明

仕様

このデモは、Three.jsのTextGeometryを使って英字テキストを1文字ずつ3Dメッシュ化し、金属質感のマテリアルを適用してリアルタイム描画します。

主な機能は以下のとおりです。

  • テキスト入力→3D変換: 入力欄にテキストを入力し「Morph」ボタンまたはEnterキーで、3Dメタル文字を生成します。最大20文字まで対応しています。
  • モーフアニメーション: 既存の文字は縮小して消え、新しい文字が1文字ずつ時間差でスケールアップして出現します。
  • メタル質感: Canvasで生成したヘアラインブラシ風テクスチャをMeshStandardMaterialに適用し、metalness: 0.9roughness: 0.2で金属的な光沢を再現しています。
  • 多灯ライティング: 環境光、ディレクショナルライト、ポイントライト4灯の計6灯構成で、立体感のある陰影とリムライトを演出します。影(PCFSoftShadowMap)も有効です。
  • OrbitControls: マウスドラッグで回転、ホイールでズームが可能です。ダンピングによるなめらかな操作感を備えています。
  • レスポンシブ対応: 画面幅600px以下ではカメラ位置を自動で引き、UI要素のサイズも調整されます。

カスタマイズ

  • 初期テキストの変更: rebuildText("HAPPY")の引数を書き換えると、初期表示される文字を変更できます。
  • 文字の色: マテリアルのcolor(デフォルト0xc0c0c0=シルバー)を変更すれば、ゴールドや銅色など自由に変えられます。たとえばゴールドなら0xd4a84bのような値を指定します。
  • 金属感の調整: metalness(金属度)とroughness(粗さ)の値を変えると、ピカピカの鏡面からマットな金属まで質感を調整できます。
  • 文字サイズ・厚み: TextGeometryのsize(文字の大きさ)とheight(押し出しの厚み)を変更すると、文字のボリューム感が変わります。
  • ベベル: bevelThicknessbevelSizebevelSegmentsで文字の角の丸みを調整できます。値を大きくするとより丸みを帯びた仕上がりになります。
  • 背景色: scene.backgroundの色を変更すれば、背景の雰囲気を変えられます。
  • ライティング: 各ライトの色・強度・位置を変更すると、照明の雰囲気を大きく変えられます。暖色系に寄せたい場合はポイントライトの色を調整してください。
  • ボタンの色: CSSの.kumonosu-morph-buttonbackgroundを変更すれば、ボタンカラーをサイトのテーマに合わせられます。

注意点

  • 日本語非対応: 使用フォント(Helvetiker Bold)は英数字と一部記号のみ対応しています。日本語を入力するとエラーまたは表示されません。UI上にも注意書きを表示しています。
  • Three.jsの読み込み: unpkg.com経由でThree.js v0.150.0とOrbitControls・TextGeometry・FontLoaderを読み込んでいます。本番環境ではバージョン固定やローカルホスティングを推奨します。
  • フォントの読み込み: threejs.orgからJSON形式のフォントを非同期で読み込んでいます。ネットワーク状況によっては初期表示に時間がかかる場合があります。
  • メモリ管理: テキスト変更時に古いジオメトリとマテリアルをdispose()で明示的に解放していますが、頻繁に切り替えるとメモリ負荷が蓄積する可能性があります。
  • パフォーマンス: devicePixelRatioの上限を2に制限しています。文字数が多い場合やベベルのセグメント数を増やしすぎると、頂点数が増加してパフォーマンスに影響します。
  • importmap: type="importmap"を使用しているため、対応していない古いブラウザでは動作しません。