forked from Zakaria/hermes-agent
415 lines
11 KiB
HTML
415 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Signal - Digital Pet Companion</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||
background: linear-gradient(135deg, #0a0e27 0%, #0f1a35 50%, #0a0e27 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #e9feff;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
text-align: center;
|
||
max-width: 600px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 28px;
|
||
margin-bottom: 10px;
|
||
color: #7df9ff;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 14px;
|
||
opacity: 0.8;
|
||
margin-bottom: 40px;
|
||
color: #9ee8f3;
|
||
}
|
||
|
||
.pet-container {
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: flex-end;
|
||
height: 300px;
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.digital-pet-avatar {
|
||
width: 120px;
|
||
height: 120px;
|
||
border-radius: 28px 28px 34px 34px;
|
||
border: 2px solid rgba(135, 206, 250, 0.45);
|
||
background: radial-gradient(circle at 40% 25%, rgba(125, 249, 255, 0.24), rgba(6, 16, 28, 0.92));
|
||
display: grid;
|
||
grid-template-rows: 20px 1fr 24px;
|
||
place-items: center;
|
||
cursor: pointer;
|
||
box-shadow: 0 0 24px rgba(74, 222, 255, 0.2), inset 0 0 18px rgba(74, 222, 255, 0.08);
|
||
transition: transform 180ms ease, opacity 180ms ease, filter 180ms ease;
|
||
user-select: none;
|
||
}
|
||
|
||
.digital-pet-avatar:hover {
|
||
outline: 2px solid rgba(125, 249, 255, 0.65);
|
||
outline-offset: 3px;
|
||
transform: translateY(-3px) scale(1.04);
|
||
}
|
||
|
||
.digital-pet-avatar[data-mode="idle"] {
|
||
animation: breathe 3.6s ease-in-out infinite;
|
||
}
|
||
|
||
.digital-pet-avatar[data-mode="sleeping"] {
|
||
opacity: 0.58;
|
||
filter: saturate(0.55);
|
||
animation: none;
|
||
}
|
||
|
||
.antenna {
|
||
color: #7df9ff;
|
||
font-size: 24px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.face {
|
||
font-size: 28px;
|
||
letter-spacing: 2px;
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
width: 70px;
|
||
height: 32px;
|
||
}
|
||
|
||
.eye {
|
||
position: relative;
|
||
width: 24px;
|
||
height: 24px;
|
||
background: #e9feff;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pupil {
|
||
position: absolute;
|
||
width: 10px;
|
||
height: 10px;
|
||
background: #0a0e27;
|
||
border-radius: 50%;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
transition: transform 30ms ease-out;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.pupil::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 4px;
|
||
height: 4px;
|
||
background: rgba(255, 255, 255, 0.4);
|
||
border-radius: 50%;
|
||
top: 2px;
|
||
left: 2px;
|
||
}
|
||
|
||
.sleeping-eye {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
opacity: 0;
|
||
font-size: 16px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.digital-pet-avatar[data-mode="sleeping"] .eye {
|
||
background: transparent;
|
||
}
|
||
|
||
.digital-pet-avatar[data-mode="sleeping"] .pupil {
|
||
opacity: 0;
|
||
}
|
||
|
||
.digital-pet-avatar[data-mode="sleeping"] .sleeping-eye {
|
||
opacity: 1;
|
||
}
|
||
|
||
.digital-pet-avatar[data-mode="hidden"] .eye {
|
||
background: transparent;
|
||
}
|
||
|
||
.digital-pet-avatar[data-mode="hidden"] .pupil {
|
||
opacity: 0;
|
||
}
|
||
|
||
.name {
|
||
font-size: 12px;
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.message-bubble {
|
||
position: absolute;
|
||
bottom: 140px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
max-width: 280px;
|
||
padding: 10px 14px;
|
||
border: 1px solid rgba(135, 206, 250, 0.35);
|
||
border-radius: 12px;
|
||
background: rgba(3, 10, 20, 0.92);
|
||
color: #e9feff;
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.32);
|
||
opacity: 1;
|
||
transition: opacity 300ms ease;
|
||
}
|
||
|
||
.message-bubble.hidden {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 20px;
|
||
border: 1px solid rgba(135, 206, 250, 0.45);
|
||
background: rgba(125, 249, 255, 0.08);
|
||
color: #7df9ff;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
transition: all 200ms ease;
|
||
}
|
||
|
||
button:hover {
|
||
background: rgba(125, 249, 255, 0.15);
|
||
border-color: rgba(125, 249, 255, 0.65);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
button:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.info {
|
||
margin-top: 40px;
|
||
padding: 20px;
|
||
border: 1px solid rgba(135, 206, 250, 0.25);
|
||
border-radius: 12px;
|
||
background: rgba(125, 249, 255, 0.04);
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.info p {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
@keyframes breathe {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-3px); }
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
h1 { font-size: 24px; }
|
||
.digital-pet-avatar { width: 90px; height: 90px; }
|
||
.controls { gap: 10px; }
|
||
button { padding: 8px 16px; font-size: 11px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>Signal</h1>
|
||
<p class="subtitle">A quiet station companion</p>
|
||
|
||
<div class="pet-container">
|
||
<div class="message-bubble hidden" id="messageBubble"></div>
|
||
<button class="digital-pet-avatar" id="petAvatar" data-mode="idle" title="Click Signal">
|
||
<span class="antenna">⌁</span>
|
||
<div class="face" id="faceContainer">
|
||
<div class="eye" id="eyeLeft">
|
||
<div class="pupil" id="pupilLeft"></div>
|
||
<span class="sleeping-eye">−</span>
|
||
</div>
|
||
<div class="eye" id="eyeRight">
|
||
<div class="pupil" id="pupilRight"></div>
|
||
<span class="sleeping-eye">−</span>
|
||
</div>
|
||
</div>
|
||
<span class="name">Signal</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button onclick="petClick()">Click Signal</button>
|
||
<button onclick="toggleMode()">Toggle Sleep</button>
|
||
<button onclick="getRandomMessage()">Show Message</button>
|
||
</div>
|
||
|
||
<div class="info">
|
||
<p><strong>About Signal:</strong> A friendly, calm companion focused on keeping watch without demanding attention.</p>
|
||
<p><strong>Interactions:</strong> Click to react, hover to sleep, or get random messages to see different moods.</p>
|
||
<p><strong>Moods:</strong> calm (•_•), pleased (•‿•), curious (•?•), or tired (-_-)</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const moodData = {
|
||
calm: { messages: ["All quiet.", "Good signal.", "Watching the station.", "Systems feel steady."] },
|
||
pleased: { messages: ["Good signal today.", "Good. Now we know.", "The station holds.", "That helped."] },
|
||
curious: { messages: ["Interesting.", "Small systems matter.", "Antenna aligned."] },
|
||
tired: { messages: ["Still with you.", "Powering down softly."] }
|
||
};
|
||
|
||
let currentState = {
|
||
mode: "idle",
|
||
mood: "calm",
|
||
message: null,
|
||
messageTimeout: null,
|
||
mouseX: 0,
|
||
mouseY: 0
|
||
};
|
||
|
||
const petAvatar = document.getElementById("petAvatar");
|
||
const messageBubble = document.getElementById("messageBubble");
|
||
const pupilLeft = document.getElementById("pupilLeft");
|
||
const pupilRight = document.getElementById("pupilRight");
|
||
|
||
// Track mouse movement
|
||
document.addEventListener("mousemove", (e) => {
|
||
currentState.mouseX = e.clientX;
|
||
currentState.mouseY = e.clientY;
|
||
updateEyeDirection();
|
||
});
|
||
|
||
function updateEyeDirection() {
|
||
// Only update eyes when awake and visible
|
||
if (currentState.mode === "sleeping" || currentState.mode === "hidden") {
|
||
return;
|
||
}
|
||
|
||
const avatarRect = petAvatar.getBoundingClientRect();
|
||
const avatarCenterX = avatarRect.left + avatarRect.width / 2;
|
||
const avatarCenterY = avatarRect.top + avatarRect.height / 2;
|
||
|
||
const deltaX = currentState.mouseX - avatarCenterX;
|
||
const deltaY = currentState.mouseY - avatarCenterY;
|
||
const angle = Math.atan2(deltaY, deltaX);
|
||
|
||
// Calculate pupil offset within the eye (max 7px from center of eye)
|
||
const maxOffset = 7;
|
||
const offsetX = Math.cos(angle) * maxOffset;
|
||
const offsetY = Math.sin(angle) * maxOffset;
|
||
|
||
// Apply offset to both pupils
|
||
pupilLeft.style.transform = `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px))`;
|
||
pupilRight.style.transform = `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px))`;
|
||
}
|
||
|
||
function updateFace() {
|
||
petAvatar.setAttribute("data-mode", currentState.mode);
|
||
// Pupils visibility is handled by CSS based on data-mode
|
||
updateEyeDirection();
|
||
}
|
||
|
||
function showMessage(text) {
|
||
messageBubble.textContent = text;
|
||
messageBubble.classList.remove("hidden");
|
||
currentState.message = text;
|
||
|
||
if (currentState.messageTimeout) clearTimeout(currentState.messageTimeout);
|
||
currentState.messageTimeout = setTimeout(() => {
|
||
messageBubble.classList.add("hidden");
|
||
currentState.message = null;
|
||
}, 3000);
|
||
}
|
||
|
||
function petClick() {
|
||
if (currentState.mode === "sleeping") {
|
||
currentState.mode = "idle";
|
||
showMessage("Back online.");
|
||
} else {
|
||
currentState.mode = "reacting";
|
||
const moods = Object.keys(moodData);
|
||
currentState.mood = moods[Math.floor(Math.random() * moods.length)];
|
||
|
||
const messages = moodData[currentState.mood].messages;
|
||
showMessage(messages[Math.floor(Math.random() * messages.length)]);
|
||
|
||
setTimeout(() => {
|
||
currentState.mode = "idle";
|
||
updateFace();
|
||
}, 1500);
|
||
}
|
||
updateFace();
|
||
}
|
||
|
||
function toggleMode() {
|
||
if (currentState.mode === "sleeping") {
|
||
currentState.mode = "idle";
|
||
showMessage("Quiet watch resumed.");
|
||
} else {
|
||
currentState.mode = "sleeping";
|
||
showMessage("Going quiet.");
|
||
}
|
||
updateFace();
|
||
}
|
||
|
||
function getRandomMessage() {
|
||
const moods = Object.keys(moodData);
|
||
const randomMood = moods[Math.floor(Math.random() * moods.length)];
|
||
const messages = moodData[randomMood].messages;
|
||
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
||
|
||
currentState.mood = randomMood;
|
||
if (currentState.mode !== "sleeping") currentState.mode = "reacting";
|
||
updateFace();
|
||
showMessage(randomMessage);
|
||
|
||
if (currentState.mode === "reacting") {
|
||
setTimeout(() => {
|
||
currentState.mode = "idle";
|
||
updateFace();
|
||
}, 1500);
|
||
}
|
||
}
|
||
|
||
// Initial update
|
||
updateFace();
|
||
</script>
|
||
</body>
|
||
</html>
|