pet/index.html.bak

858 lines
23 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="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>