feat(metronome): walker 区改用 MP4 视频,playbackRate 跟 BPM 同步
将之前的 PNG runner + CSS 律动方案,升级为真正的卡通 walk cycle 视频: - 删除 runner.png(静态图无法表现关节运动) - 新增 runner.mp4(LottieFiles 卡通跑步动画,150x150, 60fps, 27KB) - walker 区从 image + CSS 抖动 改为 video + playback-rate 调速 - 视频原速基准 124 BPM(0.97s 一个 walk cycle = 2 步) - playbackRate = clamp(bpm/124, 0.5, 2.0),贴合微信小程序限制 - 视频与节拍器同步策略: · watch isPlaying → 自动 play/pause · watch playbackRate → 调用 videoCtx.playbackRate API 兜底 · onMounted 默认暂停,等用户点开始才播 - 视频白底跟暗色卡片冲突,改用"嵌入式高亮卡片"设计: · runner-card 白底 + 32rpx 圆角 + 阴影,像专业健康 APP · video object-fit contain 保证不被裁切 - 移除冗余的 .ground 地面线样式 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -28,15 +28,24 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="walker" :class="{ playing: isPlaying }">
|
<view class="walker" :class="{ playing: isPlaying }">
|
||||||
<view class="runner-wrap">
|
<view class="runner-card">
|
||||||
<image
|
<video
|
||||||
class="runner"
|
id="runner-video"
|
||||||
src="/static/training/runner.png"
|
class="runner-video"
|
||||||
mode="aspectFit"
|
src="/static/training/runner.mp4"
|
||||||
|
:muted="true"
|
||||||
|
:loop="true"
|
||||||
|
:autoplay="true"
|
||||||
|
:playback-rate="playbackRate"
|
||||||
|
:show-controls="false"
|
||||||
|
:show-fullscreen-btn="false"
|
||||||
|
:show-play-btn="false"
|
||||||
|
:show-center-play-btn="false"
|
||||||
|
:enable-progress-gesture="false"
|
||||||
|
:show-mute-btn="false"
|
||||||
|
object-fit="contain"
|
||||||
/>
|
/>
|
||||||
<view class="runner-shadow" />
|
|
||||||
</view>
|
</view>
|
||||||
<view class="ground" />
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -78,10 +87,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onUnmounted } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { onShow, onHide } from '@dcloudio/uni-app'
|
import { onShow, onHide } from '@dcloudio/uni-app'
|
||||||
import { useMetronome } from '@/hooks/useMetronome'
|
import { useMetronome } from '@/hooks/useMetronome'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频原速对应的 BPM 基准
|
||||||
|
* runner.mp4 时长 0.97s, 一个 walk cycle (左右脚各一次) ≈ 124 BPM 步频
|
||||||
|
* 用户调速时 playbackRate = bpm / VIDEO_BASE_BPM
|
||||||
|
* 微信小程序限制 playback-rate 范围 [0.5, 2.0]
|
||||||
|
*/
|
||||||
|
const VIDEO_BASE_BPM = 124
|
||||||
|
|
||||||
const METER_OPTIONS = [
|
const METER_OPTIONS = [
|
||||||
{ label: '2', value: 2 },
|
{ label: '2', value: 2 },
|
||||||
{ label: '3', value: 3 },
|
{ label: '3', value: 3 },
|
||||||
@@ -126,6 +143,36 @@ const togglePlay = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 视频实例与 BPM 同步 */
|
||||||
|
let videoCtx: UniApp.VideoContext | null = null
|
||||||
|
|
||||||
|
const playbackRate = computed(() => {
|
||||||
|
const rate = bpm.value / VIDEO_BASE_BPM
|
||||||
|
return Math.max(0.5, Math.min(2.0, +rate.toFixed(2)))
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
videoCtx = uni.createVideoContext('runner-video')
|
||||||
|
/* 进页面默认让视频保持暂停,跟节拍器 isPlaying 状态同步 */
|
||||||
|
setTimeout(() => videoCtx?.pause(), 50)
|
||||||
|
})
|
||||||
|
|
||||||
|
/* 节拍器状态变化 → 同步视频播放/暂停 */
|
||||||
|
watch(isPlaying, (playing) => {
|
||||||
|
if (playing) {
|
||||||
|
videoCtx?.play()
|
||||||
|
} else {
|
||||||
|
videoCtx?.pause()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/* BPM 变化 → 调整视频播放速度(部分基础库需要手动 API 调用) */
|
||||||
|
watch(playbackRate, (rate) => {
|
||||||
|
try {
|
||||||
|
videoCtx?.playbackRate?.(rate)
|
||||||
|
} catch (_) {}
|
||||||
|
})
|
||||||
|
|
||||||
const rootStyle = computed(() => ({
|
const rootStyle = computed(() => ({
|
||||||
'--beat-duration': `${intervalMs.value}ms`,
|
'--beat-duration': `${intervalMs.value}ms`,
|
||||||
'--step-duration': `${intervalMs.value}ms`,
|
'--step-duration': `${intervalMs.value}ms`,
|
||||||
@@ -412,103 +459,28 @@ $radius-btn: 20rpx;
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.walker .ground {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 22rpx;
|
|
||||||
left: 10%;
|
|
||||||
right: 10%;
|
|
||||||
height: 1rpx;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
transparent 0%,
|
|
||||||
rgba(255, 255, 255, 0.18) 30%,
|
|
||||||
rgba(255, 255, 255, 0.18) 70%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* 跑步小人(静态 PNG + CSS 律动)
|
* 跑步小人视频(白卡嵌入)
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
.runner-wrap {
|
.runner-card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 18rpx;
|
bottom: 18rpx;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 200rpx;
|
width: 220rpx;
|
||||||
height: 200rpx;
|
height: 220rpx;
|
||||||
display: flex;
|
background: #fff;
|
||||||
flex-direction: column;
|
border-radius: 32rpx;
|
||||||
align-items: center;
|
overflow: hidden;
|
||||||
justify-content: flex-end;
|
box-shadow:
|
||||||
|
0 12rpx 32rpx rgba(0, 0, 0, 0.25),
|
||||||
|
inset 0 0 0 1rpx rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.runner {
|
.runner-video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transform-origin: bottom center;
|
background: #fff;
|
||||||
/* 默认状态保持微妙的呼吸感,不至于完全静止 */
|
|
||||||
transition: filter 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 跟节拍同步: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(2rpx) rotate(-1.5deg) scale(1, 0.99);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
/* 离地最高点,身体抬起 */
|
|
||||||
transform: translateY(-14rpx) rotate(0deg) scale(1, 1.01);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
/* 另一脚着地半程,反向倾斜 */
|
|
||||||
transform: translateY(2rpx) rotate(1.5deg) scale(1, 0.99);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(0) rotate(0deg) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes runner-shadow-pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 0.5;
|
|
||||||
transform: scaleX(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
/* 跳起时阴影变小变淡 */
|
|
||||||
opacity: 0.2;
|
|
||||||
transform: scaleX(0.7);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Reference in New Issue
Block a user