SignApp / src /sign_app /ui /signEngine.js
SearingShot's picture
Deploy SignApp main app
bb5dd0a
Raw
History Blame Contribute Delete
14 kB
import { FINGERSPELL, FINGER_BONE_MAP } from "./fingerspellDictionary.js"
/**
* Sign Engine β€” orchestrates sign playback, movements, and fingerspelling
*/
/* ── State ─────────────────────────────────────────────── */
let _currentSignIndex = -1
let _playing = false
/* ── Exported callbacks (set by main.js) ───────────────── */
export let onSignStart = null // (index, sign) => void
export let onSignEnd = null // () => void
export let onLetterStart = null // (letter) => void
export function setCallbacks({ onSignStart: s, onSignEnd: e, onLetterStart: l }) {
onSignStart = s
onSignEnd = e
onLetterStart = l
}
/* ── Play a full sentence of signs ─────────────────────── */
export function playSentence(signs) {
if (_playing) return
_playing = true
_currentSignIndex = 0
window.signing = true
playNext(signs)
}
function playNext(signs) {
if (_currentSignIndex >= signs.length) {
// Done β€” return to neutral after a brief hold
setTimeout(() => {
returnToNeutral()
_playing = false
_currentSignIndex = -1
window.signing = false
if (onSignEnd) onSignEnd()
}, 800)
return
}
const sign = signs[_currentSignIndex]
if (onSignStart) onSignStart(_currentSignIndex, sign)
if (sign.type === "fingerspell") {
playFingerspellLetter(sign, () => {
_currentSignIndex++
setTimeout(() => playNext(signs), 250) // short gap between letters
})
} else {
playSign(sign, () => {
_currentSignIndex++
setTimeout(() => playNext(signs), 700) // longer gap between signs
})
}
}
/* ── Play a single sign (handshape + location + movement + expression) ── */
function playSign(sign, done) {
// 1. Apply handshape
applyHandshapePose(sign.handshape)
// 2. Apply location (arm positioning)
window.applyLocation(sign.location)
// 3. Apply expression
if (window.applyExpression) {
window.applyExpression(sign.expression || "neutral")
}
// 4. Apply movement animation
const moveDuration = applyMovement(sign.movement)
// Wait for the pose to settle + movement to complete
setTimeout(() => {
if (window.applyExpression) window.applyExpression("neutral")
done()
}, Math.max(600, moveDuration + 200))
}
/* ── Fingerspell a single letter ───────────────────────── */
function playFingerspellLetter(sign, done) {
const letter = (sign.letter || sign.handshape || "").toUpperCase()
if (onLetterStart) onLetterStart(letter)
const pose = FINGERSPELL[letter]
if (!pose) {
done()
return
}
// Apply full multi-joint fingerspell pose
for (const key in pose) {
const boneName = FINGER_BONE_MAP[key]
if (boneName && window.targetRotations !== undefined) {
window.targetRotations[boneName] = { ...pose[key] }
}
}
// Ensure neutral hand position for fingerspelling
window.applyLocation("neutral_space")
setTimeout(done, 450) // hold each letter
}
/* ── Handshape pose application ────────────────────────── */
function applyHandshapePose(shape) {
if (!shape) return
// Check if this shape exists in FINGERSPELL dictionary first (reuse finger poses)
const shapeUpper = shape.toUpperCase()
if (FINGERSPELL[shapeUpper]) {
const pose = FINGERSPELL[shapeUpper]
for (const key in pose) {
const boneName = FINGER_BONE_MAP[key]
if (boneName && window.targetRotations !== undefined) {
window.targetRotations[boneName] = { ...pose[key] }
}
}
return
}
// Built-in handshape aliases
if (window.applyHandshape) {
window.applyHandshape(shapeUpper)
}
}
/* ── Movement animations ───────────────────────────────── */
/**
* Apply a movement animation. Returns duration in ms.
*/
function applyMovement(movement) {
if (!movement || movement === "none") return 0
switch (movement.toLowerCase()) {
case "tap": return animateTap()
case "double_tap": return animateDoubleTap()
case "circle_clockwise": return animateCircle(1)
case "circle_counterclockwise": return animateCircle(-1)
case "forward": return animateForward()
case "pull_in": return animatePullIn()
case "down": return animateDown()
case "up": return animateUp()
case "side_to_side": return animateSideToSide()
case "nod": return animateNod()
case "twist": return animateTwist()
case "wave": return animateWave()
case "touch": return animateTouch()
default:
console.log("Unknown movement:", movement)
return 0
}
}
/* ── Movement implementations ──────────────────────────── */
function animateTap() {
const boneR = "RightForeArm"
const boneL = "LeftForeArm"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x - 0.15 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x - 0.15 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 200)
return 400
}
function animateDoubleTap() {
const boneR = "RightForeArm"
const boneL = "LeftForeArm"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
const applyTap = () => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x - 0.15 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x - 0.15 }
}
const applyReset = () => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}
applyTap()
setTimeout(() => {
applyReset()
setTimeout(() => {
applyTap()
setTimeout(() => applyReset(), 150)
}, 200)
}, 150)
return 700
}
function animateCircle(direction) {
const boneR = "RightHand"
const boneL = "LeftHand"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
const steps = 8
const radius = 0.2
let step = 0
const interval = setInterval(() => {
const angle = (step / steps) * Math.PI * 2 * direction
if (window.targetRotations[boneR]) {
window.targetRotations[boneR] = {
x: currentR.x + Math.sin(angle) * radius,
y: currentR.y + Math.cos(angle) * radius,
z: currentR.z
}
}
if (window.targetRotations[boneL]) {
// Mirror circle on left hand (invert Y)
window.targetRotations[boneL] = {
x: currentL.x + Math.sin(angle) * radius,
y: currentL.y - Math.cos(angle) * radius,
z: currentL.z
}
}
step++
if (step >= steps) {
clearInterval(interval)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}
}, 80)
return steps * 80 + 100
}
function animateForward() {
const boneR = "RightForeArm"
const boneL = "LeftForeArm"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x - 0.3 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x - 0.3 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 350)
return 500
}
function animatePullIn() {
const boneR = "RightForeArm"
const boneL = "LeftForeArm"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x + 0.3 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x + 0.3 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 350)
return 500
}
function animateDown() {
const boneR = "RightArm"
const boneL = "LeftArm"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x + 0.3 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x + 0.3 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 350)
return 500
}
function animateUp() {
const boneR = "RightArm"
const boneL = "LeftArm"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x - 0.3 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x - 0.3 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 350)
return 500
}
function animateSideToSide() {
const boneR = "RightHand"
const boneL = "LeftHand"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, z: currentR.z - 0.2 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, z: currentL.z + 0.2 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, z: currentR.z + 0.2 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, z: currentL.z - 0.2 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 200)
}, 200)
return 600
}
function animateNod() {
const boneR = "RightHand"
const boneL = "LeftHand"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x + 0.25 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x + 0.25 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 250)
return 450
}
function animateTwist() {
const boneR = "RightHand"
const boneL = "LeftHand"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, y: currentR.y + 0.4 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, y: currentL.y - 0.4 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, y: currentR.y - 0.4 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, y: currentL.y + 0.4 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 200)
}, 250)
return 650
}
function animateWave() {
const boneR = "RightHand"
const boneL = "LeftHand"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
let step = 0
const steps = 6
const interval = setInterval(() => {
const val = Math.sin(step * 1.2) * 0.2
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, z: currentR.z + val }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, z: currentL.z - val }
step++
if (step >= steps) {
clearInterval(interval)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}
}, 100)
return steps * 100 + 100
}
function animateTouch() {
const boneR = "RightForeArm"
const boneL = "LeftForeArm"
const currentR = getCurrentRotation(boneR)
const currentL = getCurrentRotation(boneL)
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x + 0.15 }
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x + 0.15 }
setTimeout(() => {
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
}, 200)
return 400
}
/* ── Helpers ───────────────────────────────────────────── */
function getCurrentRotation(boneName) {
if (window.targetRotations && window.targetRotations[boneName]) {
return { ...window.targetRotations[boneName] }
}
return { x: 0, y: 0, z: 0 }
}
function returnToNeutral() {
if (window.clearPose) window.clearPose()
}