pet/index.html.bak

858 lines
23 KiB
HTML
Raw Normal View History

2025-11-20 07:01:22 +00:00
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<title>電子寵物 - 頭後方 ZZZ / 💀 + 墓碑</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: radial-gradient(circle at top, #ffe8c7, #f5c7c7);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
/* 外殼 */
.device {
width: 280px;
height: 340px;
border-radius: 60% 60% 55% 55%;
background: radial-gradient(circle at 30% 0%, #ffe7ff, #ffc6f1);
box-shadow:
0 18px 40px rgba(0, 0, 0, 0.35),
inset 0 4px 10px rgba(255, 255, 255, 0.8);
padding-top: 38px;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
}
.device-title {
font-size: 14px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #333;
}
.screen-wrapper {
width: 210px;
height: 160px;
border-radius: 18px;
background: #888;
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
padding: 6px;
}
.screen {
position: relative;
width: 100%;
height: 100%;
border-radius: 10px;
background: #c3e4aa;
overflow: hidden;
box-shadow:
inset 0 0 0 2px rgba(0, 0, 0, 0.25),
inset 0 0 10px rgba(0, 0, 0, 0.35);
}
/* LCD 格線 */
.screen::before {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px);
background-size: 3px 3px;
opacity: 0.55;
pointer-events: none;
}
.buttons {
display: flex;
gap: 16px;
margin-top: 4px;
}
.btn {
width: 30px;
height: 30px;
border-radius: 50%;
background: radial-gradient(circle at 30% 20%, #fff8e7, #f3a3af);
box-shadow:
0 4px 0 #c26c7a,
0 4px 8px rgba(0, 0, 0, 0.35);
}
/* 狀態切換按鈕Demo 用) */
.state-controls {
display: flex;
gap: 8px;
font-size: 12px;
}
.state-controls button {
padding: 4px 10px;
border-radius: 999px;
border: none;
cursor: pointer;
background: #ffffffaa;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.state-controls button.active {
background: #ffb3c6;
}
/* ---------------- 通用寵物 sprite ---------------- */
.pet-root {
position: absolute;
transform-origin: center bottom;
}
.pet-inner {
position: relative;
transform-origin: center bottom;
}
.pet-inner.face-right {
transform: scaleX(1);
}
.pet-inner.face-left {
transform: scaleX(-1);
}
.pet-pixel {
position: absolute;
}
/* ===== 狀態Idle / Sleep / Sick / Dead控制小雞 ===== */
.state-idle .pet-root {
animation:
pet-breathe-idle 2s ease-in-out infinite,
pet-head-tilt 6s ease-in-out infinite;
filter: none;
opacity: 1;
display: block;
}
.state-sleep .pet-root {
animation: pet-breathe-sleep 4s ease-in-out infinite;
filter: none;
opacity: 1;
display: block;
}
.state-sick .pet-root {
animation:
pet-breathe-idle 2s ease-in-out infinite,
pet-sick-shake 0.6s ease-in-out infinite;
filter: brightness(0.8) saturate(0.9);
opacity: 1;
display: block;
}
/* Dead小雞整隻消失由墓碑取代 */
.state-dead .pet-root {
display: none;
}
@keyframes pet-breathe-idle {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.03);
}
}
@keyframes pet-breathe-sleep {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.015);
}
}
@keyframes pet-head-tilt {
0%,
100% {
transform: rotate(0deg) scale(1);
}
25% {
transform: rotate(-2deg) scale(1.02);
}
75% {
transform: rotate(2deg) scale(1.02);
}
}
@keyframes pet-sick-shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-2px);
}
75% {
transform: translateX(2px);
}
}
/* ===== 尾巴 ===== */
.tail-pixel {
transform-origin: center center;
}
.state-idle .tail-pixel {
animation: tail-wag-idle 0.5s ease-in-out infinite;
}
.state-sleep .tail-pixel {
animation: tail-wag-sleep 1.6s ease-in-out infinite;
}
.state-sick .tail-pixel {
animation: tail-wag-sick 0.7s ease-in-out infinite;
}
.state-dead .tail-pixel {
animation: none;
}
@keyframes tail-wag-idle {
0% {
transform: translateX(0px);
}
50% {
transform: translateX(1px);
}
100% {
transform: translateX(0px);
}
}
@keyframes tail-wag-sleep {
0% {
transform: translateX(0px);
}
50% {
transform: translateX(0.5px);
}
100% {
transform: translateX(0px);
}
}
@keyframes tail-wag-sick {
0% {
transform: translateX(0px);
}
50% {
transform: translateX(0.8px);
}
100% {
transform: translateX(0px);
}
}
/* ===== 腳 ===== */
.leg-front,
.leg-back {
transform-origin: center center;
}
.state-idle .leg-front {
animation: leg-front-step 0.6s ease-in-out infinite;
}
.state-idle .leg-back {
animation: leg-back-step 0.6s ease-in-out infinite;
}
.state-sick .leg-front,
.state-sick .leg-back {
animation: leg-sick-step 0.8s ease-in-out infinite;
}
.state-sleep .leg-front,
.state-sleep .leg-back,
.state-dead .leg-front,
.state-dead .leg-back {
animation: none;
}
@keyframes leg-front-step {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-1px);
}
}
@keyframes leg-back-step {
0%,
100% {
transform: translateY(-1px);
}
50% {
transform: translateY(0);
}
}
@keyframes leg-sick-step {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-0.5px);
}
}
/* ===== 耳朵 ===== */
.ear-pixel {
transform-origin: center center;
}
.state-idle .ear-pixel {
animation: ear-twitch 3.2s ease-in-out infinite;
}
.state-sick .ear-pixel {
animation: ear-twitch 4s ease-in-out infinite;
}
.state-sleep .ear-pixel,
.state-dead .ear-pixel {
animation: none;
}
@keyframes ear-twitch {
0%,
90%,
100% {
transform: translateY(0);
}
92% {
transform: translateY(-1px);
}
96% {
transform: translateY(0);
}
}
/* ===== 腮紅 ===== */
.blush-dot {
transform-origin: center center;
}
.state-idle .blush-dot,
.state-sick .blush-dot {
animation: blush-pulse 2s ease-in-out infinite;
}
.state-sleep .blush-dot {
animation: blush-pulse 3s ease-in-out infinite;
}
.state-dead .blush-dot {
animation: none;
}
@keyframes blush-pulse {
0%,
100% {
opacity: 0.7;
}
50% {
opacity: 1;
}
}
/* ===== Sleep ZZZ跟著頭後方跑 ===== */
.sleep-zzz {
position: absolute;
font-weight: bold;
color: #5a7a4a;
pointer-events: none;
opacity: 0;
display: none;
}
.sleep-zzz span {
position: absolute;
font-size: 10px;
opacity: 0;
animation: zzz-float 3s ease-in-out infinite;
}
.sleep-zzz .z1 {
left: 0;
animation-delay: 0s;
}
.sleep-zzz .z2 {
left: 8px;
animation-delay: 0.8s;
}
.sleep-zzz .z3 {
left: 15px;
animation-delay: 1.6s;
}
@keyframes zzz-float {
0% {
opacity: 0;
transform: translateY(0) scale(0.7);
}
20% {
opacity: 1;
transform: translateY(-4px) scale(0.9);
}
80% {
opacity: 1;
transform: translateY(-16px) scale(1.05);
}
100% {
opacity: 0;
transform: translateY(-24px) scale(1.1);
}
}
.state-sleep .sleep-zzz {
display: block;
opacity: 1;
}
.state-dead .sleep-zzz {
display: none;
opacity: 0;
}
/* ===== Sick 骷髏頭(跟著頭後方跑) ===== */
.sick-icon {
position: absolute;
font-size: 14px;
pointer-events: none;
opacity: 0;
display: none;
}
.state-sick .sick-icon {
display: block;
opacity: 1;
animation: sick-icon-pulse 1.2s ease-in-out infinite;
}
.state-sleep .sick-icon,
.state-dead .sick-icon {
display: none;
opacity: 0;
}
@keyframes sick-icon-pulse {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}
/* ===== 墓碑 ===== */
.tombstone {
position: absolute;
left: 50%;
top: 52%;
transform: translate(-50%, -50%);
width: 48px;
height: 56px;
border-radius: 16px 16px 6px 6px;
background: #9ba7a0;
border: 2px solid #5e6861;
box-shadow:
0 0 0 2px rgba(0, 0, 0, 0.2),
0 4px 4px rgba(0, 0, 0, 0.35);
display: none;
}
.tombstone::before {
content: "";
position: absolute;
left: 8px;
right: 8px;
top: 16px;
height: 2px;
background: #5e6861;
}
.tombstone::after {
content: "RIP";
position: absolute;
top: 22px;
left: 50%;
transform: translateX(-50%);
font-size: 8px;
letter-spacing: 1px;
color: #39413c;
}
.state-dead .tombstone {
display: block;
animation: tomb-float 3s ease-in-out infinite;
}
@keyframes tomb-float {
0%,
100% {
transform: translate(-50%, -50%) translateY(0);
}
50% {
transform: translate(-50%, -50%) translateY(-4px);
}
}
</style>
</head>
<body>
<div class="device">
<div class="device-title">PET EGG</div>
<div class="screen-wrapper">
<div class="screen" id="screen">
<!-- 睡覺 ZZZ位置由 JS 根據頭+朝向計算) -->
<div class="sleep-zzz" id="sleepIcon">
<span class="z1">Z</span>
<span class="z2">Z</span>
<span class="z3">Z</span>
</div>
<!-- 生病骷髏頭(位置由 JS 根據頭+朝向計算) -->
<div class="sick-icon" id="sickIcon">💀</div>
<!-- 死亡墓碑 -->
<div class="tombstone"></div>
</div>
</div>
<div class="buttons">
<div class="btn"></div>
<div class="btn"></div>
<div class="btn"></div>
</div>
</div>
<!-- Demo 狀態切換 -->
<div class="state-controls">
<button data-state="idle" class="active">Idle</button>
<button data-state="sleep">Sleep</button>
<button data-state="sick">Sick</button>
<button data-state="dead">Dead</button>
</div>
<script>
/* =======================
1. PRESET
======================= */
const SPRITE_PRESETS = {
tigerChick: {
name: 'tigerChick',
pixelSize: 3,
sprite: [
'1100000111000000',
'1241111331000000',
'1005102301000000',
'1054103320000000',
'1241143320100000',
'1230432311100110',
'1245321330100421',
'1240001311100111',
'1020103030111421',
'0100000331245210',
'0001111111240210',
'0015022221345210',
'0004022235350010',
'0011400023203210',
'0005011112115210',
'0001100001100110',
],
palette: {
'1': '#2b2825',
'2': '#d0974b',
'3': '#e09037',
'4': '#4a2b0d',
'5': '#724e22',
},
tailPixels: [
[15, 8], [14, 8],
[15, 9], [14, 9],
[15, 10], [14, 10],
[15, 11], [14, 11],
],
legFrontPixels: [
[6, 13], [7, 13],
[6, 14], [7, 14],
],
legBackPixels: [
[9, 13], [10, 13],
[9, 14], [10, 14],
],
earPixels: [
[2, 0], [3, 0], [4, 0],
[11, 0], [12, 0], [13, 0],
],
blushPixels: [
[4, 7], [5, 7],
[10, 7], [11, 7],
],
// 頭後方標記點(依朝向不同用不同座標)
iconBackLeft: { x: 3, y: 2 }, // 面向右時:在左邊這個點附近(後腦勺)
iconBackRight: { x: 12, y: 2 }, // 面向左時:在右邊這個點附近(後腦勺)
},
};
const CURRENT_PRESET = SPRITE_PRESETS.tigerChick;
let currentState = 'idle';
/* =======================
2. 建立 sprite
======================= */
function createPetSprite(preset) {
const {
sprite, palette, pixelSize,
tailPixels = [],
legFrontPixels = [],
legBackPixels = [],
earPixels = [],
blushPixels = [],
} = preset;
const rows = sprite.length;
const cols = sprite[0].length;
const width = cols * pixelSize;
const height = rows * pixelSize;
const rootEl = document.createElement('div');
rootEl.className = 'pet-root';
rootEl.style.width = width + 'px';
rootEl.style.height = height + 'px';
const innerEl = document.createElement('div');
innerEl.className = 'pet-inner face-right';
rootEl.appendChild(innerEl);
sprite.forEach((row, y) => {
[...row].forEach((ch, x) => {
if (ch === '0') return;
const dot = document.createElement('div');
const isTail = tailPixels.some(([tx, ty]) => tx === x && ty === y);
const isLegFront = legFrontPixels.some(([lx, ly]) => lx === x && ly === y);
const isLegBack = legBackPixels.some(([lx, ly]) => lx === x && ly === y);
const isEar = earPixels.some(([ex, ey]) => ex === x && ey === y);
const isBlush = blushPixels.some(([bx, by]) => bx === x && by === y);
let className = 'pet-pixel';
if (isTail) className += ' tail-pixel';
if (isLegFront) className += ' leg-front';
if (isLegBack) className += ' leg-back';
if (isEar) className += ' ear-pixel';
if (isBlush) className += ' blush-dot';
dot.className = className;
dot.style.width = pixelSize + 'px';
dot.style.height = pixelSize + 'px';
dot.style.left = (x * pixelSize) + 'px';
dot.style.top = (y * pixelSize) + 'px';
dot.style.background = palette[ch] || '#000';
innerEl.appendChild(dot);
});
});
return { rootEl, innerEl, width, height };
}
/* =======================
3. 初始化 + 移動 + 頭後方 ICON 定位
======================= */
const screenEl = document.getElementById('screen');
const sleepIconEl = document.getElementById('sleepIcon');
const sickIconEl = document.getElementById('sickIcon');
const { rootEl: petRoot, innerEl: petInner } = (function initPetOnScreen(preset, screenEl) {
const { rootEl, innerEl, width, height } = createPetSprite(preset);
screenEl.appendChild(rootEl);
const margin = 8;
let x = Math.floor(screenEl.clientWidth / 2 - width / 2);
let y = Math.floor(screenEl.clientHeight / 2 - height / 2);
rootEl.style.left = x + 'px';
rootEl.style.top = y + 'px';
function updateHeadIconsPosition() {
const { pixelSize, iconBackLeft, iconBackRight } = CURRENT_PRESET;
const isFacingRight = innerEl.classList.contains('face-right');
// 面向右 → icon 用左邊的標記點;面向左 → icon 用右邊的標記點
const marker = isFacingRight ? iconBackLeft : iconBackRight;
const baseX = rootEl.offsetLeft + marker.x * pixelSize;
const baseY = rootEl.offsetTop + marker.y * pixelSize;
// ZZZ 稍微比頭高一點
sleepIconEl.style.left = (baseX - 2) + 'px';
sleepIconEl.style.top = (baseY - 10) + 'px';
// 骷髏頭稍微貼近頭後側一點
sickIconEl.style.left = (baseX - 2) + 'px';
sickIconEl.style.top = (baseY - 6) + 'px';
}
function moveRandomly() {
if (currentState === 'sleep' || currentState === 'dead') {
updateHeadIconsPosition();
return;
}
const step = preset.pixelSize * 2;
const dir = Math.floor(Math.random() * 4);
const oldX = rootEl.offsetLeft;
const oldY = rootEl.offsetTop;
let newX = oldX;
let newY = oldY;
if (dir === 0) newY -= step;
if (dir === 1) newY += step;
if (dir === 2) newX -= step;
if (dir === 3) newX += step;
const maxX = screenEl.clientWidth - width - margin;
const maxY = screenEl.clientHeight - height - margin;
const minX = margin;
const minY = margin;
newX = Math.max(minX, Math.min(maxX, newX));
newY = Math.max(minY, Math.min(maxY, newY));
rootEl.style.left = newX + 'px';
rootEl.style.top = newY + 'px';
const dx = newX - oldX;
if (dx > 0) {
innerEl.classList.remove('face-right');
innerEl.classList.add('face-left');
} else if (dx < 0) {
innerEl.classList.remove('face-left');
innerEl.classList.add('face-right');
}
updateHeadIconsPosition();
}
// 初始定位一次
updateHeadIconsPosition();
// 每 600ms 移動/更新一次
setInterval(moveRandomly, 600);
// 視窗尺寸變化時也重新對頭定位一次
window.addEventListener('resize', updateHeadIconsPosition);
return { rootEl, innerEl };
})(CURRENT_PRESET, screenEl);
/* =======================
4. 狀態切換
======================= */
function setState(newState) {
currentState = newState;
screenEl.classList.remove('state-idle', 'state-sleep', 'state-sick', 'state-dead');
screenEl.classList.add('state-' + newState);
document.querySelectorAll('.state-controls button').forEach(btn => {
btn.classList.toggle('active', btn.dataset.state === newState);
});
}
document.querySelectorAll('.state-controls button').forEach(btn => {
btn.addEventListener('click', () => {
setState(btn.dataset.state);
});
});
setState('idle');
</script>
</body>
</html>