generated from ztimson/template
All checks were successful
Build and publish / Build Container (push) Successful in 1m28s
238 lines
8.2 KiB
JavaScript
238 lines
8.2 KiB
JavaScript
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 = `
|
|
<style>
|
|
:host {
|
|
display: block;
|
|
position: relative;
|
|
pointer-events: none;
|
|
width: 500px;
|
|
height: 500px;
|
|
}
|
|
.avatar-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.avatar {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
filter: drop-shadow(2px 4px 6px black);
|
|
}
|
|
.emote-overlay {
|
|
position: absolute;
|
|
width: auto;
|
|
height: auto;
|
|
object-fit: contain;
|
|
pointer-events: none;
|
|
}
|
|
.mouth-overlay {
|
|
position: absolute;
|
|
pointer-events: none;
|
|
z-index: 10;
|
|
}
|
|
</style>
|
|
<div class="avatar-container">
|
|
<img src="${this.navi.avatar}" class="avatar" alt="Avatar">
|
|
</div>`;
|
|
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);
|