import {TTS} from '../tts.mjs'; class AvatarComponent extends HTMLElement { static get observedAttributes() { return []; } constructor() { super(); this.attachShadow({mode: 'open'}); this.activeEmotes = []; this.mouthSvg = null; this.mouthState = 'closed'; this.setupMouthAnimation(); this.navi = window.navi; this.navi.animations().then(animations => { this.animations = animations; if(!this.animations) return console.error(`Invalid animations: ${this.animations}`); this.render(this.animations); navi.on('emote', emote => this.emote(emote)); }); window.emote = this.emote.bind(this); } render(data) { this.shadowRoot.innerHTML = `
Avatar
`; this.loadMouthSvg(); } loadMouthSvg() { fetch('/emotes/mouth.svg').then(r => r.text()).then(svg => { const container = document.createElement('div'); container.className = 'mouth-overlay'; container.innerHTML = svg; this.mouthSvg = container.firstElementChild; const mouthPos = this.animations?.emote?.['mouth'] || {x: 50, y: 60, r: 0}; container.style.left = `${mouthPos.x}%`; container.style.top = `${mouthPos.y}%`; container.style.transform = `translate(-50%, -50%) rotate(${mouthPos.r || 0}deg)`; container.style.width = '50px'; container.style.height = '25px'; const avatarContainer = this.shadowRoot.querySelector('.avatar-container'); if(avatarContainer) avatarContainer.appendChild(container); }); } setupMouthAnimation() { const tts = TTS.getInstance(); let mouthAnimationInterval = null; tts.on('onSentenceStart', () => { if(mouthAnimationInterval) return; const next = () => { mouthAnimationInterval = setTimeout(() => { next(); this.toggleMouthState(); }, ~~(Math.random() * 100) + 100); } next(); }); tts.on('onSentenceEnd', () => this.setMouthState('closed')); tts.on('onComplete', () => { if(mouthAnimationInterval) { clearTimeout(mouthAnimationInterval); mouthAnimationInterval = null; } this.setMouthState('closed'); }); } toggleMouthState() { if(!this.mouthSvg) return; this.setMouthState(this.mouthState === 'open' ? 'partial' : 'open'); } setMouthState(state) { if(!this.mouthSvg) return; this.mouthState = state; this.mouthSvg.classList.remove('closed', 'partial', 'open'); this.mouthSvg.classList.add(state); } clear(all = true) { if(all) { const a = this.shadowRoot.querySelector('.avatar'); a.animate([{filter: 'drop-shadow(2px 4px 6px black) grayscale(0%) brightness(100%)'}], {duration: 100, fill: 'forwards'}); } this.activeEmotes.forEach(e => e.remove()); this.activeEmotes = []; } emote(emote) { const animate = (e, emote, style, index) => { const duration = 3000; if(emote === 'blush') { e.animate([ {transform: `scale(0.75) rotate(${style.r ?? 0}deg)`, opacity: 0}, {transform: `scale(0.75) rotate(${style.r ?? 0}deg)`, opacity: 1} ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); } else if(emote === 'cry') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-25%) scale(0)`}, {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.5)`} ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); } else if(emote === 'dead') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(0)`}, {transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-10%)`} ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); setTimeout(() => { e.animate([ {transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-10%)`}, {transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-8%)`} ], {duration: 1500, easing: 'ease-in-out', iterations: Infinity, direction: 'alternate'}); }, duration); } else if(emote === 'drool') { e.src = `${this.navi.api}/emotes/tear.png`; e.animate([ {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-12.5%) scale(0)`}, {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.25)`} ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); } else if(emote === 'love') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) scale(0.5)`}, {transform: `rotate(${style.r ?? 0}deg) scale(0.7)`} ], {duration: 200, easing: 'steps(2, jump-end)', iterations: Infinity, direction: 'alternate'}); } else if(emote === 'question') { e.style.transform = `rotate(${style.r ?? 0}deg)`; e.animate([ {opacity: 1, offset: 0}, {opacity: 1, offset: 0.49}, {opacity: 0, offset: 0.5}, {opacity: 0, offset: 1} ], {duration: 200, iterations: Infinity, direction: 'alternate', delay: (index % 2) * 200}); } else if(emote === 'realize') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) scale(0.9)`}, {transform: `rotate(${style.r ?? 0}deg) scale(1.1)`} ], {duration: 500, easing: 'ease-out', iterations: Infinity, direction: 'alternate'}); } else if(emote === 'stars') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) scale(0.25)`}, {transform: `rotate(${style.r ?? 0}deg) scale(0.3)`} ], {duration: 100, easing: 'steps(2, jump-end)', iterations: Infinity, direction: 'alternate'}); } else if(emote === 'stress') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) scale(0.9)`}, {transform: `rotate(${style.r ?? 0}deg) scale(1.1)`} ], {duration: 333, easing: 'ease-out', iterations: Infinity, direction: 'alternate'}); } else if(emote === 'sigh') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) translate(0, 0)`}, {transform: `rotate(${style.r ?? 0}deg) translate(10%, 10%)`} ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); } else if(emote === 'sweat') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) scale(0.5) translateY(0)`}, {transform: `rotate(${style.r ?? 0}deg) scale(0.5) translateY(20%)`} ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); } else if(emote === 'tear') { e.animate([ {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-15%) scale(0)`}, {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.3)`} ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); } }; if(!emote || emote === 'none') return this.clear(); if(!this.animations.emote[emote]) throw new Error(`Invalid animation: ${emote}`); const pos = this.animations.emote[emote]; this.clear(false); const a = this.shadowRoot.querySelector('.avatar'); const container = this.shadowRoot.querySelector('.avatar-container'); const positions = Array.isArray(pos) ? pos : (pos.x != null ? [pos] : []); if(['dead', 'grey'].includes(emote)) { a.animate([ {filter: 'drop-shadow(2px 4px 6px black) grayscale(100%) brightness(150%)'} ], {duration: 100, fill: 'forwards'}); } else { a.animate([ {filter: 'drop-shadow(2px 4px 6px black) grayscale(0%) brightness(100%)'} ], {duration: 100, fill: 'forwards'}); } positions.forEach((p, i) => { const e = document.createElement('img'); e.className = 'emote-overlay'; e.src = `${this.navi.api}/emotes/${emote}.png`; e.style.top = `${p.y}%`; e.style.left = `${p.x}%`; container.appendChild(e); this.activeEmotes.push(e); animate(e, emote, p, i); }); } } customElements.define('avatar-component', AvatarComponent);