858 lines
23 KiB
HTML
858 lines
23 KiB
HTML
<!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> |