| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Lip‑Sync Ark Avatar</title> |
|
|
| |
| |
| |
| <style> |
| :root { |
| --accent‑1: #6965db; |
| --accent‑2: #3a86ff; |
| --bg‑dark : #0f0f1a; |
| --text‑lite: #e5e5f7; |
| } |
| |
| * { box‑sizing: border‑box; margin: 0; padding: 0; } |
| html,body { height: 100%; overflow: hidden; font‑family: 'Segoe UI', Tahoma, sans‑serif; background: var(--bg‑dark); color: var(--text‑lite); } |
| |
| |
| #three‑canvas { position: fixed; inset: 0; z‑index: 1; } |
| |
| |
| #ui { position: fixed; left: 0; right: 0; bottom: 2rem; display: flex; justify‑content: center; gap: 1rem; z‑index: 2; } |
| button { |
| padding: .8rem 1.6rem; border: none; border‑radius: 40px; |
| background: linear‑gradient(100deg,var(--accent‑1),var(--accent‑2)); |
| color: #fff; font‑size: 1rem; font‑weight: 600; cursor: pointer; |
| box‑shadow: 0 4px 15px rgba(0,0,0,.25); transition: transform .2s; |
| } |
| button:hover { transform: translateY(-3px); } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <canvas id="three‑canvas"></canvas> |
|
|
| |
| <div id="ui"> |
| <button id="speakBtn">Say it 👉 “Hello I’m your personal assistant”</button> |
| </div> |
|
|
| |
| <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/controls/OrbitControls.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/loaders/GLTFLoader.js"></script> |
|
|
| <script> |
| |
| |
| |
| const canvas = document.getElementById('three‑canvas'); |
| const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true }); |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio,2)); |
| const scene = new THREE.Scene(); |
| |
| |
| const camera = new THREE.PerspectiveCamera(35, window.innerWidth/window.innerHeight, 0.1, 100); |
| camera.position.set(0, 1.55, 3.5); |
| |
| |
| const controls = new THREE.OrbitControls(camera, canvas); |
| controls.enableDamping = true; |
| |
| |
| function onResize(){ |
| camera.aspect = window.innerWidth / window.innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| } |
| window.addEventListener('resize', onResize); |
| onResize(); |
| |
| |
| |
| |
| |
| const bgTex = new THREE.TextureLoader().load('bg.jpg', tex=>{ tex.encoding = THREE.sRGBEncoding; }); |
| const bgMat = new THREE.MeshBasicMaterial({ map:bgTex }); |
| const bgGeo = new THREE.PlaneGeometry(16, 9); |
| const bg = new THREE.Mesh(bgGeo, bgMat); |
| bg.position.z = -5; |
| bg.scale.set(2,2,1); |
| scene.add(bg); |
| |
| |
| |
| |
| |
| |
| let avatar, mouthIndex = null; |
| |
| const loader = new THREE.GLTFLoader(); |
| loader.load('avatar.glb', gltf=>{ |
| avatar = gltf.scene; |
| avatar.traverse(obj=>{ |
| if (obj.isMesh && obj.morphTargetDictionary) { |
| |
| const dict = obj.morphTargetDictionary; |
| const possible = ['viseme_aa','mouthOpen','jawOpen','vrc.v_morph_aa']; |
| for(const key of possible){ if(key in dict){ mouthIndex = dict[key]; break; } } |
| if(mouthIndex!==null){ obj.userData.isMouth = true; } |
| } |
| }); |
| |
| |
| const box = new THREE.Box3().setFromObject(avatar); |
| const size = new THREE.Vector3(); box.getSize(size); |
| avatar.scale.setScalar(1.6/size.y); |
| box.setFromObject(avatar); |
| const center = new THREE.Vector3(); box.getCenter(center); |
| avatar.position.sub(center); avatar.position.y -= box.min.y; |
| |
| scene.add(avatar); |
| }); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const mouthAnim = { |
| strength: 0 |
| }; |
| |
| function speak(text){ |
| if(!window.speechSynthesis) return alert('SpeechSynthesis unsupported'); |
| const utter = new SpeechSynthesisUtterance(text); |
| utter.lang = 'en-US'; |
| utter.rate = 1; |
| utter.pitch = 1; |
| utter.onboundary = ({ name }) => { |
| if(name === 'word') { |
| |
| mouthAnim.strength = 1; |
| } |
| }; |
| window.speechSynthesis.speak(utter); |
| } |
| |
| |
| document.getElementById('speakBtn').addEventListener('click', ()=>{ |
| speak("Hello I'm your personal assistant"); |
| }); |
| |
| |
| |
| |
| |
| const clock = new THREE.Clock(); |
| function tick(){ |
| requestAnimationFrame(tick); |
| const dt = clock.getDelta(); |
| |
| |
| mouthAnim.strength = THREE.MathUtils.damp(mouthAnim.strength, 0, 5, dt); |
| |
| if(avatar && mouthIndex!==null){ |
| avatar.traverse(obj=>{ |
| if(obj.userData.isMouth){ |
| obj.morphTargetInfluences[mouthIndex] = mouthAnim.strength; |
| } |
| }); |
| } |
| |
| controls.update(); |
| renderer.render(scene, camera); |
| } |
| tick(); |
| </script> |
| </body> |
| </html> |
|
|