generated from ztimson/template
Home screen update
All checks were successful
Build and publish / Build Container (push) Successful in 1m28s
All checks were successful
Build and publish / Build Container (push) Successful in 1m28s
This commit is contained in:
237
public/components/avatar.mjs
Normal file
237
public/components/avatar.mjs
Normal file
@@ -0,0 +1,237 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user