feat(metronome): walker 区接入卡通跑步小人,跟 BPM 同步律动

- 替换原来两只脚印为一张跑步小人 PNG(static/training/runner.png)
- 静态 PNG + CSS 律动:
  · runner-bob keyframes: 1 拍内做"下蹲→起跳→反向倾斜→落地"
    四个阶段,模拟跑步身体起伏
  · 上下抖动 ±14rpx, 左右轻摆 ±1.5°,scale Y 微妙压缩
  · drop-shadow 让人物有立体感
- 阴影同步:runner-shadow-pulse 跟律动反向变化
  (跳起时阴影变小变淡 0.2,落地时变大变浓 0.5)
- 动画 duration 绑定 --step-duration = 60000/bpm
  · BPM 60  → 1000ms/次,慢悠悠
  · BPM 120 → 500ms/次,正常跑
  · BPM 180 → 333ms/次,紧张快跑

注意: 用户提供的文件实际是静态 PNG 不是 SVG 动图,
所以是"画面整体律动"而非真正的 walk cycle 关节运动。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 15:45:43 +08:00
co-authored by Cursor
parent a08031b901
commit a3ee72b34d
2 changed files with 65 additions and 95 deletions
+65 -95
View File
@@ -28,21 +28,13 @@
</view>
<view class="walker" :class="{ playing: isPlaying }">
<view class="foot foot-left">
<view class="runner-wrap">
<image
class="footprint"
src="/static/training/footprint.svg"
class="runner"
src="/static/training/runner.png"
mode="aspectFit"
/>
<view class="shadow" />
</view>
<view class="foot foot-right">
<image
class="footprint"
src="/static/training/footprint.svg"
mode="aspectFit"
/>
<view class="shadow" />
<view class="runner-shadow" />
</view>
<view class="ground" />
</view>
@@ -414,7 +406,7 @@ $radius-btn: 20rpx;
.walker {
position: relative;
width: 100%;
height: 220rpx;
height: 240rpx;
display: flex;
justify-content: center;
align-items: flex-end;
@@ -436,108 +428,86 @@ $radius-btn: 20rpx;
}
/* ============================================================
* 拟真脚印(SVG 矢量,单笔成型)
* 跑步小人(静态 PNG + CSS 律动)
* ============================================================ */
.foot {
.runner-wrap {
position: absolute;
bottom: 22rpx;
width: 96rpx;
height: 144rpx;
.footprint {
display: block;
width: 100%;
height: 100%;
transform-origin: bottom center;
}
.shadow {
position: absolute;
bottom: -6rpx;
left: 50%;
transform: translateX(-50%);
width: 70rpx;
height: 8rpx;
background: radial-gradient(
ellipse at center,
rgba(0, 0, 0, 0.45),
transparent 70%
);
border-radius: 50%;
}
bottom: 18rpx;
left: 50%;
transform: translateX(-50%);
width: 200rpx;
height: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
}
.foot-left {
left: calc(50% - 124rpx);
}
.foot-left .footprint {
transform: scaleX(-1);
}
.foot-right {
left: calc(50% + 24rpx);
.runner {
width: 100%;
height: 100%;
transform-origin: bottom center;
/* 默认状态保持微妙的呼吸感,不至于完全静止 */
transition: filter 0.3s;
}
/* 跟节拍同步:1 拍 1 步,左右各占半周期 */
.walker.playing {
.foot-left .footprint {
animation: foot-step-left var(--step-duration, 750ms) ease-in-out infinite;
}
.foot-right .footprint {
animation: foot-step-right var(--step-duration, 750ms) ease-in-out infinite;
animation-delay: calc(var(--step-duration, 750ms) * -0.5);
}
.foot-left .shadow {
animation: shadow-pulse var(--step-duration, 750ms) ease-in-out infinite;
}
.foot-right .shadow {
animation: shadow-pulse var(--step-duration, 750ms) ease-in-out infinite;
animation-delay: calc(var(--step-duration, 750ms) * -0.5);
}
.runner-shadow {
position: absolute;
bottom: -8rpx;
width: 120rpx;
height: 12rpx;
background: radial-gradient(
ellipse at center,
rgba(0, 0, 0, 0.45),
transparent 70%
);
border-radius: 50%;
transform-origin: center;
}
@keyframes foot-step-right {
0%,
100% {
transform: translateY(0) rotate(0deg);
/* 跟节拍同步:1 拍 1 次律动 */
.walker.playing .runner {
animation: runner-bob var(--step-duration, 750ms) ease-in-out infinite;
/* 高 BPM 时加速度模糊,让"跑得很快"有视觉表达 */
filter: drop-shadow(0 4rpx 12rpx rgba(0, 0, 0, 0.25));
}
.walker.playing .runner-shadow {
animation: runner-shadow-pulse var(--step-duration, 750ms) ease-in-out infinite;
}
/* 跑步律动:身体上下起伏 + 微妙左右摆动 */
@keyframes runner-bob {
0% {
transform: translateY(0) rotate(0deg) scale(1);
}
25% {
transform: translateY(-40rpx) rotate(8deg);
/* 第一拍着地半程,身体最低 */
transform: translateY(2rpx) rotate(-1.5deg) scale(1, 0.99);
}
50% {
transform: translateY(-2rpx) rotate(0deg);
/* 离地最高点,身体抬起 */
transform: translateY(-14rpx) rotate(0deg) scale(1, 1.01);
}
75% {
transform: translateY(0) rotate(0deg);
/* 另一脚着地半程,反向倾斜 */
transform: translateY(2rpx) rotate(1.5deg) scale(1, 0.99);
}
100% {
transform: translateY(0) rotate(0deg) scale(1);
}
}
@keyframes foot-step-left {
@keyframes runner-shadow-pulse {
0%,
100% {
transform: scaleX(-1) translateY(0) rotate(0deg);
}
25% {
transform: scaleX(-1) translateY(-40rpx) rotate(8deg);
opacity: 0.5;
transform: scaleX(1);
}
50% {
transform: scaleX(-1) translateY(-2rpx) rotate(0deg);
}
75% {
transform: scaleX(-1) translateY(0) rotate(0deg);
}
}
@keyframes shadow-pulse {
0%,
100% {
opacity: 0.55;
transform: translateX(-50%) scaleX(1);
}
25% {
opacity: 0.18;
transform: translateX(-50%) scaleX(0.5);
}
50% {
opacity: 0.7;
transform: translateX(-50%) scaleX(1.08);
/* 跳起时阴影变小变淡 */
opacity: 0.2;
transform: scaleX(0.7);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB