| <!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.8, 64, 64); |
| headGeometry.scale(0.8, 1.1, 0.9); |
| const headMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xF5D0B9, |
| roughness: 0.4, |
| metalness: 0.1, |
| map: new THREE.TextureLoader().load('https://static.photos/people/1024x576/35') |
| }); |
| const head = new THREE.Mesh(headGeometry, headMaterial); |
| headGroup.add(head); |
| |
| |
| const eyeGeometry = new THREE.SphereGeometry(0.25, 32, 32); |
| const eyeMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xffffff, |
| roughness: 0.1, |
| metalness: 0.1 |
| }); |
| |
| |
| const eyeLeft = new THREE.Mesh(eyeGeometry, eyeMaterial); |
| eyeLeft.position.set(-0.7, 0.3, 1.8); |
| headGroup.add(eyeLeft); |
| |
| |
| const eyeRight = new THREE.Mesh(eyeGeometry, eyeMaterial.clone()); |
| eyeRight.position.set(0.7, 0.3, 1.8); |
| headGroup.add(eyeRight); |
| |
| |
| const pupilGeometry = new THREE.SphereGeometry(0.12, 32, 32); |
| const pupilMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x4A2C12, |
| }); |
| |
| |
| const pupilLeft = new THREE.Mesh(pupilGeometry, pupilMaterial); |
| pupilLeft.position.set(0, 0, 0.25); |
| eyeLeft.add(pupilLeft); |
| |
| |
| const pupilRight = new THREE.Mesh(pupilGeometry, pupilMaterial.clone()); |
| pupilRight.position.set(0, 0, 0.25); |
| eyeRight.add(pupilRight); |
| |
| |
| const eyelashGeometry = new THREE.ConeGeometry(0.02, 0.15, 4); |
| const eyelashMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 }); |
| |
| |
| for (let i = 0; i < 8; i++) { |
| const angle = (i / 8) * Math.PI * 0.5 - Math.PI * 0.25; |
| const eyelash = new THREE.Mesh(eyelashGeometry, eyelashMaterial); |
| eyelash.position.set(-0.7 + Math.cos(angle) * 0.3, 0.3 + Math.sin(angle) * 0.3, 1.85); |
| eyelash.rotation.z = angle; |
| headGroup.add(eyelash); |
| } |
| |
| |
| for (let i = 0; i < 8; i++) { |
| const angle = (i / 8) * Math.PI * 0.5 - Math.PI * 0.25; |
| const eyelash = new THREE.Mesh(eyelashGeometry, eyelashMaterial.clone()); |
| eyelash.position.set(0.7 + Math.cos(angle) * 0.3, 0.3 + Math.sin(angle) * 0.3, 1.85); |
| eyelash.rotation.z = angle; |
| headGroup.add(eyelash); |
| } |
| |
| |
| const noseGeometry = new THREE.ConeGeometry(0.3, 0.6, 32); |
| const noseMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xE8BBA5, |
| roughness: 0.4 |
| }); |
| const nose = new THREE.Mesh(noseGeometry, noseMaterial); |
| nose.position.set(0, 0, 1.9); |
| nose.rotation.x = Math.PI / 10; |
| headGroup.add(nose); |
| |
| |
| const lipGeometry = new THREE.TorusGeometry(0.4, 0.08, 16, 32, Math.PI); |
| const lipMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xC45C6B, |
| roughness: 0.3, |
| metalness: 0.2 |
| }); |
| const lips = new THREE.Mesh(lipGeometry, lipMaterial); |
| lips.position.set(0, -0.3, 1.7); |
| lips.rotation.x = Math.PI / 8; |
| headGroup.add(lips); |
| |
| |
| const eyebrowGeometry = new THREE.BoxGeometry(0.5, 0.05, 0.02); |
| const eyebrowMaterial = new THREE.MeshStandardMaterial({ color: 0x3A2A1A }); |
| |
| |
| const leftEyebrow = new THREE.Mesh(eyebrowGeometry, eyebrowMaterial); |
| leftEyebrow.position.set(-0.7, 0.5, 1.8); |
| leftEyebrow.rotation.z = -0.2; |
| headGroup.add(leftEyebrow); |
| |
| |
| const rightEyebrow = new THREE.Mesh(eyebrowGeometry, eyebrowMaterial.clone()); |
| rightEyebrow.position.set(0.7, 0.5, 1.8); |
| rightEyebrow.rotation.z = 0.2; |
| headGroup.add(rightEyebrow); |
| |
| |
| const hairGeometry = new THREE.SphereGeometry(2.0, 32, 32); |
| hairGeometry.scale(0.9, 1.2, 1.0); |
| const hairMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x4B3A2D, |
| roughness: 0.8, |
| metalness: 0.1 |
| }); |
| const hair = new THREE.Mesh(hairGeometry, hairMaterial); |
| hair.position.set(0, 0.4, 0); |
| headGroup.add(hair); |
| |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); |
| scene.add(ambientLight); |
| |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); |
| directionalLight.position.set(3, 5, 5); |
| directionalLight.castShadow = true; |
| directionalLight.shadow.mapSize.width = 1024; |
| directionalLight.shadow.mapSize.height = 1024; |
| scene.add(directionalLight); |
| |
| const fillLight = new THREE.DirectionalLight(0xffffff, 0.5); |
| fillLight.position.set(-3, 2, 3); |
| scene.add(fillLight); |
| |
| const rimLight = new THREE.DirectionalLight(0xffffff, 0.3); |
| rimLight.position.set(0, 3, -5); |
| scene.add(rimLight); |
| |
| 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 blinkTimer = 0; |
| let blinkState = false; |
| |
| function animate() { |
| requestAnimationFrame(animate); |
| |
| |
| blinkTimer += 0.01; |
| if (blinkTimer > 5 && !blinkState) { |
| eyeLeft.scale.y = 0.05; |
| eyeRight.scale.y = 0.05; |
| blinkState = true; |
| blinkTimer = 0; |
| } else if (blinkTimer > 0.1 && blinkState) { |
| eyeLeft.scale.y = 1; |
| eyeRight.scale.y = 1; |
| blinkState = false; |
| blinkTimer = 0; |
| } |
| |
| 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> |