feat(metronome): 节拍器下方加脚步行走动画
- 节拍器圆下方新增"脚印踏步"动画区域 · 纯 CSS+SVG-like(div 拼接),不依赖 canvas/Lottie/视频 · 左右脚交替踏步,起脚时落地阴影变小变淡,落地时变大变浓 · 1 拍 1 步,左右各占半周期(animation-delay 错相 0.5) - 跟节拍器同步:CSS 变量 --step-duration = 60000/bpm,BPM 改了 步频立刻跟着变;暂停时 animation-play-state 自动停 - 智能配速提示: · <70 BPM 慢走 · 适合热身 · 70-99 健走 · 日常配速 · 100-129 快走 · 心率提升 · 130-169 慢跑 · 进入有氧 · ≥170 快跑 · 高强度 - 重构 ringStyle/coreStyle 为统一 rootStyle,挂在根节点用 CSS 变量级联(--beat-duration / --step-duration) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,20 +1,11 @@
|
||||
<template>
|
||||
<view class="metronome-page">
|
||||
<view class="metronome-page" :style="rootStyle">
|
||||
<view class="pulse-stage" @click="togglePlay">
|
||||
<view
|
||||
class="pulse-ring"
|
||||
:class="{ playing: isPlaying }"
|
||||
:style="ringStyle"
|
||||
/>
|
||||
<view
|
||||
class="pulse-ring pulse-ring-2"
|
||||
:class="{ playing: isPlaying }"
|
||||
:style="ringStyle"
|
||||
/>
|
||||
<view class="pulse-ring" :class="{ playing: isPlaying }" />
|
||||
<view class="pulse-ring pulse-ring-2" :class="{ playing: isPlaying }" />
|
||||
<view
|
||||
class="pulse-core"
|
||||
:class="{ playing: isPlaying, accent: isAccent }"
|
||||
:style="coreStyle"
|
||||
>
|
||||
<text class="bpm-num">{{ bpm }}</text>
|
||||
<text class="bpm-label">速度</text>
|
||||
@@ -28,6 +19,34 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 脚步行走动画:跟随 BPM 同步,1 拍 1 步 -->
|
||||
<view class="walker" :class="{ playing: isPlaying }">
|
||||
<view class="foot foot-left">
|
||||
<view class="footprint">
|
||||
<view class="heel" />
|
||||
<view class="toe toe-1" />
|
||||
<view class="toe toe-2" />
|
||||
<view class="toe toe-3" />
|
||||
<view class="toe toe-4" />
|
||||
<view class="toe toe-5" />
|
||||
</view>
|
||||
<view class="shadow" />
|
||||
</view>
|
||||
<view class="foot foot-right">
|
||||
<view class="footprint">
|
||||
<view class="heel" />
|
||||
<view class="toe toe-1" />
|
||||
<view class="toe toe-2" />
|
||||
<view class="toe toe-3" />
|
||||
<view class="toe toe-4" />
|
||||
<view class="toe toe-5" />
|
||||
</view>
|
||||
<view class="shadow" />
|
||||
</view>
|
||||
<view class="ground" />
|
||||
<text class="walker-tip">{{ walkerTip }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bpm-control">
|
||||
<view class="step-btn big" @click="adjustBpm(-10)">
|
||||
<text class="step-num">-10</text>
|
||||
@@ -113,12 +132,9 @@ const togglePlay = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const ringStyle = computed(() => ({
|
||||
'--beat-duration': `${intervalMs.value}ms`,
|
||||
}))
|
||||
|
||||
const coreStyle = computed(() => ({
|
||||
const rootStyle = computed(() => ({
|
||||
'--beat-duration': `${intervalMs.value}ms`,
|
||||
'--step-duration': `${intervalMs.value}ms`,
|
||||
}))
|
||||
|
||||
const statusHint = computed(() => {
|
||||
@@ -129,6 +145,15 @@ const statusHint = computed(() => {
|
||||
return '轻点圆圈开始'
|
||||
})
|
||||
|
||||
const walkerTip = computed(() => {
|
||||
if (!isPlaying.value) return '开始后跟节奏踏步'
|
||||
if (bpm.value < 70) return '慢走 · 适合热身'
|
||||
if (bpm.value < 100) return '健走 · 日常配速'
|
||||
if (bpm.value < 130) return '快走 · 心率提升'
|
||||
if (bpm.value < 170) return '慢跑 · 进入有氧'
|
||||
return '快跑 · 高强度'
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
preload()
|
||||
})
|
||||
@@ -288,6 +313,208 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 脚步行走动画 ===== */
|
||||
.walker {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
margin-bottom: 50rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.walker .ground {
|
||||
position: absolute;
|
||||
bottom: 36rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 480rpx;
|
||||
height: 2rpx;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(34, 197, 94, 0.45) 30%,
|
||||
rgba(34, 197, 94, 0.45) 70%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.walker .walker-tip {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #cbd5e1;
|
||||
letter-spacing: 2rpx;
|
||||
padding: 6rpx 24rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.foot {
|
||||
position: absolute;
|
||||
bottom: 36rpx;
|
||||
width: 110rpx;
|
||||
height: 130rpx;
|
||||
|
||||
.footprint {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
/* 脚跟+脚掌:一颗椭圆形 */
|
||||
.heel {
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 78rpx;
|
||||
height: 90rpx;
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
border-radius: 50% 50% 46% 46% / 56% 56% 44% 44%;
|
||||
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 5 个脚趾(右脚视角:从左到右大→小) */
|
||||
.toe {
|
||||
position: absolute;
|
||||
background: #16a34a;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.toe-1 {
|
||||
width: 22rpx;
|
||||
height: 24rpx;
|
||||
bottom: 96rpx;
|
||||
left: 18rpx;
|
||||
}
|
||||
.toe-2 {
|
||||
width: 18rpx;
|
||||
height: 20rpx;
|
||||
bottom: 102rpx;
|
||||
left: 40rpx;
|
||||
}
|
||||
.toe-3 {
|
||||
width: 16rpx;
|
||||
height: 18rpx;
|
||||
bottom: 100rpx;
|
||||
left: 58rpx;
|
||||
}
|
||||
.toe-4 {
|
||||
width: 14rpx;
|
||||
height: 16rpx;
|
||||
bottom: 94rpx;
|
||||
left: 72rpx;
|
||||
}
|
||||
.toe-5 {
|
||||
width: 12rpx;
|
||||
height: 14rpx;
|
||||
bottom: 86rpx;
|
||||
left: 84rpx;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
position: absolute;
|
||||
bottom: -8rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scaleX(1);
|
||||
width: 90rpx;
|
||||
height: 12rpx;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(0, 0, 0, 0.4),
|
||||
transparent 70%
|
||||
);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左脚:置左,内层 footprint 镜像翻转(大脚趾朝外侧) */
|
||||
.foot-left {
|
||||
left: calc(50% - 130rpx);
|
||||
}
|
||||
.foot-left .footprint {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* 右脚:置右 */
|
||||
.foot-right {
|
||||
left: calc(50% + 20rpx);
|
||||
}
|
||||
|
||||
/* 跟节拍同步:1 拍 1 步,左右各占半周期(0.5 偏移) */
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes foot-step-right {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-44rpx) rotate(8deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-2rpx) rotate(0deg);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 左脚保留 scaleX(-1) 镜像 */
|
||||
@keyframes foot-step-left {
|
||||
0%,
|
||||
100% {
|
||||
transform: scaleX(-1) translateY(0) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: scaleX(-1) translateY(-44rpx) rotate(8deg);
|
||||
}
|
||||
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.15;
|
||||
transform: translateX(-50%) scaleX(0.55);
|
||||
}
|
||||
50% {
|
||||
/* 落地瞬间阴影最浓 */
|
||||
opacity: 0.7;
|
||||
transform: translateX(-50%) scaleX(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== BPM 步进按钮 ===== */
|
||||
.bpm-control {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user