fork(fix): Clone AMLL 并修复 BUG

- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork)
- 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
lqtmcstudio
2026-06-07 00:02:14 +08:00
parent 783d2c3dee
commit 72f4510dc8
458 changed files with 86075 additions and 1665 deletions

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<title>AMLL Core Test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="src/test.ts" type="module" defer></script>
<style>
:root {
font-family:
-apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
max-width: 100vw;
width: 100vw;
max-height: 100vh;
height: 100vh;
overflow: hidden;
background: #222;
margin: 0;
}
#player {
max-height: 100vh;
height: 100vh;
max-width: 100vw;
width: 100vw;
overflow: hidden;
}
</style>
</head>
<body>
<div id="player"></div>
</body>
</html>

View File

@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html>
<head>
<title>AMLL Core Test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="src/mg-test.ts" type="module" defer></script>
<style>
:root {
font-family:
-apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
max-width: 100vw;
width: 100vw;
max-height: 100vh;
height: 100vh;
overflow: hidden;
background: #222;
margin: 0;
}
#bg {
max-height: 100vh;
height: 100vh;
max-width: 100vw;
width: 100vw;
overflow: hidden;
}
#result {
position: absolute;
right: 20px;
bottom: 20px;
width: 50em;
height: 20em;
border: solid 1px #3337;
background-color: #1117;
resize: both;
color: white;
font-family:
"Fira Code", "SF Pro Display", "PingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
#result::placeholder {
color: #888;
}
.dragger {
position: absolute;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border-radius: 50%;
border: solid 3px #8888;
cursor: move;
user-select: none;
}
.dragger-handle {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #4af;
border: solid 2px #fff;
cursor: pointer;
transform: translate(-50%, -50%);
z-index: 10;
}
.dragger-line {
position: absolute;
height: 2px;
background-color: #4af8;
transform-origin: left center;
pointer-events: none;
}
.active {
border-color: #f44;
}
</style>
</head>
<body>
<canvas id="bg"></canvas>
<textarea id="result" placeholder="控制点代码将会在这里显示"></textarea>
</body>
</html>

View File

@@ -0,0 +1,32 @@
{
"name": "@applemusic-like-lyrics/playground-core-legacy",
"private": true,
"version": "0.0.0",
"type": "module",
"nx": {
"tags": [
"playground"
]
},
"scripts": {
"dev": "vite dev"
},
"devDependencies": {
"@applemusic-like-lyrics/core": "workspace:^",
"@applemusic-like-lyrics/lyric": "workspace:^",
"@applemusic-like-lyrics/ttml": "workspace:^",
"@pixi/app": "^7.4.3",
"@pixi/core": "^7.4.3",
"@pixi/display": "^7.4.3",
"@pixi/filter-blur": "^7.4.3",
"@pixi/filter-bulge-pinch": "^5.1.1",
"@pixi/filter-color-matrix": "^7.4.3",
"@pixi/sprite": "^7.4.3",
"lil-gui": "^0.21.0",
"stats.js": "^0.17.0",
"vite": "catalog:"
},
"dependencies": {
"@types/stats.js": "^0.17.4"
}
}

View File

@@ -0,0 +1,504 @@
/**
* @fileoverview
* 调试 MG 渲染器的测试脚本
*
* @author SteveXMH
*/
import { MeshGradientRenderer } from "@applemusic-like-lyrics/core";
import GUI from "lil-gui";
import Stats from "stats.js";
const debugValues = {
image: new URL(location.href).searchParams.get("image") || "",
controlPointSize: 4,
subdivideDepth: 15,
wireFrame: false,
};
const canvas = document.getElementById("bg") as HTMLCanvasElement;
const mgRenderer = new MeshGradientRenderer(canvas);
mgRenderer.setManualControl(true);
mgRenderer.setRenderScale(1);
mgRenderer.setFPS(Number.POSITIVE_INFINITY);
function updateControlPointDraggers() {
for (const el of document.querySelectorAll(".dragger")) {
const x = Number.parseInt(el.getAttribute("x") ?? "", 10);
const y = Number.parseInt(el.getAttribute("y") ?? "", 10);
const point = mgRenderer.getControlPoint(x, y);
if (point === undefined) return;
const px = (point.location.x + 1) * 50;
const py = (1 - point.location.y) * 50;
(el as HTMLElement).style.left = `${px}%`;
(el as HTMLElement).style.top = `${py}%`;
// Update handles
const uHandle = el.querySelector(".u-handle") as HTMLElement;
const uLine = el.querySelector(".u-line") as HTMLElement;
if (uHandle && uLine) {
const uLen = point.uScale * 50;
const uAngle = -point.uRot;
uHandle.style.left = `${10 + Math.cos(uAngle) * uLen}px`;
uHandle.style.top = `${10 + Math.sin(uAngle) * uLen}px`;
uLine.style.width = `${uLen}px`;
uLine.style.transform = `rotate(${uAngle}rad)`;
uLine.style.left = "10px";
uLine.style.top = "10px";
}
const vHandle = el.querySelector(".v-handle") as HTMLElement;
const vLine = el.querySelector(".v-line") as HTMLElement;
if (vHandle && vLine) {
const vLen = point.vScale * 50;
// vRot is relative to vertical axis (PI/2) in WebGL
const vAngle = -(point.vRot + Math.PI / 2);
vHandle.style.left = `${10 + Math.cos(vAngle) * vLen}px`;
vHandle.style.top = `${10 + Math.sin(vAngle) * vLen}px`;
vLine.style.width = `${vLen}px`;
vLine.style.transform = `rotate(${vAngle}rad)`;
vLine.style.left = "10px";
vLine.style.top = "10px";
}
}
}
let draggerGui: GUI | undefined;
function setActiveDragger(x: number, y: number) {
if (draggerGui) {
draggerGui.destroy();
draggerGui = undefined;
}
const point = mgRenderer.getControlPoint(x, y);
if (point) {
draggerGui = gui.addFolder(`控制点 (${x}, ${y})`);
const obj = {
uAngle: (point.uRot * 180) / Math.PI,
vAngle: (point.vRot * 180) / Math.PI,
uScale: point.uScale,
vScale: point.vScale,
};
draggerGui
.add(obj, "uAngle", -180, 180)
.name("横向扭曲角度")
.onChange((v: number) => {
point.uRot = (v * Math.PI) / 180;
updateControlPointDraggers();
updateResult();
});
draggerGui
.add(obj, "vAngle", -180, 180)
.name("纵向扭曲角度")
.onChange((v: number) => {
point.vRot = (v * Math.PI) / 180;
updateControlPointDraggers();
updateResult();
});
draggerGui
.add(obj, "uScale", 0.1, 10)
.name("横向缩放")
.onChange((v: number) => {
point.uScale = v;
updateControlPointDraggers();
updateResult();
});
draggerGui
.add(obj, "vScale", 0.1, 10)
.name("纵向缩放")
.onChange((v: number) => {
point.vScale = v;
updateControlPointDraggers();
updateResult();
});
}
}
window.addEventListener("resize", updateControlPointDraggers);
const resultTextArea = document.getElementById("result") as HTMLTextAreaElement;
resultTextArea.value = "// 控制点的设置代码将会在这里显示";
function updateResult() {
const result = [
`preset(${debugValues.controlPointSize}, ${debugValues.controlPointSize}, [`,
];
for (let y = 0; y < debugValues.controlPointSize; y++) {
for (let x = 0; x < debugValues.controlPointSize; x++) {
const point = mgRenderer.getControlPoint(x, y);
if (point === undefined) continue;
const px = Number(point.location.x.toFixed(4));
const py = Number(point.location.y.toFixed(4));
const ur = Number(point.uRot.toFixed(4));
const vr = Number(point.vRot.toFixed(4));
const up = Number(point.uScale.toFixed(4));
const vp = Number(point.vScale.toFixed(4));
let pStr = ` p(${x}, ${y}, ${px}, ${py}`;
if (ur !== 0 || vr !== 0 || up !== 1 || vp !== 1) {
pStr += `, ${ur}, ${vr}`;
if (up !== 1 || vp !== 1) {
pStr += `, ${up}, ${vp}`;
}
}
pStr += `),`;
result.push(pStr);
}
}
result.push("]),");
resultTextArea.value = result.join("\n");
}
function resizeControlPoint() {
document.querySelectorAll(".dragger").forEach((el) => {
el.parentElement?.removeChild(el);
});
mgRenderer.resizeControlPoints(
debugValues.controlPointSize,
debugValues.controlPointSize,
);
mgRenderer.resetSubdivition(debugValues.subdivideDepth);
for (let y = 0; y < debugValues.controlPointSize; y++) {
for (let x = 0; x < debugValues.controlPointSize; x++) {
const point = mgRenderer.getControlPoint(x, y);
if (point === undefined) continue;
const dragger = document.createElement("div");
const draggerInput = document.createElement("input");
draggerInput.type = "color";
draggerInput.style.position = "absolute";
draggerInput.style.visibility = "hidden";
dragger.appendChild(draggerInput);
dragger.setAttribute("x", `${x}`);
dragger.setAttribute("y", `${y}`);
dragger.className = "dragger";
dragger.style.left = `${(x * 100) / (debugValues.controlPointSize - 1)}%`;
dragger.style.top = `${
((debugValues.controlPointSize - y - 1) * 100) /
(debugValues.controlPointSize - 1)
}%`;
// Add U/V handles
const uLine = document.createElement("div");
uLine.className = "dragger-line u-line";
const uHandle = document.createElement("div");
uHandle.className = "dragger-handle u-handle";
uHandle.style.backgroundColor = "#f44";
const vLine = document.createElement("div");
vLine.className = "dragger-line v-line";
const vHandle = document.createElement("div");
vHandle.className = "dragger-handle v-handle";
vHandle.style.backgroundColor = "#4f4";
dragger.appendChild(uLine);
dragger.appendChild(uHandle);
dragger.appendChild(vLine);
dragger.appendChild(vHandle);
// Handle U dragging
uHandle.addEventListener("mousedown", (evt) => {
if (point === undefined) return;
evt.stopPropagation();
const rect = dragger.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
function onMouseMove(e: MouseEvent) {
if (point === undefined) return;
const dx = e.clientX - centerX;
const dy = e.clientY - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
const isCorner =
(x === 0 || x === debugValues.controlPointSize - 1) &&
(y === 0 || y === debugValues.controlPointSize - 1);
const isEdge =
x === 0 ||
x === debugValues.controlPointSize - 1 ||
y === 0 ||
y === debugValues.controlPointSize - 1;
if (!isCorner) {
if (isEdge) {
// For edge points, only allow scaling, keep rotation at 0
point.uRot = 0;
} else {
point.uRot = Math.atan2(-dy, dx);
}
}
point.uScale = Math.max(0.1, dist / 50);
updateControlPointDraggers();
updateResult();
if (draggerGui) {
draggerGui.controllersRecursive().forEach((c) => {
c.updateDisplay();
});
}
}
function onMouseUp() {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
}
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
});
// Handle V dragging
vHandle.addEventListener("mousedown", (evt) => {
if (point === undefined) return;
evt.stopPropagation();
const rect = dragger.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
function onMouseMove(e: MouseEvent) {
if (point === undefined) return;
const dx = e.clientX - centerX;
const dy = e.clientY - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
const isCorner =
(x === 0 || x === debugValues.controlPointSize - 1) &&
(y === 0 || y === debugValues.controlPointSize - 1);
const isEdge =
x === 0 ||
x === debugValues.controlPointSize - 1 ||
y === 0 ||
y === debugValues.controlPointSize - 1;
if (!isCorner) {
if (isEdge) {
// For edge points, only allow scaling, keep rotation at 0
point.vRot = 0;
} else {
// vRot is relative to vertical axis
point.vRot = Math.atan2(-dy, dx) - Math.PI / 2;
}
}
point.vScale = Math.max(0.1, dist / 50);
updateControlPointDraggers();
updateResult();
if (draggerGui) {
draggerGui.controllersRecursive().forEach((c) => {
c.updateDisplay();
});
}
}
function onMouseUp() {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
}
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
});
draggerInput.addEventListener("input", () => {
// mgRenderer.getControlPoint(x, y).color = dragger.value;
const c = draggerInput.value;
console.log(c);
dragger.style.backgroundColor = c;
const color = c.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
if (color) {
point.color.r = Number.parseInt(color[1], 16) / 255;
point.color.g = Number.parseInt(color[2], 16) / 255;
point.color.b = Number.parseInt(color[3], 16) / 255;
dragger.setAttribute("r", `${point.color.r}`);
dragger.setAttribute("g", `${point.color.g}`);
dragger.setAttribute("b", `${point.color.b}`);
updateResult();
}
});
let dragging = false;
dragger.addEventListener("mousedown", (evt) => {
evt.stopPropagation();
const isCorner =
(x === 0 || x === debugValues.controlPointSize - 1) &&
(y === 0 || y === debugValues.controlPointSize - 1);
function onMouseMove(evt: MouseEvent) {
if (!isCorner) {
dragger.style.left = `${Math.min(
window.innerWidth,
Math.max(0, evt.clientX),
)}px`;
dragger.style.top = `${Math.min(
window.innerHeight,
Math.max(0, evt.clientY),
)}px`;
if (point) {
point.location.x = Math.max(
-1,
Math.min(1, (evt.clientX / window.innerWidth) * 2 - 1),
);
point.location.y = Math.max(
-1,
Math.min(1, -((evt.clientY / window.innerHeight) * 2 - 1)),
);
}
}
dragging = true;
updateControlPointDraggers();
updateResult();
evt.stopPropagation();
}
function onMouseUp(evt: MouseEvent) {
if (dragging) {
dragging = false;
} else if (dragger.classList.contains("active")) {
draggerInput.click();
} else {
for (const el of document.querySelectorAll(".dragger.active")) {
el.classList.remove("active");
}
dragger.classList.add("active");
setActiveDragger(x, y);
}
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
evt.stopPropagation();
}
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
});
document.body.appendChild(dragger);
}
}
updateResult();
}
function subdivide() {
mgRenderer.resetSubdivition(debugValues.subdivideDepth);
}
function reloadImage() {
mgRenderer
.setAlbum(debugValues.image)
.catch(() =>
mgRenderer.setAlbum(
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAADUExURf///6fEG8gAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAKSURBVBjTY2AAAAACAAGYY2zXAAAAAElFTkSuQmCC",
),
)
.finally(() => {
resizeControlPoint();
updateControlPointDraggers();
subdivide();
});
}
reloadImage();
const gui = new GUI();
gui.close();
gui.title("MG Renderer 调试页面");
gui.add(debugValues, "image").name("图片 URL").onFinishChange(reloadImage);
gui
.add(debugValues, "controlPointSize", 3, 10, 1)
.name("控制点矩阵大小")
.onFinishChange(resizeControlPoint);
gui
.add(debugValues, "subdivideDepth", 2, 50, 1)
.name("细分深度")
.onChange(subdivide);
gui
.add(debugValues, "wireFrame")
.name("线框模式")
.onChange((v: boolean) => mgRenderer.setWireFrame(v));
/** @internal */
export interface ControlPointConf {
cx: number;
cy: number;
x: number;
y: number;
ur: number;
vr: number;
up: number;
vp: number;
}
/** @internal */
export interface ControlPointPreset {
width: number;
height: number;
conf: ControlPointConf[];
}
const actions = {
copyCode: () => {
navigator.clipboard.writeText(resultTextArea.value).then(() => {
alert("代码已复制到剪贴板");
});
},
loadCode: () => {
try {
const code = resultTextArea.value;
const preset = (
width: number,
height: number,
conf: ControlPointConf[],
): ControlPointPreset => {
return { width, height, conf };
};
const p = (
cx: number,
cy: number,
x: number,
y: number,
ur = 0,
vr = 0,
up = 1,
vp = 1,
): ControlPointConf => ({ cx, cy, x, y, ur, vr, up, vp });
// Remove trailing comma if exists
const cleanCode = code.trim().replace(/,$/, "");
const fn = new Function("preset", "p", `return ${cleanCode}`);
const loadedPreset = fn(preset, p) as ControlPointPreset | undefined;
if (loadedPreset) {
debugValues.controlPointSize = loadedPreset.width;
gui.controllersRecursive().forEach((c) => {
c.updateDisplay();
});
resizeControlPoint();
for (const conf of loadedPreset.conf) {
const point = mgRenderer.getControlPoint(conf.cx, conf.cy);
if (point) {
point.location.x = conf.x;
point.location.y = conf.y;
point.uRot = conf.ur;
point.vRot = conf.vr;
point.uScale = conf.up;
point.vScale = conf.vp;
}
}
updateControlPointDraggers();
updateResult();
alert("预设加载成功");
}
} catch (e) {
alert(`加载失败,请检查代码格式是否正确\n${e}`);
}
},
};
gui.add(actions, "copyCode").name("复制预设代码");
gui.add(actions, "loadCode").name("从文本框加载预设");
const stats = new Stats();
stats.showPanel(0);
stats.dom.style.left = "50px";
document.body.appendChild(stats.dom);
const frame = () => {
stats.end();
stats.begin();
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);

View File

@@ -0,0 +1,449 @@
/**
* @fileoverview
* 此处是一个简易的组件加载测试脚本,用来调试歌词
*
* @author SteveXMH
*/
import type { LyricLine } from "@applemusic-like-lyrics/core";
import {
BackgroundRender,
DomLyricPlayer,
type LyricLineMouseEvent,
MeshGradientRenderer,
PixiRenderer,
} from "@applemusic-like-lyrics/core";
import * as lyrics from "@applemusic-like-lyrics/lyric";
import {
parseLrc,
parseLys,
parseQrc,
parseYrc,
type LyricLine as RawLyricLine,
} from "@applemusic-like-lyrics/lyric";
import { parseTTML } from "@applemusic-like-lyrics/ttml";
import GUI from "lil-gui";
import Stats from "stats.js";
export interface SpringParams {
mass: number; // = 1.0
damping: number; // = 10.0
stiffness: number; // = 100.0
soft: boolean; // = false
}
window.lyrics = lyrics;
const audio = document.createElement("audio");
audio.volume = 0.5;
audio.preload = "auto";
audio.addEventListener("play", () => lyricPlayer.resume());
audio.addEventListener("pause", () => lyricPlayer.pause());
const debugValues = {
lyric: new URL(location.href).searchParams.get("lyric") || "",
music: new URL(location.href).searchParams.get("music") || "",
album: new URL(location.href).searchParams.get("album") || "",
enableSpring: true,
bgFPS: 60,
bgMode: new URL(location.href).searchParams.get("bg") || "mg",
bgScale: 1,
bgFlowSpeed: 0.2,
bgPlaying: true,
bgStaticMode: false,
currentTime: 0,
enableBlur: true,
playing: false,
async mockPlay() {
this.playing = true;
const startTime = Date.now();
const baseTime = this.currentTime * 1000;
while (this.playing && this.currentTime < 300) {
const time = Date.now() - startTime;
this.currentTime = (baseTime + time) / 1000;
progress.updateDisplay();
lyricPlayer.setCurrentTime(baseTime + time);
await waitFrame();
}
},
forceUpdateAlbum() {
window.globalBackground.setAlbum(debugValues.album);
},
forceUpdateLyric() {
loadLyric();
},
play() {
this.playing = true;
audio.load();
audio.play();
},
pause() {
this.playing = false;
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
},
fadeWidth: 0.5,
lineSprings: {
posX: {
mass: 1,
damping: 10,
stiffness: 100,
soft: false,
} as SpringParams,
posY: {
mass: 1,
damping: 15,
stiffness: 100,
soft: false,
} as SpringParams,
scale: {
mass: 1,
damping: 20,
stiffness: 100,
soft: false,
} as SpringParams,
},
};
function recreateBGRenderer(mode: string) {
window.globalBackground?.dispose();
if (mode === "pixi") {
window.globalBackground = BackgroundRender.new(PixiRenderer);
} else if (mode === "mg") {
window.globalBackground = BackgroundRender.new(MeshGradientRenderer);
} else {
throw new Error("Unknown renderer mode");
}
const bg = window.globalBackground;
bg.setFPS(debugValues.bgFPS);
bg.setRenderScale(debugValues.bgScale);
bg.setStaticMode(debugValues.bgStaticMode);
bg.setFlowSpeed(debugValues.bgFlowSpeed);
bg.getElement().style.position = "absolute";
bg.getElement().style.top = "0";
bg.getElement().style.left = "0";
bg.getElement().style.width = "100%";
bg.getElement().style.height = "100%";
bg.setAlbum(debugValues.album);
}
audio.src = debugValues.music;
audio.load();
const gui = new GUI();
gui.close();
gui.title("AMLL 歌词测试页面");
const lyricController = gui
.add(debugValues, "lyric")
.name("歌词文件")
.onFinishChange(async (url: string) => {
lyricPlayer.setLyricLines(parseTTML(await (await fetch(url)).text()).lines);
});
const localFileApi = {
openLocalLyricFile() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".ttml,.lrc,.yrc,.lys,.qrc";
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
localLyricExt = file.name;
if (localLyricUrl) {
URL.revokeObjectURL(localLyricUrl);
}
localLyricUrl = URL.createObjectURL(file);
debugValues.lyric = localLyricUrl;
lyricController.updateDisplay();
await loadLyric();
};
input.click();
},
openLocalMusicFile() {
const input = document.createElement("input");
input.type = "file";
input.accept = "audio/*";
input.onchange = () => {
const file = input.files?.[0];
if (!file) return;
if (localMusicUrl) {
URL.revokeObjectURL(localMusicUrl);
}
localMusicUrl = URL.createObjectURL(file);
debugValues.music = localMusicUrl;
audio.src = localMusicUrl;
audio.load();
musicController.updateDisplay();
};
input.click();
},
};
gui.add(localFileApi, "openLocalLyricFile").name("打开本地歌词");
gui.add(localFileApi, "openLocalMusicFile").name("打开本地歌曲");
const musicController = gui
.add(debugValues, "music")
.name("歌曲")
.onFinishChange((v: string) => {
audio.src = v;
});
gui
.add(debugValues, "album")
.name("专辑图片")
.onFinishChange((v: string) => {
window.globalBackground.setAlbum(v);
});
gui.add(debugValues, "forceUpdateAlbum").name("强制更新专辑图片");
gui.add(debugValues, "forceUpdateLyric").name("强制更新歌词");
const bgGui = gui.addFolder("背景");
bgGui
.add(debugValues, "bgPlaying")
.name("播放")
.onFinishChange((v: boolean) => {
if (v) {
window.globalBackground.resume();
} else {
window.globalBackground.pause();
}
});
bgGui
.add(debugValues, "bgMode", ["pixi", "mg"])
.name("背景渲染器")
.onFinishChange((v: string) => {
recreateBGRenderer(v);
});
bgGui
.add(debugValues, "bgScale", 0.01, 1, 0.01)
.name("分辨率比率")
.onChange((v: number) => {
window.globalBackground.setRenderScale(v);
});
bgGui
.add(debugValues, "bgFPS", 1, 1000, 1)
.name("帧率")
.onFinishChange((v: number) => {
window.globalBackground.setFPS(v);
});
bgGui
.add(debugValues, "bgFlowSpeed", 0, 10, 0.1)
.name("流动速度")
.onFinishChange((v: number) => {
window.globalBackground.setFlowSpeed(v);
});
bgGui
.add(debugValues, "bgStaticMode")
.name("静态模式")
.onFinishChange((v: boolean) => {
window.globalBackground.setStaticMode(v);
});
{
const animation = gui.addFolder("歌词行动画/效果");
animation
.add(debugValues, "fadeWidth", 0, 10, 0.01)
.name("歌词渐变宽度")
.onChange((v: number) => {
lyricPlayer.setWordFadeWidth(v);
});
animation
.add(debugValues, "enableBlur")
.name("启用歌词模糊")
.onChange((v: boolean) => {
lyricPlayer.setEnableBlur(v);
});
animation
.add(debugValues, "enableSpring")
.name("使用弹簧动画")
.onChange((v: boolean) => {
lyricPlayer.setEnableSpring(v);
});
function addSpringDbg(name: string, obj: SpringParams, onChange: () => void) {
const x = animation.addFolder(name);
x.close();
x.add(obj, "mass").name("质量").onFinishChange(onChange);
x.add(obj, "damping").name("阻力").onFinishChange(onChange);
x.add(obj, "stiffness").name("弹性").onFinishChange(onChange);
x.add(obj, "soft")
.name("强制软弹簧(当阻力小于 1 时有用)")
.onFinishChange(onChange);
}
addSpringDbg("水平位移弹簧", debugValues.lineSprings.posX, () => {
lyricPlayer.setLinePosXSpringParams(debugValues.lineSprings.posX);
});
addSpringDbg("垂直位移弹簧", debugValues.lineSprings.posY, () => {
lyricPlayer.setLinePosYSpringParams(debugValues.lineSprings.posY);
});
addSpringDbg("缩放弹簧", debugValues.lineSprings.scale, () => {
lyricPlayer.setLineScaleSpringParams(debugValues.lineSprings.scale);
});
}
const playerGui = gui.addFolder("音乐播放器");
const progress = playerGui
.add(debugValues, "currentTime")
.min(0)
.step(1)
.name("当前进度")
.onChange((v: number) => {
audio.currentTime = v;
lyricPlayer.setCurrentTime(v * 1000, true);
});
playerGui.add(debugValues, "play").name("加载/播放");
playerGui.add(debugValues, "pause").name("暂停/继续");
const lyricPlayer = new DomLyricPlayer();
lyricPlayer.addEventListener("line-click", (evt) => {
const e = evt as LyricLineMouseEvent;
evt.preventDefault();
evt.stopImmediatePropagation();
evt.stopPropagation();
console.log(e.line, e.lineIndex);
const time = e.line.getLine().startTime;
lyricPlayer.setCurrentTime(time, true);
audio.currentTime = time / 1000;
});
const stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
let lastTime = -1;
const frame = (time: number) => {
stats.end();
if (lastTime === -1) {
lastTime = time;
}
if (!audio.paused) {
const time = (audio.currentTime * 1000) | 0;
debugValues.currentTime = (time / 1000) | 0;
progress.max(audio.duration | 0);
progress.updateDisplay();
lyricPlayer.setCurrentTime(time);
}
lyricPlayer.update(time - lastTime);
lastTime = time;
stats.begin();
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
declare global {
interface Window {
globalLyricPlayer: DomLyricPlayer;
globalBackground:
| BackgroundRender<PixiRenderer>
| BackgroundRender<MeshGradientRenderer>;
lyrics: typeof lyrics;
}
}
window.globalLyricPlayer = lyricPlayer;
const waitFrame = (): Promise<number> =>
new Promise((resolve) => requestAnimationFrame(resolve));
let localLyricUrl: string | null = null;
let localLyricExt: string | null = null;
let localMusicUrl: string | null = null;
const mapLyric = (
line: RawLyricLine,
_i: number,
_lines: RawLyricLine[],
): LyricLine => ({
words: line.words.map((word) => ({
...word,
obscene: false,
romanWord: word.romanWord ?? "",
})),
startTime: line.words[0]?.startTime ?? 0,
endTime:
line.words[line.words.length - 1]?.endTime ?? Number.POSITIVE_INFINITY,
translatedLyric: "",
romanLyric: "",
isBG: false,
isDuet: false,
});
async function loadLyric() {
const lyricFile = debugValues.lyric;
const content = await (await fetch(lyricFile)).text();
const lyricSource = (localLyricExt ?? lyricFile).toLowerCase();
if (lyricSource.endsWith(".ttml")) {
lyricPlayer.setLyricLines(parseTTML(content).lines);
} else if (lyricSource.endsWith(".lrc")) {
lyricPlayer.setLyricLines(parseLrc(content).map(mapLyric));
} else if (lyricSource.endsWith(".yrc")) {
lyricPlayer.setLyricLines(parseYrc(content).map(mapLyric));
} else if (lyricSource.endsWith(".lys")) {
lyricPlayer.setLyricLines(parseLys(content).map(mapLyric));
} else if (lyricSource.endsWith(".qrc")) {
lyricPlayer.setLyricLines(parseQrc(content).map(mapLyric));
} else if (lyricFile === "bug") {
const buildLyricLines = (
lyric: string,
startTime = 1000,
otherParams: Partial<LyricLine> = {},
): LyricLine => {
let curTime = startTime;
const words = [];
for (const word of lyric.split("|")) {
const [text, duration] = word.split(",");
const endTime = curTime + Number.parseInt(duration, 10);
words.push({
word: text,
romanWord: "",
startTime: curTime,
endTime,
obscene: false,
});
curTime = endTime;
}
return {
startTime,
endTime: curTime + 3000,
translatedLyric: "",
romanLyric: "",
isBG: false,
isDuet: false,
words,
...otherParams,
};
};
const DEMO_LYRIC: LyricLine[] = [
buildLyricLines(
"Apple ,750|Music ,500|Like ,500|Ly,400|ri,500|cs ,250",
1000,
),
buildLyricLines("BG ,750|Lyrics ,1000", 2000, {
isBG: true,
}),
buildLyricLines("Next ,1000|Lyrics,1000", 2500, {
// isDuet: true,
}),
];
lyricPlayer.setLyricLines(DEMO_LYRIC);
}
}
(async () => {
recreateBGRenderer(debugValues.bgMode);
audio.style.display = "none";
// lyricPlayer.getBottomLineElement().innerHTML = "Test Bottom Line";
const player = document.getElementById("player");
if (player) {
player.appendChild(audio);
player.appendChild(window.globalBackground.getElement());
player.appendChild(lyricPlayer.getElement());
}
if (!debugValues.enableSpring) {
lyricPlayer.setEnableSpring(false);
}
await loadLyric();
// debugValues.play();
// debugValues.currentTime = 34;
// debugValues.mockPlay();
})();

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src"]
}

View File

@@ -0,0 +1,20 @@
import path from "node:path";
import { defineConfig } from "vite";
export default defineConfig({
resolve: {
alias: {
"@applemusic-like-lyrics/core": path.resolve(__dirname, "../../core/src"),
"@applemusic-like-lyrics/core/style.css": path.resolve(
__dirname,
"../../core/src/styles/index.css",
),
"@applemusic-like-lyrics/lyric": path.resolve(
__dirname,
"../../lyric/src",
),
"@applemusic-like-lyrics/ttml": path.resolve(__dirname, "../../ttml/src"),
"@amll-core-src": path.resolve(__dirname, "../../core/src"),
},
},
});