forked from miao-moe/QZMusic_PC
fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork) - 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
37
amll-local/packages/playground/core-legacy/index.html
Normal file
37
amll-local/packages/playground/core-legacy/index.html
Normal 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>
|
||||
91
amll-local/packages/playground/core-legacy/mg.html
Normal file
91
amll-local/packages/playground/core-legacy/mg.html
Normal 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>
|
||||
32
amll-local/packages/playground/core-legacy/package.json
Normal file
32
amll-local/packages/playground/core-legacy/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
504
amll-local/packages/playground/core-legacy/src/mg-test.ts
Normal file
504
amll-local/packages/playground/core-legacy/src/mg-test.ts
Normal 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);
|
||||
449
amll-local/packages/playground/core-legacy/src/test.ts
Normal file
449
amll-local/packages/playground/core-legacy/src/test.ts
Normal 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();
|
||||
})();
|
||||
4
amll-local/packages/playground/core-legacy/tsconfig.json
Normal file
4
amll-local/packages/playground/core-legacy/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
20
amll-local/packages/playground/core-legacy/vite.config.ts
Normal file
20
amll-local/packages/playground/core-legacy/vite.config.ts
Normal 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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user