|
|
<!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/[email protected]/build/three.min.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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> |
|
|
|