Compare commits
2 Commits
c6913c611d
...
e82151daa0
| Author | SHA1 | Date | |
|---|---|---|---|
| e82151daa0 | |||
| 8d609db90e |
@@ -23,7 +23,7 @@ CELEBRATION_DURATION = 5 # Seconds to show celebration after recovery
|
|||||||
# Emote variations with paired animations
|
# Emote variations with paired animations
|
||||||
OPTIMAL_EMOTES = [
|
OPTIMAL_EMOTES = [
|
||||||
("( ^_^)", "breathing"), # calm, content
|
("( ^_^)", "breathing"), # calm, content
|
||||||
("( ᵔᴥᵔ)", "floating"), # dreamy
|
("( ˙▿˙)", "floating"), # content
|
||||||
("(◕‿◕)", "bouncing"), # cheerful
|
("(◕‿◕)", "bouncing"), # cheerful
|
||||||
("( ・ω・)", "swaying"), # curious
|
("( ・ω・)", "swaying"), # curious
|
||||||
("( ˘▽˘)", "breathing"), # cozy
|
("( ˘▽˘)", "breathing"), # cozy
|
||||||
@@ -50,6 +50,9 @@ active_events = {} # id -> {priority, message, timestamp, ttl}
|
|||||||
# State tracking for personality
|
# State tracking for personality
|
||||||
previous_priority = 4
|
previous_priority = 4
|
||||||
celebrating_until = 0
|
celebrating_until = 0
|
||||||
|
blinking_until = 0
|
||||||
|
blink_emote = None
|
||||||
|
blink_animation = None
|
||||||
last_emote_change = 0
|
last_emote_change = 0
|
||||||
current_optimal_emote = OPTIMAL_EMOTES[0][0]
|
current_optimal_emote = OPTIMAL_EMOTES[0][0]
|
||||||
current_optimal_animation = OPTIMAL_EMOTES[0][1]
|
current_optimal_animation = OPTIMAL_EMOTES[0][1]
|
||||||
@@ -63,7 +66,8 @@ SLEEP_ANIMATION = "sleeping"
|
|||||||
|
|
||||||
def get_current_state():
|
def get_current_state():
|
||||||
"""Determine current state based on active events."""
|
"""Determine current state based on active events."""
|
||||||
global previous_priority, celebrating_until, last_emote_change, current_optimal_emote, current_optimal_animation
|
global previous_priority, celebrating_until, blinking_until, blink_emote, blink_animation
|
||||||
|
global last_emote_change, current_optimal_emote, current_optimal_animation
|
||||||
|
|
||||||
# Sleep mode overrides everything
|
# Sleep mode overrides everything
|
||||||
if is_sleeping:
|
if is_sleeping:
|
||||||
@@ -107,15 +111,19 @@ def get_current_state():
|
|||||||
if now < celebrating_until:
|
if now < celebrating_until:
|
||||||
# Celebration mode
|
# Celebration mode
|
||||||
emote, animation = CELEBRATION_EMOTE
|
emote, animation = CELEBRATION_EMOTE
|
||||||
|
elif now < blinking_until:
|
||||||
|
# Brief blink/wink (1-2 seconds)
|
||||||
|
emote = blink_emote
|
||||||
|
animation = blink_animation
|
||||||
else:
|
else:
|
||||||
# Rotate optimal emotes every 5 minutes, occasional idle expression
|
# Rotate optimal emotes every 5 minutes
|
||||||
if now - last_emote_change > 300:
|
if now - last_emote_change > 300:
|
||||||
last_emote_change = now
|
last_emote_change = now
|
||||||
# 15% chance of an idle expression (wink/blink)
|
|
||||||
if random.random() < 0.15:
|
|
||||||
current_optimal_emote, current_optimal_animation = random.choice(IDLE_EMOTES)
|
|
||||||
else:
|
|
||||||
current_optimal_emote, current_optimal_animation = random.choice(OPTIMAL_EMOTES)
|
current_optimal_emote, current_optimal_animation = random.choice(OPTIMAL_EMOTES)
|
||||||
|
# 15% chance of a brief blink/wink
|
||||||
|
if random.random() < 0.15:
|
||||||
|
blink_emote, blink_animation = random.choice(IDLE_EMOTES)
|
||||||
|
blinking_until = now + random.uniform(1, 2)
|
||||||
emote = current_optimal_emote
|
emote = current_optimal_emote
|
||||||
animation = current_optimal_animation
|
animation = current_optimal_animation
|
||||||
|
|
||||||
|
|||||||
92
index.html
92
index.html
@@ -199,6 +199,95 @@
|
|||||||
const messageEl = document.getElementById('message');
|
const messageEl = document.getElementById('message');
|
||||||
const POLL_INTERVAL = 2000;
|
const POLL_INTERVAL = 2000;
|
||||||
|
|
||||||
|
// Sound system
|
||||||
|
let audioCtx = null;
|
||||||
|
let soundEnabled = new URLSearchParams(window.location.search).get('sound') === 'on';
|
||||||
|
let lastState = null;
|
||||||
|
|
||||||
|
function initAudio() {
|
||||||
|
if (!audioCtx) {
|
||||||
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
}
|
||||||
|
if (audioCtx.state === 'suspended') {
|
||||||
|
audioCtx.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playTone(frequency, duration, type = 'sine', volume = 0.1) {
|
||||||
|
if (!soundEnabled || !audioCtx) return;
|
||||||
|
const osc = audioCtx.createOscillator();
|
||||||
|
const gain = audioCtx.createGain();
|
||||||
|
osc.type = type;
|
||||||
|
osc.frequency.value = frequency;
|
||||||
|
gain.gain.value = volume;
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(audioCtx.destination);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(audioCtx.currentTime + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playWarningSound() {
|
||||||
|
// Soft double-beep
|
||||||
|
playTone(440, 0.15);
|
||||||
|
setTimeout(() => playTone(440, 0.15), 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playCriticalSound() {
|
||||||
|
// Urgent descending tone
|
||||||
|
playTone(600, 0.2);
|
||||||
|
setTimeout(() => playTone(400, 0.3), 220);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playNotifySound() {
|
||||||
|
// Gentle ping
|
||||||
|
playTone(880, 0.1, 'sine', 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playRecoverySound() {
|
||||||
|
// Happy ascending chirp
|
||||||
|
playTone(523, 0.1);
|
||||||
|
setTimeout(() => playTone(659, 0.1), 100);
|
||||||
|
setTimeout(() => playTone(784, 0.15), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStateChange(newState, newEmote) {
|
||||||
|
if (!lastState) {
|
||||||
|
lastState = newState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State transitions that trigger sounds
|
||||||
|
if (newState !== lastState) {
|
||||||
|
if (newState === 'critical') {
|
||||||
|
playCriticalSound();
|
||||||
|
} else if (newState === 'warning') {
|
||||||
|
playWarningSound();
|
||||||
|
} else if (newState === 'notify') {
|
||||||
|
playNotifySound();
|
||||||
|
} else if (newState === 'optimal' && lastState !== 'optimal' && lastState !== 'sleeping') {
|
||||||
|
// Recovery - also check for celebration emote
|
||||||
|
playRecoverySound();
|
||||||
|
}
|
||||||
|
lastState = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable sound on first tap (browser autoplay policy)
|
||||||
|
document.body.addEventListener('click', () => {
|
||||||
|
if (!soundEnabled) {
|
||||||
|
soundEnabled = true;
|
||||||
|
initAudio();
|
||||||
|
// Brief confirmation chirp
|
||||||
|
playTone(660, 0.08, 'sine', 0.05);
|
||||||
|
}
|
||||||
|
}, { once: false });
|
||||||
|
|
||||||
|
// Also init if ?sound=on
|
||||||
|
if (soundEnabled) {
|
||||||
|
document.addEventListener('DOMContentLoaded', initAudio);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchStatus() {
|
async function fetchStatus() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/status');
|
const response = await fetch('/status');
|
||||||
@@ -216,6 +305,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateDisplay(data) {
|
function updateDisplay(data) {
|
||||||
|
// Check for state changes and play sounds
|
||||||
|
handleStateChange(data.current_state, data.active_emote);
|
||||||
|
|
||||||
emoteEl.textContent = data.active_emote;
|
emoteEl.textContent = data.active_emote;
|
||||||
emoteEl.style.color = data.color;
|
emoteEl.style.color = data.color;
|
||||||
messageEl.style.color = data.color;
|
messageEl.style.color = data.color;
|
||||||
|
|||||||
Reference in New Issue
Block a user