@ganziliang/desktop-pet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/assets/awaken/README.md +82 -0
- package/assets/awaken/astral-burst.png +0 -0
- package/assets/awaken/astral-charge.png +0 -0
- package/assets/awaken/nocturne-burst.png +0 -0
- package/assets/awaken/nocturne-charge.png +0 -0
- package/assets/awaken/seraph-burst.png +0 -0
- package/assets/awaken/seraph-charge.png +0 -0
- package/assets/awaken/thunder-burst.png +0 -0
- package/assets/awaken/thunder-charge.png +0 -0
- package/assets/outfits.json +18 -0
- package/assets/pet-base.png +0 -0
- package/assets/pet-blink.png +0 -0
- package/assets/pet-idle-blink.png +0 -0
- package/assets/pet-idle.png +0 -0
- package/assets/pet-run.png +0 -0
- package/assets/pet-swim.png +0 -0
- package/assets/pet-walk.png +0 -0
- package/assets/sprites.json +132 -0
- package/bin/desktop-pet.js +43 -0
- package/examples/pi-pet-bridge.ts +101 -0
- package/main.js +1143 -0
- package/package.json +58 -0
- package/preload.js +29 -0
- package/renderer/app.js +1219 -0
- package/renderer/awaken.js +775 -0
- package/renderer/fx.js +1057 -0
- package/renderer/index.html +43 -0
- package/renderer/style.css +599 -0
- package/scripts/debug-run.ps1 +6 -0
- package/scripts/drop-probe.ps1 +120 -0
- package/scripts/hold-probe.ps1 +86 -0
- package/scripts/knockout.py +98 -0
- package/scripts/roam-probe.ps1 +95 -0
- package/scripts/sim-drag.ps1 +107 -0
- package/scripts/spritesheet.py +425 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data:; img-src 'self' data:; style-src 'self' 'unsafe-inline'" />
|
|
6
|
+
<title>桌面小控件</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="stage" class="stage">
|
|
11
|
+
<div id="hearts" class="hearts"></div>
|
|
12
|
+
|
|
13
|
+
<div id="bubble" class="bubble">
|
|
14
|
+
<span id="bubble-text"></span>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div id="anchor" class="anchor">
|
|
18
|
+
<div id="facer" class="facer">
|
|
19
|
+
<div id="character" class="character">
|
|
20
|
+
<div id="sprite" class="sprite" aria-hidden="true"></div>
|
|
21
|
+
<div class="shadow"></div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- 游泳时的水面:盖在角色下半身之上,顶边是动画波浪 -->
|
|
27
|
+
<div id="water" class="water" aria-hidden="true"></div>
|
|
28
|
+
|
|
29
|
+
<div id="hint" class="hint">拖我 ~</div>
|
|
30
|
+
|
|
31
|
+
<!--
|
|
32
|
+
特效层:盖在角色之上。
|
|
33
|
+
暗幕(觉醒的黑屏切入)也画在这里,而不是单独一层 div —— 它是每帧的第一个
|
|
34
|
+
绘制操作,后面的粒子和艺术字自然落在暗幕之上,不用纠结 DOM 层序。
|
|
35
|
+
-->
|
|
36
|
+
<canvas id="fx" class="fx" aria-hidden="true"></canvas>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<script src="fx.js"></script>
|
|
40
|
+
<script src="awaken.js"></script>
|
|
41
|
+
<script src="app.js"></script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--pet-scale: 1;
|
|
3
|
+
--tilt: 0deg;
|
|
4
|
+
--lean: 0deg;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html,
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
height: 100%;
|
|
15
|
+
background: transparent;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
user-select: none;
|
|
18
|
+
cursor: default;
|
|
19
|
+
-webkit-user-drag: none;
|
|
20
|
+
font-family: "Microsoft YaHei", "PingFang SC", system-ui, sans-serif;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.stage {
|
|
24
|
+
position: relative;
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 100%;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ------------------------------------------------------------------ 角色 */
|
|
30
|
+
|
|
31
|
+
.anchor {
|
|
32
|
+
position: absolute;
|
|
33
|
+
left: 50%;
|
|
34
|
+
bottom: 6px;
|
|
35
|
+
z-index: 2;
|
|
36
|
+
/* --sink:游泳时整体沉入水线以下 */
|
|
37
|
+
transform: translateX(-50%) translateY(var(--sink, 0px));
|
|
38
|
+
/* bottom 也一起过渡:觉醒时窗口被放大,角色要「滑」到画面中间而不是瞬移 */
|
|
39
|
+
transition: transform 380ms cubic-bezier(0.34, 1.25, 0.5, 1),
|
|
40
|
+
bottom 460ms cubic-bezier(0.3, 1.1, 0.4, 1);
|
|
41
|
+
will-change: transform;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* 朝向翻转独立成一层:这样 keyframes 里的 transform 不会被 scaleX 顶掉 */
|
|
45
|
+
.facer {
|
|
46
|
+
transform: scaleX(var(--face, 1));
|
|
47
|
+
transition: transform 170ms ease;
|
|
48
|
+
will-change: transform;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.character {
|
|
52
|
+
position: relative;
|
|
53
|
+
transform-origin: 50% 100%;
|
|
54
|
+
transform: rotate(var(--tilt)) translateY(var(--bob, 0px));
|
|
55
|
+
will-change: transform;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* 角色是精灵图:宽/高/背景图/帧数都由渲染层按当前动作设置 */
|
|
59
|
+
.sprite {
|
|
60
|
+
display: block;
|
|
61
|
+
pointer-events: none;
|
|
62
|
+
background-repeat: no-repeat;
|
|
63
|
+
background-position: 0 0;
|
|
64
|
+
filter: drop-shadow(0 6px 10px rgba(90, 60, 90, 0.18));
|
|
65
|
+
transform: translate(var(--look-x, 0px), var(--look-y, 0px)) rotate(var(--lean));
|
|
66
|
+
transition: transform 90ms linear, filter 200ms ease;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* 逐帧播放:steps() 把 background-position-x 按帧数切成每一帧 */
|
|
70
|
+
.sprite.moving {
|
|
71
|
+
animation: sheet-cycle var(--gait, 560ms) steps(var(--nframes, 1)) infinite;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@keyframes sheet-cycle {
|
|
75
|
+
from {
|
|
76
|
+
background-position-x: 0px;
|
|
77
|
+
}
|
|
78
|
+
to {
|
|
79
|
+
background-position-x: calc(-1 * var(--strip, 0px));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.shadow {
|
|
84
|
+
position: absolute;
|
|
85
|
+
left: 50%;
|
|
86
|
+
bottom: -2px;
|
|
87
|
+
width: 62%;
|
|
88
|
+
height: 16px;
|
|
89
|
+
transform: translateX(-50%);
|
|
90
|
+
background: radial-gradient(ellipse at center, rgba(70, 40, 70, 0.32), rgba(70, 40, 70, 0) 70%);
|
|
91
|
+
filter: blur(2px);
|
|
92
|
+
z-index: -1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* 待机呼吸 / 上下浮动 */
|
|
96
|
+
@keyframes bob {
|
|
97
|
+
0%,
|
|
98
|
+
100% {
|
|
99
|
+
transform: rotate(var(--tilt)) translateY(0);
|
|
100
|
+
}
|
|
101
|
+
50% {
|
|
102
|
+
transform: rotate(var(--tilt)) translateY(-9px);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* 拖拽时随速度摆动 */
|
|
107
|
+
@keyframes dangle {
|
|
108
|
+
0%,
|
|
109
|
+
100% {
|
|
110
|
+
transform: rotate(var(--tilt)) translateY(0);
|
|
111
|
+
}
|
|
112
|
+
50% {
|
|
113
|
+
transform: rotate(var(--tilt)) translateY(-4px);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* 跳一下 */
|
|
118
|
+
@keyframes hop {
|
|
119
|
+
0% {
|
|
120
|
+
transform: rotate(0deg) translateY(0) scale(1, 1);
|
|
121
|
+
}
|
|
122
|
+
15% {
|
|
123
|
+
transform: rotate(0deg) translateY(0) scale(1.08, 0.9);
|
|
124
|
+
}
|
|
125
|
+
45% {
|
|
126
|
+
transform: rotate(4deg) translateY(-42px) scale(0.96, 1.06);
|
|
127
|
+
}
|
|
128
|
+
70% {
|
|
129
|
+
transform: rotate(-2deg) translateY(0) scale(1.1, 0.88);
|
|
130
|
+
}
|
|
131
|
+
85% {
|
|
132
|
+
transform: rotate(0deg) translateY(0) scale(0.98, 1.02);
|
|
133
|
+
}
|
|
134
|
+
100% {
|
|
135
|
+
transform: rotate(0deg) translateY(0) scale(1, 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* 落地弹一下 */
|
|
140
|
+
@keyframes land {
|
|
141
|
+
0% {
|
|
142
|
+
transform: rotate(0deg) translateY(0) scale(1.14, 0.86);
|
|
143
|
+
}
|
|
144
|
+
40% {
|
|
145
|
+
transform: rotate(0deg) translateY(-16px) scale(0.96, 1.05);
|
|
146
|
+
}
|
|
147
|
+
70% {
|
|
148
|
+
transform: rotate(0deg) translateY(0) scale(1.05, 0.95);
|
|
149
|
+
}
|
|
150
|
+
100% {
|
|
151
|
+
transform: rotate(0deg) translateY(0) scale(1, 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* 跳舞 */
|
|
156
|
+
@keyframes dance {
|
|
157
|
+
0% {
|
|
158
|
+
transform: rotate(0deg) translateY(0);
|
|
159
|
+
}
|
|
160
|
+
20% {
|
|
161
|
+
transform: rotate(-9deg) translateY(-10px) scale(1.02, 0.98);
|
|
162
|
+
}
|
|
163
|
+
45% {
|
|
164
|
+
transform: rotate(2deg) translateY(0) scale(0.99, 1.02);
|
|
165
|
+
}
|
|
166
|
+
70% {
|
|
167
|
+
transform: rotate(9deg) translateY(-10px) scale(1.02, 0.98);
|
|
168
|
+
}
|
|
169
|
+
100% {
|
|
170
|
+
transform: rotate(0deg) translateY(0);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* 说话时小幅点头 */
|
|
175
|
+
@keyframes talk {
|
|
176
|
+
0%,
|
|
177
|
+
100% {
|
|
178
|
+
transform: rotate(0deg) translateY(0);
|
|
179
|
+
}
|
|
180
|
+
25% {
|
|
181
|
+
transform: rotate(-1.5deg) translateY(-3px);
|
|
182
|
+
}
|
|
183
|
+
75% {
|
|
184
|
+
transform: rotate(1.5deg) translateY(-1px);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* 睡觉:慢呼吸 + 变暗 */
|
|
189
|
+
@keyframes sleep-breathe {
|
|
190
|
+
0%,
|
|
191
|
+
100% {
|
|
192
|
+
transform: rotate(-2deg) translateY(0) scale(1.03, 0.97);
|
|
193
|
+
}
|
|
194
|
+
50% {
|
|
195
|
+
transform: rotate(-2deg) translateY(-3px) scale(1.01, 0.99);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* 摇头 */
|
|
200
|
+
@keyframes shake {
|
|
201
|
+
0%,
|
|
202
|
+
100% {
|
|
203
|
+
transform: rotate(0);
|
|
204
|
+
}
|
|
205
|
+
25% {
|
|
206
|
+
transform: rotate(-6deg);
|
|
207
|
+
}
|
|
208
|
+
75% {
|
|
209
|
+
transform: rotate(6deg);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ----------------------------------------------------------- 状态机动画 */
|
|
214
|
+
|
|
215
|
+
.stage[data-anim="idle"] .character {
|
|
216
|
+
animation: bob 3.4s ease-in-out infinite;
|
|
217
|
+
}
|
|
218
|
+
.stage[data-anim="drag"] .character {
|
|
219
|
+
animation: dangle 1.1s ease-in-out infinite;
|
|
220
|
+
}
|
|
221
|
+
.stage[data-anim="hop"] .character {
|
|
222
|
+
animation: hop 0.62s cubic-bezier(0.28, 0.9, 0.3, 1) 1;
|
|
223
|
+
}
|
|
224
|
+
.stage[data-anim="land"] .character {
|
|
225
|
+
animation: land 0.5s ease-out 1;
|
|
226
|
+
}
|
|
227
|
+
.stage[data-anim="dance"] .character {
|
|
228
|
+
animation: dance 0.9s ease-in-out 3;
|
|
229
|
+
}
|
|
230
|
+
.stage[data-anim="talk"] .character {
|
|
231
|
+
animation: talk 0.4s ease-in-out infinite;
|
|
232
|
+
}
|
|
233
|
+
.stage[data-anim="shake"] .character {
|
|
234
|
+
animation: shake 0.45s ease-in-out 1;
|
|
235
|
+
}
|
|
236
|
+
.stage[data-anim="sleep"] .character {
|
|
237
|
+
animation: sleep-breathe 4.2s ease-in-out infinite;
|
|
238
|
+
filter: saturate(0.72) brightness(0.9);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* 自动活动:--gait 是每轮帧循环的时长,由渲染层按速度设 */
|
|
242
|
+
|
|
243
|
+
/* 游泳时整体压暗一点、偏冷,像泡在水里 */
|
|
244
|
+
.stage.swimming .sprite {
|
|
245
|
+
filter: drop-shadow(0 6px 10px rgba(20, 60, 100, 0.35)) saturate(0.88) brightness(0.94);
|
|
246
|
+
}
|
|
247
|
+
.stage.swimming .character {
|
|
248
|
+
filter: blur(0.3px);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* -------------------------------------------------------------- 水面 */
|
|
252
|
+
|
|
253
|
+
.water {
|
|
254
|
+
position: absolute;
|
|
255
|
+
left: -14%;
|
|
256
|
+
right: -14%;
|
|
257
|
+
bottom: 0;
|
|
258
|
+
/* 角色放大时水也加深,不然 1.5 倍大小下角色会整个浮在水面上 */
|
|
259
|
+
height: calc(152px * var(--pet-scale));
|
|
260
|
+
z-index: 3;
|
|
261
|
+
opacity: 0;
|
|
262
|
+
pointer-events: none;
|
|
263
|
+
transition: opacity 380ms ease;
|
|
264
|
+
background: linear-gradient(
|
|
265
|
+
180deg,
|
|
266
|
+
rgba(150, 216, 255, 0.5),
|
|
267
|
+
rgba(58, 140, 214, 0.72) 45%,
|
|
268
|
+
rgba(24, 86, 158, 0.86)
|
|
269
|
+
);
|
|
270
|
+
/* 两边渐隐,不然就是一条蓝色矩形在桌面上平移 */
|
|
271
|
+
-webkit-mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
|
|
272
|
+
mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* 波浪顶边:一排半圆沿 x 轴循环平移 */
|
|
276
|
+
.water::before {
|
|
277
|
+
content: "";
|
|
278
|
+
position: absolute;
|
|
279
|
+
top: -9px;
|
|
280
|
+
left: 0;
|
|
281
|
+
width: 200%;
|
|
282
|
+
height: 18px;
|
|
283
|
+
background-image: radial-gradient(
|
|
284
|
+
circle at 11px 16px,
|
|
285
|
+
rgba(206, 240, 255, 0.9) 0 10px,
|
|
286
|
+
rgba(206, 240, 255, 0) 11px
|
|
287
|
+
);
|
|
288
|
+
background-size: 22px 18px;
|
|
289
|
+
background-repeat: repeat-x;
|
|
290
|
+
animation: wave-slide 2.6s linear infinite;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.water::after {
|
|
294
|
+
content: "";
|
|
295
|
+
position: absolute;
|
|
296
|
+
inset: 0;
|
|
297
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.22), transparent 22%);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@keyframes wave-slide {
|
|
301
|
+
from {
|
|
302
|
+
transform: translateX(0);
|
|
303
|
+
}
|
|
304
|
+
to {
|
|
305
|
+
transform: translateX(-22px);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.stage.swimming .water {
|
|
310
|
+
opacity: 1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.stage.sleeping .sprite {
|
|
314
|
+
transform: translate(0, 2px) rotate(1deg);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* 悬停:轻微放大 + 高亮 */
|
|
318
|
+
.stage.hovering .sprite {
|
|
319
|
+
filter: drop-shadow(0 8px 14px rgba(255, 140, 190, 0.45)) brightness(1.04);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ------------------------------------------------------------ 对话气泡 */
|
|
323
|
+
|
|
324
|
+
.bubble {
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: 0;
|
|
327
|
+
left: 50%;
|
|
328
|
+
z-index: 6;
|
|
329
|
+
max-width: 94%;
|
|
330
|
+
padding: 8px 12px;
|
|
331
|
+
border-radius: 14px;
|
|
332
|
+
background: linear-gradient(160deg, #fff7fb, #ffe6f2);
|
|
333
|
+
border: 2px solid #ffc3de;
|
|
334
|
+
color: #6b3550;
|
|
335
|
+
font-size: calc(12.5px * var(--pet-scale));
|
|
336
|
+
line-height: 1.5;
|
|
337
|
+
text-align: center;
|
|
338
|
+
box-shadow: 0 6px 18px rgba(214, 106, 158, 0.28);
|
|
339
|
+
opacity: 0;
|
|
340
|
+
transform: translate(-50%, 6px) scale(0.9);
|
|
341
|
+
transform-origin: 50% 100%;
|
|
342
|
+
transition: opacity 160ms ease, transform 190ms cubic-bezier(0.2, 1.3, 0.4, 1);
|
|
343
|
+
pointer-events: none;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.bubble::after {
|
|
347
|
+
content: "";
|
|
348
|
+
position: absolute;
|
|
349
|
+
left: 50%;
|
|
350
|
+
bottom: -9px;
|
|
351
|
+
width: 12px;
|
|
352
|
+
height: 12px;
|
|
353
|
+
margin-left: -6px;
|
|
354
|
+
background: #ffe6f2;
|
|
355
|
+
border-right: 2px solid #ffc3de;
|
|
356
|
+
border-bottom: 2px solid #ffc3de;
|
|
357
|
+
transform: rotate(45deg);
|
|
358
|
+
border-bottom-right-radius: 3px;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.bubble.show {
|
|
362
|
+
opacity: 1;
|
|
363
|
+
transform: translate(-50%, 0) scale(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* -------------------------------------------------------------- 小爱心 */
|
|
367
|
+
|
|
368
|
+
.hearts {
|
|
369
|
+
position: absolute;
|
|
370
|
+
inset: 0;
|
|
371
|
+
z-index: 4;
|
|
372
|
+
overflow: hidden;
|
|
373
|
+
pointer-events: none;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* 所有粒子共用底座 */
|
|
377
|
+
.p {
|
|
378
|
+
position: absolute;
|
|
379
|
+
pointer-events: none;
|
|
380
|
+
will-change: transform, opacity;
|
|
381
|
+
user-select: none;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.heart {
|
|
385
|
+
font-size: calc(14px * var(--pet-scale));
|
|
386
|
+
animation: float-up 1.5s ease-out forwards;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* 跑步扬起的尘土(往身后飘) */
|
|
390
|
+
.dust {
|
|
391
|
+
font-size: calc(13px * var(--pet-scale));
|
|
392
|
+
color: rgba(168, 158, 178, 0.92);
|
|
393
|
+
animation: dust-puff 0.72s ease-out forwards;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@keyframes dust-puff {
|
|
397
|
+
0% {
|
|
398
|
+
opacity: 0;
|
|
399
|
+
transform: translate(-50%, -50%) scale(0.5);
|
|
400
|
+
}
|
|
401
|
+
25% {
|
|
402
|
+
opacity: 0.85;
|
|
403
|
+
transform: translate(-50%, -50%) scale(1.15);
|
|
404
|
+
}
|
|
405
|
+
100% {
|
|
406
|
+
opacity: 0;
|
|
407
|
+
transform: translate(calc(-50% + var(--drift, -30px)), calc(-50% - 14px)) scale(1.6);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* 游泳气泡:慢慢上浮并左右打个旋 */
|
|
412
|
+
.bubble-p {
|
|
413
|
+
font-size: calc(12px * var(--pet-scale));
|
|
414
|
+
color: rgba(224, 246, 255, 0.95);
|
|
415
|
+
text-shadow: 0 0 4px rgba(120, 200, 255, 0.8);
|
|
416
|
+
animation: bubble-rise 1.4s ease-out forwards;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@keyframes bubble-rise {
|
|
420
|
+
0% {
|
|
421
|
+
opacity: 0;
|
|
422
|
+
transform: translate(-50%, -50%) scale(0.4);
|
|
423
|
+
}
|
|
424
|
+
20% {
|
|
425
|
+
opacity: 0.95;
|
|
426
|
+
transform: translate(-50%, -50%) scale(1);
|
|
427
|
+
}
|
|
428
|
+
60% {
|
|
429
|
+
transform: translate(calc(-50% + var(--drift, 10px)), calc(-50% + var(--rise, -30px))) scale(1.05);
|
|
430
|
+
}
|
|
431
|
+
100% {
|
|
432
|
+
opacity: 0;
|
|
433
|
+
transform: translate(calc(-50% + var(--drift, 10px) * -0.6), calc(-50% + var(--rise, -30px) * 1.7))
|
|
434
|
+
scale(1.1);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* 入水 / 出水的水花 */
|
|
439
|
+
.splash {
|
|
440
|
+
font-size: calc(13px * var(--pet-scale));
|
|
441
|
+
color: #dff2ff;
|
|
442
|
+
animation: splash-pop 0.9s cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
@keyframes splash-pop {
|
|
446
|
+
0% {
|
|
447
|
+
opacity: 0;
|
|
448
|
+
transform: translate(-50%, -50%) scale(0.4);
|
|
449
|
+
}
|
|
450
|
+
22% {
|
|
451
|
+
opacity: 1;
|
|
452
|
+
transform: translate(-50%, -50%) scale(1.15);
|
|
453
|
+
}
|
|
454
|
+
100% {
|
|
455
|
+
opacity: 0;
|
|
456
|
+
transform: translate(calc(-50% + var(--drift, 20px)), calc(-50% + var(--rise, -34px) * 1.6)) scale(0.7);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* 水面涟漪:一圈不断扩散的椭圆 */
|
|
461
|
+
.ripple {
|
|
462
|
+
width: calc(34px * var(--pet-scale));
|
|
463
|
+
height: calc(12px * var(--pet-scale));
|
|
464
|
+
border: 2px solid rgba(214, 241, 255, 0.85);
|
|
465
|
+
border-radius: 50%;
|
|
466
|
+
animation: ripple-spread 1.2s ease-out forwards;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@keyframes ripple-spread {
|
|
470
|
+
0% {
|
|
471
|
+
opacity: 0.6;
|
|
472
|
+
transform: translate(-50%, -50%) scale(0.35, 0.35);
|
|
473
|
+
}
|
|
474
|
+
100% {
|
|
475
|
+
opacity: 0;
|
|
476
|
+
transform: translate(-50%, -50%) scale(2.1, 0.9);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
@keyframes float-up {
|
|
481
|
+
0% {
|
|
482
|
+
opacity: 0;
|
|
483
|
+
transform: translateY(0) scale(0.6);
|
|
484
|
+
}
|
|
485
|
+
20% {
|
|
486
|
+
opacity: 1;
|
|
487
|
+
transform: translateY(-6px) scale(1.1);
|
|
488
|
+
}
|
|
489
|
+
100% {
|
|
490
|
+
opacity: 0;
|
|
491
|
+
transform: translateY(-58px) scale(0.85) translateX(var(--drift, 8px));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* ------------------------------------------------------------ 首次提示 */
|
|
496
|
+
|
|
497
|
+
.hint {
|
|
498
|
+
position: absolute;
|
|
499
|
+
top: 86%;
|
|
500
|
+
left: 50%;
|
|
501
|
+
z-index: 6;
|
|
502
|
+
transform: translateX(-50%);
|
|
503
|
+
padding: 3px 9px;
|
|
504
|
+
border-radius: 10px;
|
|
505
|
+
font-size: calc(11px * var(--pet-scale));
|
|
506
|
+
color: #8a5a72;
|
|
507
|
+
background: rgba(255, 255, 255, 0.78);
|
|
508
|
+
border: 1px dashed #e5a9c6;
|
|
509
|
+
opacity: 0;
|
|
510
|
+
transition: opacity 300ms ease;
|
|
511
|
+
pointer-events: none;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.stage.hinting .hint {
|
|
515
|
+
opacity: 1;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/* -------------------------------------------------------------- 特效层 */
|
|
519
|
+
|
|
520
|
+
.fx {
|
|
521
|
+
position: absolute;
|
|
522
|
+
inset: 0;
|
|
523
|
+
width: 100%;
|
|
524
|
+
height: 100%;
|
|
525
|
+
z-index: 9;
|
|
526
|
+
pointer-events: none;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* ------------------------------------------------------------ 觉醒演出 */
|
|
530
|
+
|
|
531
|
+
/*
|
|
532
|
+
* 觉醒时窗口会被主进程放大到「屏幕的百分之几」,暗幕替我们把这一帧遮住,
|
|
533
|
+
* 所以这里只需要把角色往上抬到画面中部、并做一次「镜头推近」。
|
|
534
|
+
*/
|
|
535
|
+
.stage.awakening .anchor {
|
|
536
|
+
bottom: 11%;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.stage.awakening .sprite {
|
|
540
|
+
transform-origin: 50% 100%;
|
|
541
|
+
transform: scale(var(--awaken-zoom, 1.5));
|
|
542
|
+
transition: transform 460ms cubic-bezier(0.3, 1.2, 0.4, 1);
|
|
543
|
+
filter: drop-shadow(0 12px 34px rgba(255, 196, 120, 0.45)) brightness(1.07) saturate(1.12);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* 觉醒期间呼吸/眨眼之类的小动作全部停下,免得和演出抢镜头 */
|
|
547
|
+
.stage.awakening .character {
|
|
548
|
+
animation: none !important;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* 「拖我 ~」这种开机提示不要在演出的画面上穿帮 */
|
|
552
|
+
.stage.awakening .hint {
|
|
553
|
+
opacity: 0 !important;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* 爆发时的画面抖动。用 translate 而不是 transform,避免和 body 上的别的变换打架 */
|
|
557
|
+
@keyframes awaken-shake {
|
|
558
|
+
0%, 100% { translate: 0 0; }
|
|
559
|
+
15% { translate: -7px 4px; }
|
|
560
|
+
30% { translate: 6px -4px; }
|
|
561
|
+
45% { translate: -5px 2px; }
|
|
562
|
+
60% { translate: 4px -3px; }
|
|
563
|
+
80% { translate: -2px 1px; }
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.stage.shaking {
|
|
567
|
+
animation: awaken-shake 0.46s ease-out;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/*
|
|
571
|
+
* 全屏白闪(和 canvas 暗幕叠加用,比单靠 canvas 更「烫」)。
|
|
572
|
+
* 这个必须短、必须收:最开始写成了 0.42s + 峰值 0.76 的满屏径向白,
|
|
573
|
+
* 结果爆发那一帧整个窗口洗白,四套角色全糊成一片白雾。
|
|
574
|
+
* 现在收成 0.28s + 更紧的光心,读起来是「炸了一下」而不是「一直白着」。
|
|
575
|
+
*/
|
|
576
|
+
.stage.flashing::after {
|
|
577
|
+
content: "";
|
|
578
|
+
position: absolute;
|
|
579
|
+
inset: 0;
|
|
580
|
+
z-index: 10;
|
|
581
|
+
pointer-events: none;
|
|
582
|
+
background: radial-gradient(
|
|
583
|
+
circle at 50% 52%,
|
|
584
|
+
rgba(255, 255, 255, 0.92) 0%,
|
|
585
|
+
rgba(255, 240, 205, 0.42) 30%,
|
|
586
|
+
rgba(255, 255, 255, 0) 62%
|
|
587
|
+
);
|
|
588
|
+
animation: flash-out 0.28s ease-out forwards;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
@keyframes flash-out {
|
|
592
|
+
from { opacity: 0.9; }
|
|
593
|
+
to { opacity: 0; }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* 觉醒立绘:素材缺失时这块根本不会出现 */
|
|
597
|
+
.stage.awakening .sprite {
|
|
598
|
+
background-repeat: no-repeat;
|
|
599
|
+
}
|