🌈 style: 优化播放样式

This commit is contained in:
imsyy
2025-10-28 00:33:40 +08:00
parent feb186db65
commit 9967bcb102
7 changed files with 89 additions and 332 deletions

View File

@@ -8,27 +8,8 @@
class="full-player"
@mouseleave="playerLeave"
>
<!-- 遮罩 -->
<Transition name="fade" mode="out-in">
<div
:key="musicStore.playSong?.id ?? 0"
:class="['overlay', settingStore.playerBackgroundType]"
>
<!-- 背景模糊 -->
<img
v-if="settingStore.playerBackgroundType === 'blur'"
:src="musicStore.songCover"
class="overlay-img"
alt="cover"
/>
<!-- 流体背景 -->
<PlayerBackground
v-else-if="settingStore.playerBackgroundType === 'animation'"
:album="musicStore.songCover"
:fps="60"
/>
</div>
</Transition>
<!-- 背景 -->
<PlayerBackground />
<!-- 独立歌词 -->
<Transition name="fade" mode="out-in">
<div
@@ -200,36 +181,6 @@ onBeforeUnmount(() => {
backdrop-filter: blur(80px);
overflow: hidden;
z-index: 1000;
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: -1;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(20px);
}
&.blur {
display: flex;
align-items: center;
justify-content: center;
.overlay-img {
width: 100%;
height: auto;
transform: scale(1.5);
filter: blur(80px) contrast(1.2);
}
}
}
.lrc-instant {
position: absolute;
top: 0;
@@ -269,8 +220,8 @@ onBeforeUnmount(() => {
justify-content: center;
transition:
width 0.3s,
opacity 0.3s,
transform 0.3s;
opacity 0.5s cubic-bezier(0.34, 1.56, 0.64, 1),
transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.content-right {
position: absolute;

View File

@@ -1,71 +1,71 @@
<!-- 播放器背景 -->
<template>
<div ref="wrapperRef" class="player-background" />
<div
:class="['background', settingStore.playerBackgroundType]"
:style="{ '--main-color': statusStore.mainColor }"
>
<Transition name="fade" mode="out-in">
<!-- 背景色 -->
<div
v-if="settingStore.playerBackgroundType === 'color'"
:key="statusStore.mainColor"
class="color"
/>
<!-- 背景模糊 -->
<s-image
v-else-if="settingStore.playerBackgroundType === 'blur'"
:src="musicStore.songCover"
class="bg-img"
alt="cover"
/>
</Transition>
</div>
</template>
<script setup lang="ts">
import type { BackgroundRenderProps, BackgroundRenderRef } from "@applemusic-like-lyrics/vue";
import {
AbstractBaseRenderer,
BackgroundRender,
EplorRenderer,
} from "@applemusic-like-lyrics/core";
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
const props = defineProps<BackgroundRenderProps>();
const wrapperRef = ref<HTMLDivElement>();
const bgRenderRef = ref<AbstractBaseRenderer>();
// 初始化组件
const initBackgroundRender = () => {
if (!wrapperRef.value) return;
bgRenderRef.value = BackgroundRender.new(props.renderer ?? EplorRenderer);
const canvasEl = bgRenderRef.value.getElement();
canvasEl.style.width = "100%";
canvasEl.style.height = "100%";
wrapperRef.value.appendChild(canvasEl);
};
// 配置更改
watchEffect(() => {
if (props.album) bgRenderRef.value?.setAlbum(props.album, props.albumIsVideo);
});
watchEffect(() => {
if (props.fps) bgRenderRef.value?.setFPS(props.fps);
});
watchEffect(() => {
if (props.playing) bgRenderRef.value?.pause();
else bgRenderRef.value?.resume();
});
watchEffect(() => {
if (props.flowSpeed) bgRenderRef.value?.setFlowSpeed(props.flowSpeed);
});
watchEffect(() => {
if (props.renderScale) bgRenderRef.value?.setRenderScale(props.renderScale);
});
watchEffect(() => {
if (props.lowFreqVolume) bgRenderRef.value?.setLowFreqVolume(props.lowFreqVolume);
});
watchEffect(() => {
if (props.hasLyric !== undefined) bgRenderRef.value?.setHasLyric(props.hasLyric ?? true);
});
// 导出渲染器
defineExpose<BackgroundRenderRef>({
bgRender: bgRenderRef,
wrapperEl: wrapperRef,
});
onMounted(() => {
initBackgroundRender();
});
onUnmounted(() => {
if (bgRenderRef.value) bgRenderRef.value.dispose();
});
const musicStore = useMusicStore();
const statusStore = useStatusStore();
const settingStore = useSettingStore();
</script>
<style lang="scss" scoped>
.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: -1;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(20px);
}
&.blur {
display: flex;
align-items: center;
justify-content: center;
.bg-img {
width: 100%;
height: auto;
transform: scale(1.5);
filter: blur(80px) contrast(1.2);
}
}
&.color {
background-color: rgb(var(--main-color));
.color {
width: 100%;
height: 100%;
background-color: rgb(var(--main-color));
}
}
}
</style>

View File

@@ -127,13 +127,6 @@ const jumpPage = debounce(
line-clamp: 2;
-webkit-line-clamp: 2;
}
.extra-info {
position: absolute;
right: -34px;
display: flex;
align-items: center;
justify-content: center;
}
.n-icon {
margin-left: 12px;
transform: translateY(1px);
@@ -210,6 +203,13 @@ const jumpPage = debounce(
.name-text {
font-size: 30px;
}
.extra-info {
position: absolute;
right: -34px;
display: flex;
align-items: center;
justify-content: center;
}
}
}
&.center {

View File

@@ -128,14 +128,8 @@
},
{
label: '封面主色',
disabled: true,
value: 'color',
},
{
label: '无背景',
disabled: true,
value: 'none',
},
]"
class="set"
/>

View File

@@ -1,195 +0,0 @@
import type { RGB } from "@/types/main";
// 配置选项
interface FluidBackgroundType {
canvas: HTMLCanvasElement;
colors?: RGB[];
totalParticles?: number;
maxRadius?: number;
minRadius?: number;
speed?: number;
}
// App 类,负责管理整个画布和动画的主逻辑
export class FluidBackground {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private pixelRatio: number;
private colors: RGB[];
private totalParticles: number;
private maxRadius: number;
private minRadius: number;
private speed: number;
private particles: GlowParticle[];
private stageWidth!: number;
private stageHeight!: number;
constructor({
canvas,
colors,
totalParticles,
maxRadius,
minRadius,
speed,
}: FluidBackgroundType) {
// 使用传入的 canvas 元素
this.canvas = canvas;
// 获取 2D 绘图上下文
this.ctx = this.canvas.getContext("2d")!;
// 根据设备像素比设置比例,处理高清屏幕的显示问题
this.pixelRatio = window.devicePixelRatio > 1 ? 2 : 1;
// 设置颜色数组,粒子的颜色将从这个数组中选择
this.colors = colors || [
{ r: 45, g: 74, b: 227 },
{ r: 250, g: 255, b: 89 },
{ r: 255, g: 104, b: 248 },
{ r: 44, g: 209, b: 252 },
{ r: 54, g: 233, b: 84 },
];
// 设置色块球的数量,默认值为 15
this.totalParticles = totalParticles || 15;
// 设置色块球的最大和最小半径,默认值分别为 200 和 150
this.maxRadius = maxRadius || 200;
this.minRadius = minRadius || 150;
// 设置色块球的运动速度,默认值为 2
this.speed = speed || 2;
// 保存色块球的数组
this.particles = [];
// 监听窗口大小变化,调整画布尺寸
window.addEventListener("resize", this.resize.bind(this), false);
// 初始化画布大小和色块球
this.resize();
// 启动动画
this.animate();
}
// 处理画布大小调整的方法
private resize() {
// 获取当前窗口的宽度和高度
this.stageWidth = window.innerWidth;
this.stageHeight = window.innerHeight;
// 设置画布的宽度和高度,并根据设备像素比进行缩放
this.canvas.width = this.stageWidth * this.pixelRatio;
this.canvas.height = this.stageHeight * this.pixelRatio;
this.ctx.scale(this.pixelRatio, this.pixelRatio);
// 设置混合模式,使色块球重叠时产生更丰富的颜色效果
this.ctx.globalCompositeOperation = "saturation";
// 创建色块球
this.createParticles();
}
// 创建色块球的逻辑
private createParticles() {
let curColor = 0;
this.particles = []; // 清空粒子数组
for (let i = 0; i < this.totalParticles; i++) {
// 随机生成色块球的初始位置和半径
this.particles.push(
new GlowParticle(
Math.random() * this.stageWidth, // 随机生成 X 位置
Math.random() * this.stageHeight, // 随机生成 Y 位置
Math.random() * (this.maxRadius - this.minRadius) + this.minRadius, // 随机生成半径
this.colors[curColor % this.colors.length], // 循环选择颜色
this.speed, // 传递速度
),
);
curColor++; // 颜色索引递增
}
}
// 动画帧渲染方法
private animate() {
// 使用 requestAnimationFrame 来创建动画循环
window.requestAnimationFrame(this.animate.bind(this));
// 清除画布上的内容
this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight);
// 遍历每个色块球并调用其 animate 方法进行绘制和更新位置
for (let i = 0; i < this.totalParticles; i++) {
const item = this.particles[i];
item.animate(this.ctx, this.stageWidth, this.stageHeight);
}
}
}
// GlowParticle 类,表示单个色块球
class GlowParticle {
private x: number;
private y: number;
private radius: number;
private rgb: RGB;
private vx: number;
private vy: number;
private sinValue: number;
constructor(x: number, y: number, radius: number, rgb: RGB, speed: number) {
this.x = x; // 初始 X 位置
this.y = y; // 初始 Y 位置
this.radius = radius; // 半径
this.rgb = rgb; // 颜色对象 {r, g, b}
this.vx = Math.random() * speed; // 随机生成 X 方向的速度
this.vy = Math.random() * speed; // 随机生成 Y 方向的速度
this.sinValue = Math.random(); // 用于控制半径变化的正弦值
}
// 动画方法,更新位置和绘制色块球
animate(ctx: CanvasRenderingContext2D, stageWidth: number, stageHeight: number) {
// 更新正弦值,使色块球的半径产生波动效果
this.sinValue += 0.01;
this.radius += Math.sin(this.sinValue); // 半径在正弦波动下变化
// 更新 X 和 Y 位置
this.x += this.vx;
this.y += this.vy;
// 边界检测,如果超出画布边界则反向运动
if (this.x < -10 || this.x > stageWidth + 10) {
this.vx *= -1;
}
if (this.y < -10 || this.y > stageHeight + 10) {
this.vy *= -1;
}
// 开始绘制色块球
ctx.beginPath();
// 创建径向渐变,用于色块球的颜色过渡效果
const g = ctx.createRadialGradient(
this.x,
this.y,
this.radius * 0.01, // 渐变起始半径
this.x,
this.y,
this.radius, // 渐变结束半径
);
// 定义渐变颜色的起始点和结束点
g.addColorStop(0, `rgba(${this.rgb.r},${this.rgb.g},${this.rgb.b},1)`);
g.addColorStop(1, `rgba(${this.rgb.r},${this.rgb.g},${this.rgb.b},0)`);
// 设置填充样式为渐变
ctx.fillStyle = g;
// 画一个圆,表示色块球
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
// 填充圆形
ctx.fill();
}
}
export default FluidBackground;

View File

@@ -48,6 +48,8 @@ export const resetSongLyric = () => {
yrcAMData: [],
};
statusStore.usingTTMLLyric = false;
// 重置歌词索引
statusStore.lyricIndex = -1;
};
/**
@@ -58,6 +60,7 @@ export const resetSongLyric = () => {
*/
export const parsedLyricsData = (lyricData: any, skipExclude: boolean = false): void => {
const musicStore = useMusicStore();
const statusStore = useStatusStore();
if (lyricData.code !== 200) {
resetSongLyric();
return;
@@ -105,6 +108,8 @@ export const parsedLyricsData = (lyricData: any, skipExclude: boolean = false):
lrcAMData: parseAMData(lrcParseData, tlyricParseData, romalrcParseData, skipExclude),
yrcAMData: parseAMData(yrcParseData, ytlrcParseData, yromalrcParseData, skipExclude),
};
// 重置歌词索引
statusStore.lyricIndex = -1;
};
/**
@@ -257,6 +262,7 @@ export const parseLocalLyric = (lyric: string, format: "lrc" | "ttml") => {
*/
const parseLocalLyricLrc = (lyric: string) => {
const musicStore = useMusicStore();
const statusStore = useStatusStore();
const settingStore = useSettingStore();
// 解析
const lrc: LyricLine[] = parseLrc(lyric);
@@ -290,6 +296,8 @@ const parseLocalLyricLrc = (lyric: string) => {
yrcData: [],
yrcAMData: [],
};
// 重置歌词索引
statusStore.lyricIndex = -1;
};
/**
@@ -298,6 +306,7 @@ const parseLocalLyricLrc = (lyric: string) => {
*/
const parseLocalLyricAM = (lyric: string) => {
const musicStore = useMusicStore();
const statusStore = useStatusStore();
const settingStore = useSettingStore();
const skipExcludeLocal = !settingStore.enableExcludeLocalLyrics;
@@ -313,6 +322,8 @@ const parseLocalLyricAM = (lyric: string) => {
yrcAMData,
yrcData,
};
// 重置歌词索引
statusStore.lyricIndex = -1;
};
/**

View File

@@ -693,14 +693,12 @@ class Player {
}
// 只有一首歌的特殊处理
if (playListLength === 1) {
statusStore.lyricIndex = -1;
this.setSeek(0);
await this.play();
return;
}
// 单曲循环
if (playSongMode === "repeat-once" && autoEnd && !playHeartbeatMode) {
statusStore.lyricIndex = -1;
this.setSeek(0);
await this.play();
return;
@@ -723,8 +721,7 @@ class Player {
} else if (statusStore.playIndex >= playListLength) {
statusStore.playIndex = 0;
}
// 重置播放进度和歌词索引(切换歌曲时必须重置)
statusStore.lyricIndex = -1;
// 重置播放进度(切换歌曲时必须重置)
statusStore.currentTime = 0;
statusStore.progress = 0;
// 暂停
@@ -995,8 +992,7 @@ class Player {
}
// 更改状态
statusStore.playIndex = index;
// 重置播放进度和歌词索引(切换歌曲时必须重置)
statusStore.lyricIndex = -1;
// 重置播放进度(切换歌曲时必须重置)
statusStore.currentTime = 0;
statusStore.progress = 0;