- Rotating optimal faces with paired animations (breathing, floating, bouncing, swaying) - Occasional idle expressions (winks/blinks) with 15% chance - Recovery celebration emote with bounce animation - Connection lost state with searching animation - Face rotation every 5 minutes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
224 lines
5.8 KiB
HTML
224 lines
5.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>Sentry-Emote</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
background: #000000;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-family: monospace;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#emote {
|
|
font-size: 18vw;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
#message {
|
|
font-size: 4vw;
|
|
margin-top: 2vh;
|
|
opacity: 0.7;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Breathing animation - slow pulse */
|
|
.breathing {
|
|
animation: breathe 3s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes breathe {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.7;
|
|
transform: scale(0.98);
|
|
}
|
|
}
|
|
|
|
/* Shaking animation - rapid jitter for Critical */
|
|
.shaking {
|
|
animation: shake 0.15s linear infinite;
|
|
}
|
|
|
|
@keyframes shake {
|
|
0%, 100% { transform: translateX(0); }
|
|
25% { transform: translateX(-5px); }
|
|
75% { transform: translateX(5px); }
|
|
}
|
|
|
|
/* Popping animation - scale up for Notifications */
|
|
.popping {
|
|
animation: pop 1s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pop {
|
|
0%, 100% {
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
transform: scale(1.08);
|
|
}
|
|
}
|
|
|
|
/* Celebrating animation - bounce and wiggle */
|
|
.celebrating {
|
|
animation: celebrate 0.5s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes celebrate {
|
|
0%, 100% {
|
|
transform: translateY(0) rotate(0deg);
|
|
}
|
|
25% {
|
|
transform: translateY(-10px) rotate(-5deg);
|
|
}
|
|
75% {
|
|
transform: translateY(-10px) rotate(5deg);
|
|
}
|
|
}
|
|
|
|
/* Floating animation - gentle drift */
|
|
.floating {
|
|
animation: float 4s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% {
|
|
transform: translateY(0);
|
|
}
|
|
50% {
|
|
transform: translateY(-8px);
|
|
}
|
|
}
|
|
|
|
/* Bouncing animation - playful hop */
|
|
.bouncing {
|
|
animation: bounce 1s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes bounce {
|
|
0%, 100% {
|
|
transform: translateY(0);
|
|
}
|
|
50% {
|
|
transform: translateY(-12px);
|
|
}
|
|
}
|
|
|
|
/* Swaying animation - curious side-to-side */
|
|
.swaying {
|
|
animation: sway 3s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes sway {
|
|
0%, 100% {
|
|
transform: rotate(0deg);
|
|
}
|
|
25% {
|
|
transform: rotate(-3deg);
|
|
}
|
|
75% {
|
|
transform: rotate(3deg);
|
|
}
|
|
}
|
|
|
|
/* Blink animation - quick fade for winks */
|
|
.blink {
|
|
animation: blink 0.3s ease-in-out 1;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.3;
|
|
}
|
|
}
|
|
|
|
/* Searching animation - looking around for connection */
|
|
.searching {
|
|
animation: search 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes search {
|
|
0%, 100% {
|
|
transform: translateX(0);
|
|
opacity: 0.6;
|
|
}
|
|
25% {
|
|
transform: translateX(-10px);
|
|
opacity: 0.8;
|
|
}
|
|
75% {
|
|
transform: translateX(10px);
|
|
opacity: 0.8;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="emote" class="breathing">( ^_^)</div>
|
|
<div id="message">Loading...</div>
|
|
|
|
<script>
|
|
const emoteEl = document.getElementById('emote');
|
|
const messageEl = document.getElementById('message');
|
|
const POLL_INTERVAL = 2000;
|
|
|
|
async function fetchStatus() {
|
|
try {
|
|
const response = await fetch('/status');
|
|
if (!response.ok) throw new Error('Failed to fetch');
|
|
const data = await response.json();
|
|
updateDisplay(data);
|
|
} catch (err) {
|
|
// Connection lost state
|
|
emoteEl.textContent = '( ?.?)';
|
|
emoteEl.style.color = '#888888';
|
|
emoteEl.className = 'searching';
|
|
messageEl.style.color = '#888888';
|
|
messageEl.textContent = '';
|
|
}
|
|
}
|
|
|
|
function updateDisplay(data) {
|
|
emoteEl.textContent = data.active_emote;
|
|
emoteEl.style.color = data.color;
|
|
messageEl.style.color = data.color;
|
|
|
|
// Show event message if available, otherwise show state
|
|
const topEvent = data.active_events && data.active_events[0];
|
|
messageEl.textContent = (topEvent && topEvent.message) || data.message;
|
|
|
|
// Update animation class
|
|
emoteEl.className = '';
|
|
if (data.animation) {
|
|
emoteEl.classList.add(data.animation);
|
|
}
|
|
}
|
|
|
|
// Initial fetch and start polling
|
|
fetchStatus();
|
|
setInterval(fetchStatus, POLL_INTERVAL);
|
|
</script>
|
|
</body>
|
|
</html>
|