CSS
アニメーション
2026/01/15
2026/1/15
Webカメラの前に手をかざすだけで、数万の粒子があなたの動きに共鳴します。
本記事では、GoogleのAI技術「MediaPipe Hands」と、強力な3Dライブラリ「Three.js」を融合させた、近未来的なインタラクティブ・アートをご紹介します。
今回は、AIへの指示に使用したプロンプトも公開。最新のAI技術を使って、魔法のようなユーザー体験をWebサイトに実装する方法を詳しく解説します。
※カメラの切り忘れにご注意ください。
<div id="kumonosu-loading">
<div>Initialize AI Camera...</div>
<div class="kumonosu-loading-sub">Please allow camera access</div>
</div>
<!-- UI Panel -->
<div id="kumonosu-ui-panel">
<h2>Particle Controller</h2>
<div class="kumonosu-control-group">
<label>Shape Template</label>
<div class="kumonosu-shape-grid">
<button class="kumonosu-shape-btn kumonosu-active" data-shape="heart">Heart</button>
<button class="kumonosu-shape-btn" data-shape="flower">Flower</button>
<button class="kumonosu-shape-btn" data-shape="saturn">Saturn</button>
<button class="kumonosu-shape-btn" data-shape="spiral">Spiral</button>
<button class="kumonosu-shape-btn" data-shape="fireworks" style="grid-column: span 2;">Fireworks</button>
</div>
</div>
<div class="kumonosu-control-group">
<label>Particle Color</label>
<input type="color" id="kumonosu-color-picker" value="#4facfe">
</div>
<div class="kumonosu-control-group">
<label>Hand Interaction</label>
<div style="font-size: 11px; color: #888; line-height: 1.4;">
Open Hand: Expand / Explode<br>
Closed Fist: Contract / Tension
</div>
</div>
</div>
<div id="kumonosu-status">Waiting for camera...</div>
<!-- Canvas Container -->
<div id="kumonosu-canvas-container"></div>
<video id="kumonosu-input-video" playsinline></video>
#kumonosu-canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* Hidden video element for MediaPipe processing */
#kumonosu-input-video {
display: none;
}
/* Modern UI Panel */
#kumonosu-ui-panel {
position: absolute;
top: 20px;
right: 20px;
width: 280px;
background: rgba(20, 20, 25, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 20px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
transition: opacity 0.3s;
}
h2 {
margin: 0 0 15px 0;
font-size: 16px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
color: #ccc;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 10px;
}
.kumonosu-control-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-size: 12px;
color: #aaa;
}
/* Shape Buttons */
.kumonosu-shape-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
button {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: white;
padding: 10px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
}
button:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
button.kumonosu-active {
background: #4facfe;
border-color: #4facfe;
color: white;
box-shadow: 0 0 15px rgba(79, 172, 254, 0.4);
}
/* Color Picker */
input[type="color"] {
-webkit-appearance: none;
border: none;
width: 100%;
height: 40px;
border-radius: 8px;
cursor: pointer;
background: none;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"]::-webkit-color-swatch {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
}
/* Status Display */
#kumonosu-status {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 10;
font-size: 14px;
color: #888;
pointer-events: none;
background: rgba(0, 0, 0, 0.5);
padding: 8px 12px;
border-radius: 20px;
}
#kumonosu-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 20;
font-size: 24px;
font-weight: bold;
color: #4facfe;
text-shadow: 0 0 20px rgba(79, 172, 254, 0.8);
pointer-events: none;
text-align: center;
}
.kumonosu-loading-sub {
font-size: 14px;
color: #888;
margin-top: 10px;
font-weight: normal;
}
import * as THREE from 'https://cdn.skypack.dev/three@0.136.0';
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/OrbitControls.js';
// --- Config ---
const CONFIG = {
particleCount: 15000,
particleSize: 0.15,
baseColor: new THREE.Color('#4facfe'),
handInfluence: 0, // 0 (Fist) to 1 (Open)
currentShape: 'heart'
};
// --- Three.js Initialization ---
const container = document.getElementById('kumonosu-canvas-container');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050505, 0.02);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 20;
camera.position.y = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 1.0;
// --- Particle System ---
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(CONFIG.particleCount * 3);
const targetPositions = new Float32Array(CONFIG.particleCount * 3);
for (let i = 0; i < CONFIG.particleCount * 3; i++) {
positions[i] = (Math.random() - 0.5) * 50;
targetPositions[i] = positions[i];
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
color: CONFIG.baseColor,
size: CONFIG.particleSize,
sizeAttenuation: true,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const sprite = new THREE.TextureLoader().load('https://threejs.org/examples/textures/sprites/disc.png');
material.map = sprite;
const particleSystem = new THREE.Points(geometry, material);
scene.add(particleSystem);
// --- Shape Generation Functions ---
function getPointOnSphere(r) {
const u = Math.random();
const v = Math.random();
const theta = 2 * Math.PI * u;
const phi = Math.acos(2 * v - 1);
return {
x: r * Math.sin(phi) * Math.cos(theta),
y: r * Math.sin(phi) * Math.sin(theta),
z: r * Math.cos(phi)
};
}
function generateShape(shapeName) {
const positions = [];
const count = CONFIG.particleCount;
for (let i = 0; i < count; i++) {
let x, y, z;
if (shapeName === 'heart') {
const phi = Math.acos(2 * Math.random() - 1);
const theta = Math.random() * Math.PI * 2;
x = 16 * Math.pow(Math.sin(theta), 3) * Math.sin(phi);
y = (13 * Math.cos(theta) - 5 * Math.cos(2*theta) - 2 * Math.cos(3*theta) - Math.cos(4*theta)) * Math.sin(phi);
z = 8 * Math.cos(phi);
x *= 0.5; y *= 0.5; z *= 0.5;
} else if (shapeName === 'flower') {
const angle = i * 137.5 * (Math.PI / 180);
const r = 0.3 * Math.sqrt(i);
x = r * Math.cos(angle);
y = (Math.random() - 0.5) * 5 + Math.sin(r * 0.5) * 5;
z = r * Math.sin(angle);
} else if (shapeName === 'saturn') {
if (i < count * 0.3) {
const p = getPointOnSphere(4);
x = p.x; y = p.y; z = p.z;
} else {
const angle = Math.random() * Math.PI * 2;
const dist = 6 + Math.random() * 4;
x = Math.cos(angle) * dist;
z = Math.sin(angle) * dist;
y = (Math.random() - 0.5) * 0.5;
const tilt = 0.4;
const tempY = y * Math.cos(tilt) - z * Math.sin(tilt);
const tempZ = y * Math.sin(tilt) + z * Math.cos(tilt);
y = tempY; z = tempZ;
}
} else if (shapeName === 'spiral') {
const t = i * 0.1;
const r = 3 + Math.sin(t * 0.1);
x = r * Math.cos(t * 0.1);
y = (i * 0.02) - 10;
z = r * Math.sin(t * 0.1);
} else if (shapeName === 'fireworks') {
const p = getPointOnSphere(Math.random() * 20);
x = p.x; y = p.y; z = p.z;
}
positions.push(x, y, z);
}
return positions;
}
function updateTargetShape(shapeName) {
const newPos = generateShape(shapeName);
for(let i = 0; i < CONFIG.particleCount * 3; i++) {
targetPositions[i] = newPos[i];
}
}
updateTargetShape('heart');
// --- MediaPipe Hands Setup ---
const videoElement = document.getElementById('kumonosu-input-video');
const statusElement = document.getElementById('kumonosu-status');
const loadingElement = document.getElementById('kumonosu-loading');
if (typeof window.Hands === 'undefined' || typeof window.Camera === 'undefined') {
statusElement.innerText = "Error: MediaPipe libraries failed to load.";
loadingElement.innerText = "Library Load Error";
}
const hands = new window.Hands({locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
}});
hands.setOptions({
maxNumHands: 2,
modelComplexity: 1,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
hands.onResults(onHandsResults);
try {
const cameraUtils = new window.Camera(videoElement, {
onFrame: async () => {
await hands.send({image: videoElement});
},
width: 640,
height: 480
});
cameraUtils.start()
.then(() => {
loadingElement.style.display = 'none';
statusElement.innerText = "Camera active. Show your hand!";
})
.catch(err => {
loadingElement.innerHTML = "Camera Error<br><span style='font-size:12px; font-weight:normal'>Please allow camera access and reload</span>";
console.error("Camera start error:", err);
});
} catch (e) {
console.error("Camera initialization failed", e);
loadingElement.innerText = "Camera Init Failed";
}
function onHandsResults(results) {
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
const landmarks = results.multiHandLandmarks[0];
const wrist = landmarks[0];
const tips = [8, 12, 16, 20];
let totalDist = 0;
tips.forEach(idx => {
const tip = landmarks[idx];
const dist = Math.sqrt(Math.pow(tip.x - wrist.x, 2) + Math.pow(tip.y - wrist.y, 2));
totalDist += dist;
});
const avgDist = totalDist / tips.length;
const minVal = 0.2;
const maxVal = 0.5;
let influence = (avgDist - minVal) / (maxVal - minVal);
influence = Math.max(0, Math.min(1, influence));
CONFIG.handInfluence += (influence - CONFIG.handInfluence) * 0.1;
statusElement.innerText = `Hand detected: ${influence < 0.3 ? "Closed (Tension)" : "Open (Expand)"}`;
} else {
CONFIG.handInfluence += (0.5 - CONFIG.handInfluence) * 0.05;
statusElement.innerText = "No hand detected";
}
}
// --- Event Listeners ---
document.querySelectorAll('.kumonosu-shape-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.kumonosu-shape-btn').forEach(b => b.classList.remove('kumonosu-active'));
e.target.classList.add('kumonosu-active');
const shape = e.target.dataset.shape;
CONFIG.currentShape = shape;
updateTargetShape(shape);
});
});
document.getElementById('kumonosu-color-picker').addEventListener('input', (e) => {
material.color.set(e.target.value);
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- Animation Loop ---
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const time = clock.getElapsedTime();
controls.update();
const posAttribute = geometry.attributes.position;
const currentPos = posAttribute.array;
let targetScale = 0.5 + CONFIG.handInfluence * 1.5;
if (CONFIG.currentShape === 'fireworks') targetScale = 0.2 + CONFIG.handInfluence * 3.0;
for (let i = 0; i < CONFIG.particleCount; i++) {
const idx = i * 3;
let tx = targetPositions[idx], ty = targetPositions[idx+1], tz = targetPositions[idx+2];
let cx = currentPos[idx], cy = currentPos[idx+1], cz = currentPos[idx+2];
const lerpSpeed = 2.0 * delta;
const dist = Math.sqrt(tx*tx + ty*ty + tz*tz);
let sx = tx * targetScale, sy = ty * targetScale, sz = tz * targetScale;
if (CONFIG.handInfluence < 0.3) {
const jitter = (0.3 - CONFIG.handInfluence) * 0.5;
sx += (Math.random() - 0.5) * jitter;
sy += (Math.random() - 0.5) * jitter;
sz += (Math.random() - 0.5) * jitter;
}
const breathe = Math.sin(time * 2 + dist * 0.5) * 0.2;
sx += sx * breathe * 0.1; sy += sy * breathe * 0.1; sz += sz * breathe * 0.1;
currentPos[idx] += (sx - cx) * lerpSpeed;
currentPos[idx+1] += (sy - cy) * lerpSpeed;
currentPos[idx+2] += (sz - cz) * lerpSpeed;
}
posAttribute.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
このアニメーションは、単なる動画再生ではなく、AIによるリアルタイム推論と3Dレンダリングを組み合わせた高度なジェスチャーシステムです。
最新技術の実装は、検索エンジンからの評価(E-E-A-T)やユーザー行動指標に好影響を与える可能性があります。
SEOとユーザビリティを両立させるために、以下の運用ルールを推奨します。
JavaScript内の変数を変更することで、ブランドイメージに合わせた演出が可能です。
反応感度の調整
CONFIG.handInfluence に掛ける係数を変えることで、手の動きに対するパーティクルの「爆発力」を強めたり、逆に穏やかな変化にしたりすることが可能です。
粒子の密度を調整
particleCount: 15000 の数値を変更します。モバイル特化にするなら 5000、よりリッチにするなら 30000 に設定してください。
カラーテーマの変更
baseColor: new THREE.Color(‘#4facfe’) の16進数カラーコードを書き換えるだけで、ネオンブルーから桜色、ゴールドなど、瞬時に雰囲気を変えられます。