|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>FaceMotion Simulator</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
|
|
<link rel="stylesheet" href="style.css"> |
|
|
</head> |
|
|
<body class="bg-indigo-50 min-h-screen flex items-center justify-center p-4"> |
|
|
<div class="max-w-md w-full bg-white rounded-2xl shadow-xl overflow-hidden"> |
|
|
|
|
|
<div class="bg-indigo-600 p-6 text-white"> |
|
|
<div class="flex items-center justify-between"> |
|
|
<div> |
|
|
<h1 class="text-2xl font-bold">FaceMotion Simulator</h1> |
|
|
<p class="text-indigo-100">Interactive 3D Head Tracking</p> |
|
|
</div> |
|
|
<div class="w-12 h-12 rounded-full bg-indigo-500 flex items-center justify-center"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> |
|
|
<circle cx="9" cy="7" r="4"></circle> |
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> |
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path> |
|
|
</svg> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="canvas-container" class="w-full aspect-square bg-indigo-100 relative"> |
|
|
<canvas id="liveness-canvas" class="absolute inset-0 w-full h-full"></canvas> |
|
|
<div class="absolute bottom-4 left-0 right-0 flex justify-center"> |
|
|
<div class="bg-indigo-600 text-white px-3 py-1 rounded-full text-xs font-medium shadow-md"> |
|
|
Move the sliders below |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="p-6 space-y-6"> |
|
|
<div> |
|
|
<div class="flex justify-between mb-2"> |
|
|
<label for="rotate-y" class="text-sm font-medium text-gray-700">Horizontal Rotation</label> |
|
|
<span id="rotate-y-value" class="text-sm text-indigo-600 font-medium">0°</span> |
|
|
</div> |
|
|
<input type="range" id="rotate-y" min="-80" max="80" value="0" class="w-full h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer"> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<div class="flex justify-between mb-2"> |
|
|
<label for="rotate-x" class="text-sm font-medium text-gray-700">Vertical Rotation</label> |
|
|
<span id="rotate-x-value" class="text-sm text-indigo-600 font-medium">0°</span> |
|
|
</div> |
|
|
<input type="range" id="rotate-x" min="-45" max="45" value="0" class="w-full h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer"> |
|
|
</div> |
|
|
|
|
|
<button id="reset-btn" class="w-full py-2 px-4 bg-indigo-100 hover:bg-indigo-200 text-indigo-700 font-medium rounded-lg transition duration-200 flex items-center justify-center space-x-2"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<polyline points="1 4 1 10 7 10"></polyline> |
|
|
<polyline points="23 20 23 14 17 14"></polyline> |
|
|
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path> |
|
|
</svg> |
|
|
<span>Reset Position</span> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const canvasContainer = document.getElementById('canvas-container'); |
|
|
const canvas = document.getElementById('liveness-canvas'); |
|
|
|
|
|
|
|
|
const scene = new THREE.Scene(); |
|
|
scene.background = null; |
|
|
|
|
|
|
|
|
const camera = new THREE.PerspectiveCamera(75, canvasContainer.clientWidth / canvasContainer.clientHeight, 0.1, 1000); |
|
|
camera.position.z = 5; |
|
|
|
|
|
|
|
|
const renderer = new THREE.WebGLRenderer({ |
|
|
canvas: canvas, |
|
|
antialias: true, |
|
|
alpha: true |
|
|
}); |
|
|
renderer.setPixelRatio(window.devicePixelRatio); |
|
|
renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight); |
|
|
|
|
|
const headGroup = new THREE.Group(); |
|
|
scene.add(headGroup); |
|
|
|
|
|
|
|
|
const headGeometry = new THREE.SphereGeometry(1.5, 32, 32); |
|
|
const headMaterial = new THREE.MeshStandardMaterial({ |
|
|
color: 0x3498db, |
|
|
wireframe: true, |
|
|
transparent: true, |
|
|
opacity: 0.8 |
|
|
}); |
|
|
const head = new THREE.Mesh(headGeometry, headMaterial); |
|
|
headGroup.add(head); |
|
|
|
|
|
|
|
|
const eyeGeometry = new THREE.SphereGeometry(0.15, 16, 16); |
|
|
const eyeMaterial = new THREE.MeshStandardMaterial({ |
|
|
color: 0x2ecc71, |
|
|
emissive: 0x2ecc71, |
|
|
emissiveIntensity: 0.5 |
|
|
}); |
|
|
|
|
|
|
|
|
const eyeLeft = new THREE.Mesh(eyeGeometry, eyeMaterial); |
|
|
eyeLeft.position.set(-0.5, 0.2, 1.2); |
|
|
headGroup.add(eyeLeft); |
|
|
|
|
|
|
|
|
const eyeRight = new THREE.Mesh(eyeGeometry, eyeMaterial.clone()); |
|
|
eyeRight.position.set(0.5, 0.2, 1.2); |
|
|
headGroup.add(eyeRight); |
|
|
|
|
|
|
|
|
const mouthGeometry = new THREE.TorusGeometry(0.3, 0.02, 8, 32, Math.PI); |
|
|
const mouthMaterial = new THREE.MeshStandardMaterial({ |
|
|
color: 0xe74c3c, |
|
|
emissive: 0xe74c3c, |
|
|
emissiveIntensity: 0.3 |
|
|
}); |
|
|
const mouth = new THREE.Mesh(mouthGeometry, mouthMaterial); |
|
|
mouth.position.set(0, -0.3, 1.2); |
|
|
mouth.rotation.x = Math.PI / 8; |
|
|
headGroup.add(mouth); |
|
|
|
|
|
|
|
|
const pointsGeometry = new THREE.SphereGeometry(0.05, 8, 8); |
|
|
const pointsMaterial = new THREE.MeshStandardMaterial({ |
|
|
color: 0xf1c40f, |
|
|
emissive: 0xf1c40f, |
|
|
emissiveIntensity: 0.2 |
|
|
}); |
|
|
|
|
|
|
|
|
const pointsPositions = [ |
|
|
[0, 0.5, 1.3], |
|
|
[-0.3, -0.1, 1.3], |
|
|
[0.3, -0.1, 1.3], |
|
|
[0, 0.1, 1.5] |
|
|
]; |
|
|
|
|
|
pointsPositions.forEach(pos => { |
|
|
const point = new THREE.Mesh(pointsGeometry, pointsMaterial.clone()); |
|
|
point.position.set(pos[0], pos[1], pos[2]); |
|
|
headGroup.add(point); |
|
|
}); |
|
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x333333, 0.8); |
|
|
scene.add(ambientLight); |
|
|
|
|
|
const directionalLight1 = new THREE.DirectionalLight(0x3498db, 0.8); |
|
|
directionalLight1.position.set(5, 3, 5); |
|
|
scene.add(directionalLight1); |
|
|
|
|
|
const directionalLight2 = new THREE.DirectionalLight(0xe74c3c, 0.5); |
|
|
directionalLight2.position.set(-5, 3, 5); |
|
|
scene.add(directionalLight2); |
|
|
|
|
|
const directionalLight3 = new THREE.DirectionalLight(0x2ecc71, 0.3); |
|
|
directionalLight3.position.set(0, -5, 5); |
|
|
scene.add(directionalLight3); |
|
|
|
|
|
const sliderY = document.getElementById('rotate-y'); |
|
|
const sliderX = document.getElementById('rotate-x'); |
|
|
const rotateYValue = document.getElementById('rotate-y-value'); |
|
|
const rotateXValue = document.getElementById('rotate-x-value'); |
|
|
const resetBtn = document.getElementById('reset-btn'); |
|
|
|
|
|
sliderY.addEventListener('input', (e) => { |
|
|
const rotationRadians = (e.target.value * Math.PI) / 180; |
|
|
headGroup.rotation.y = rotationRadians; |
|
|
rotateYValue.textContent = `${e.target.value}°`; |
|
|
}); |
|
|
|
|
|
sliderX.addEventListener('input', (e) => { |
|
|
const rotationRadians = (e.target.value * Math.PI) / 180; |
|
|
headGroup.rotation.x = rotationRadians; |
|
|
rotateXValue.textContent = `${e.target.value}°`; |
|
|
}); |
|
|
|
|
|
resetBtn.addEventListener('click', () => { |
|
|
sliderY.value = 0; |
|
|
sliderX.value = 0; |
|
|
headGroup.rotation.set(0, 0, 0); |
|
|
rotateYValue.textContent = '0°'; |
|
|
rotateXValue.textContent = '0°'; |
|
|
}); |
|
|
|
|
|
let pulseDirection = 1; |
|
|
let pulseIntensity = 0; |
|
|
|
|
|
function animate() { |
|
|
requestAnimationFrame(animate); |
|
|
|
|
|
|
|
|
pulseIntensity += 0.01 * pulseDirection; |
|
|
if (pulseIntensity > 0.3 || pulseIntensity < 0) { |
|
|
pulseDirection *= -1; |
|
|
} |
|
|
|
|
|
headGroup.children.forEach(child => { |
|
|
if (child.material && child.material.emissiveIntensity !== undefined) { |
|
|
child.material.emissiveIntensity = 0.2 + pulseIntensity; |
|
|
} |
|
|
}); |
|
|
|
|
|
renderer.render(scene, camera); |
|
|
} |
|
|
|
|
|
function onResize() { |
|
|
const width = canvasContainer.clientWidth; |
|
|
const height = canvasContainer.clientHeight; |
|
|
|
|
|
renderer.setSize(width, height); |
|
|
camera.aspect = width / height; |
|
|
camera.updateProjectionMatrix(); |
|
|
} |
|
|
|
|
|
window.addEventListener('resize', onResize); |
|
|
animate(); |
|
|
onResize(); |
|
|
</script> |
|
|
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script> |
|
|
</body> |
|
|
</html> |