mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
🌈 style: 优化播放样式
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -128,14 +128,8 @@
|
||||
},
|
||||
{
|
||||
label: '封面主色',
|
||||
disabled: true,
|
||||
value: 'color',
|
||||
},
|
||||
{
|
||||
label: '无背景',
|
||||
disabled: true,
|
||||
value: 'none',
|
||||
},
|
||||
]"
|
||||
class="set"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user