Files
2026-06-14 15:18:56 -04:00

415 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>