forked from miao-moe/QZMusic_PC
fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork) - 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
76
amll-local/packages/react/CHANGELOG.md
Normal file
76
amll-local/packages/react/CHANGELOG.md
Normal file
@@ -0,0 +1,76 @@
|
||||
## 0.5.1 (2026-05-17)
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- **fix:** 修复含对唱时歌词错误提前导致的多行高亮 ([#521](https://github.com/amll-dev/applemusic-like-lyrics/pull/521))
|
||||
- **refactor:** 引入歌词组来包装主歌词和背景人声 & 前置背景人声 ([#531](https://github.com/amll-dev/applemusic-like-lyrics/pull/531))
|
||||
- **chore:** 更正 package.json 协议声明 ([#534](https://github.com/amll-dev/applemusic-like-lyrics/pull/534))
|
||||
|
||||
仓库根目录的 LICENSE 文件为 AGPL v3.0 协议,但是 package.json 中的 `license` 字段为 `GPL-3.0`。经与原开发者确认,package.json 中的 `license` 字段有误。仓库与其所有产出的 npm 包均应为 AGPL v3 only 协议,SPDX: `AGPL-3.0-only`。因此,更正各包 `package.json` 的 `license` 字段为 `AGPL-3.0-only`。
|
||||
- **chore(core):** 优化类型定义 ([#519](https://github.com/amll-dev/applemusic-like-lyrics/pull/519))
|
||||
|
||||
### Contributors
|
||||
|
||||
- apoint123 [@apoint123](https://github.com/apoint123)
|
||||
- Linho [@Linho1219](https://github.com/Linho1219)
|
||||
|
||||
## 0.5.0 (2026-05-12)
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- **refactor:** 整理核心播放器代码结构,将抽象接口部分集中到统一目录 ([#508](https://github.com/amll-dev/applemusic-like-lyrics/pull/508))
|
||||
- **refactor:** 整理核心播放器抽象类中时间线、滚动与单行布局部分的结构与状态管理 ([#509](https://github.com/amll-dev/applemusic-like-lyrics/pull/509))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- **fix:** 修复 setCurrentTime 在提供 isSeek 标志时,实际排版未遵守标志导致布局异常漂移的问题 ([#509](https://github.com/amll-dev/applemusic-like-lyrics/pull/509))
|
||||
- **fix:** 修复在同一行时间内拖拽进度条时逐字动画不同步的问题 ([#509](https://github.com/amll-dev/applemusic-like-lyrics/pull/509))
|
||||
- **fix:** 修复暂停状态下点击行跳转时仍播放逐字动画的问题 ([#509](https://github.com/amll-dev/applemusic-like-lyrics/pull/509))
|
||||
|
||||
### Contributors
|
||||
|
||||
- Linho [@Linho1219](https://github.com/Linho1219)
|
||||
|
||||
## 0.4.2 (2026-05-01)
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- **feat(core):** 平衡行长度时优先在标点处换行 ([#503](https://github.com/amll-dev/applemusic-like-lyrics/pull/503))
|
||||
- **fix:** 修复背景行注音高度错误 ([#497](https://github.com/amll-dev/applemusic-like-lyrics/pull/497))
|
||||
- **fix(core):** 修正平衡行长度时的行宽度计算 ([#502](https://github.com/amll-dev/applemusic-like-lyrics/pull/502))
|
||||
|
||||
### Contributors
|
||||
|
||||
- apoint123 [@apoint123](https://github.com/apoint123)
|
||||
- Linho [@Linho1219](https://github.com/Linho1219)
|
||||
|
||||
## 0.4.1 (2026-04-23)
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- **fix:** 在各绑定中暴露歌词优化选项 ([#492](https://github.com/amll-dev/applemusic-like-lyrics/pull/492))
|
||||
- **fix(vue):** 修复掩码模式错误的类型 ([#496](https://github.com/amll-dev/applemusic-like-lyrics/pull/496))
|
||||
- **refactor(core):** 重构平均行长度实现 ([#494](https://github.com/amll-dev/applemusic-like-lyrics/pull/494))
|
||||
|
||||
### Contributors
|
||||
|
||||
- apoint123 [@apoint123](https://github.com/apoint123)
|
||||
|
||||
## 0.4.0 (2026-04-14)
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- **chore:** 移除 canvas 歌词渲染器 ([#476](https://github.com/amll-dev/applemusic-like-lyrics/pull/476))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- **refactor:** 重构核心库测试组织模式 ([3db83c93](https://github.com/amll-dev/applemusic-like-lyrics/commit/3db83c93))
|
||||
- **docs:** 修正 optimize-lyric.ts 和 OptimizeLyricOptions 里 cleanUnintentionalOverlaps 的文档和注释 ([75a8c0bb](https://github.com/amll-dev/applemusic-like-lyrics/commit/75a8c0bb))
|
||||
- **chore:** 更换工具链 ([#476](https://github.com/amll-dev/applemusic-like-lyrics/pull/476))
|
||||
- **chore:** 在项目范围内启用 isolatedDeclarations ([#480](https://github.com/amll-dev/applemusic-like-lyrics/pull/480))
|
||||
|
||||
### Contributors
|
||||
|
||||
- apoint123 [@apoint123](https://github.com/apoint123)
|
||||
- Linho [@Linho1219](https://github.com/Linho1219)
|
||||
- MoYingJi [@MoYingJi](https://github.com/MoYingJi)
|
||||
49
amll-local/packages/react/README-CN.md
Normal file
49
amll-local/packages/react/README-CN.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# AMLL for React
|
||||
|
||||
[English](./README.md) / 简体中文
|
||||
|
||||
> 警告:此为个人项目,且尚未完成开发,可能仍有大量问题,所以请勿直接用于生产环境!
|
||||
|
||||

|
||||
[](https://www.npmjs.com/package/@applemusic-like-lyrics/react)
|
||||
[](https://www.npmjs.com/package/@applemusic-like-lyrics/react)
|
||||
|
||||
AMLL 组件库的 React 绑定,你可以通过此库来更加方便地使用 AMLL 歌词组件。
|
||||
|
||||
详情可以访问 [Core 核心组件的 README.md](../core/README-CN.md)。
|
||||
|
||||
## 安装
|
||||
|
||||
安装使用的依赖(如果以下列出的依赖包没有安装的话需要自行安装):
|
||||
```bash
|
||||
npm install @pixi/app @pixi/core @pixi/display @pixi/filter-blur @pixi/filter-bulge-pinch @pixi/filter-color-matrix @pixi/sprite jss jss-preset-default # 使用 npm
|
||||
yarn add @pixi/app @pixi/core @pixi/display @pixi/filter-blur @pixi/filter-bulge-pinch @pixi/filter-color-matrix @pixi/sprite jss jss-preset-default # 使用 yarn
|
||||
```
|
||||
|
||||
安装 React 绑定需要使用的依赖(如果以下列出的依赖包没有安装的话需要自行安装):
|
||||
```bash
|
||||
npm install react react-dom # 使用 npm
|
||||
yarn add react react-dom # 使用 yarn
|
||||
```
|
||||
|
||||
安装本体框架:
|
||||
```bash
|
||||
npm install @applemusic-like-lyrics/react # 使用 npm
|
||||
yarn add @applemusic-like-lyrics/react # 使用 yarn
|
||||
```
|
||||
|
||||
## 使用方式摘要
|
||||
|
||||
详细的 API 文档请参考 [./docs/modules.md](./docs/modules.md)
|
||||
|
||||
一个测试用途的程序可以在 [../playground/react/src/test.tsx](../playground/react/src/test.tsx) 里找到。
|
||||
|
||||
```tsx
|
||||
import { LyricPlayer } from "@applemusic-like-lyrics/react";
|
||||
|
||||
const App = () => {
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [lyricLines, setLyricLines] = useState<LyricLine[]>([]);
|
||||
return <LyricPlayer lyricLines={lyricLines} currentTime={currentTime} />
|
||||
};
|
||||
```
|
||||
49
amll-local/packages/react/README.md
Normal file
49
amll-local/packages/react/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# AMLL for React
|
||||
|
||||
English / [简体中文](./README-CN.md)
|
||||
|
||||
> Warning: This is a personal project and is still under development. There may still be many issues, so please do not use it directly in production environments!
|
||||
|
||||

|
||||
[](https://www.npmjs.com/package/@applemusic-like-lyrics/react)
|
||||
[](https://www.npmjs.com/package/@applemusic-like-lyrics/react)
|
||||
|
||||
React binding for the AMLL component library, which allows you to use AMLL lyric components more conveniently.
|
||||
|
||||
For more details, please visit [Core component README.md](../core/README.md).
|
||||
|
||||
## Installation
|
||||
|
||||
Install the required dependencies (if the dependencies listed below are not installed, you need to install them yourself):
|
||||
```bash
|
||||
npm install @pixi/app @pixi/core @pixi/display @pixi/filter-blur @pixi/filter-bulge-pinch @pixi/filter-color-matrix @pixi/sprite jss jss-preset-default # using npm
|
||||
yarn add @pixi/app @pixi/core @pixi/display @pixi/filter-blur @pixi/filter-bulge-pinch @pixi/filter-color-matrix @pixi/sprite jss jss-preset-default # using yarn
|
||||
```
|
||||
|
||||
Install the dependencies required for React binding (if the dependencies listed below are not installed, you need to install them yourself):
|
||||
```bash
|
||||
npm install react react-dom # using npm
|
||||
yarn add react react-dom # using yarn
|
||||
```
|
||||
|
||||
Install the framework:
|
||||
```bash
|
||||
npm install @applemusic-like-lyrics/react # using npm
|
||||
yarn add @applemusic-like-lyrics/react # using yarn
|
||||
```
|
||||
|
||||
## Usage Summary
|
||||
|
||||
For detailed API documentation, please refer to [./docs/modules.md](./docs/modules.md)
|
||||
|
||||
A test program can be found in [../playground/react/src/test.tsx](../playground/react/src/test.tsx).
|
||||
|
||||
```tsx
|
||||
import { LyricPlayer } from "@applemusic-like-lyrics/react";
|
||||
|
||||
const App = () => {
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [lyricLines, setLyricLines] = useState<LyricLine[]>([]);
|
||||
return <LyricPlayer lyricLines={lyricLines} currentTime={currentTime} />
|
||||
};
|
||||
```
|
||||
73
amll-local/packages/react/package.json
Normal file
73
amll-local/packages/react/package.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@applemusic-like-lyrics/react",
|
||||
"version": "0.5.1",
|
||||
"description": "AMLL 组件库的 React 绑定",
|
||||
"repository": {
|
||||
"url": "https://github.com/amll-dev/applemusic-like-lyrics.git",
|
||||
"directory": "packages/react",
|
||||
"type": "git"
|
||||
},
|
||||
"license": "AGPL-3.0-only",
|
||||
"nx": {
|
||||
"tags": [
|
||||
"library"
|
||||
],
|
||||
"targets": {
|
||||
"nx-release-publish": {
|
||||
"executor": "@nx/js:release-publish",
|
||||
"dependsOn": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build:docs": "typedoc --plugin typedoc-plugin-markdown --out docs src/index.ts",
|
||||
"typecheck": "tsgo -b",
|
||||
"build-only": "tsdown",
|
||||
"build": "run-p typecheck \"build-only {@}\" --",
|
||||
"build:dev": "tsdown",
|
||||
"fmt": "biome format --write ./src",
|
||||
"dev": "nx run @applemusic-like-lyrics/playground-react:dev"
|
||||
},
|
||||
"main": "./dist/amll-react.cjs",
|
||||
"module": "./dist/amll-react.mjs",
|
||||
"typings": "./dist/amll-react.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/amll-react.d.mts",
|
||||
"default": "./dist/amll-react.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/amll-react.d.cts",
|
||||
"default": "./dist/amll-react.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typedoc": "catalog:",
|
||||
"typedoc-plugin-markdown": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@applemusic-like-lyrics/core": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pixi/app": "*",
|
||||
"@pixi/core": "*",
|
||||
"@pixi/display": "*",
|
||||
"@pixi/filter-blur": "*",
|
||||
"@pixi/filter-color-matrix": "*",
|
||||
"@pixi/sprite": "*",
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
}
|
||||
219
amll-local/packages/react/src/bg-render.tsx
Normal file
219
amll-local/packages/react/src/bg-render.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import {
|
||||
type AbstractBaseRenderer,
|
||||
type BaseRenderer,
|
||||
BackgroundRender as CoreBackgroundRender,
|
||||
MeshGradientRenderer,
|
||||
} from "@applemusic-like-lyrics/core";
|
||||
import {
|
||||
type ForwardRefExoticComponent,
|
||||
forwardRef,
|
||||
type HTMLProps,
|
||||
type RefAttributes,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
export {
|
||||
BaseRenderer,
|
||||
MeshGradientRenderer,
|
||||
PixiRenderer,
|
||||
} from "@applemusic-like-lyrics/core";
|
||||
|
||||
/**
|
||||
* 背景渲染组件的属性
|
||||
*/
|
||||
export interface BackgroundRenderProps {
|
||||
/**
|
||||
* 设置背景专辑资源
|
||||
*/
|
||||
album?: string | HTMLImageElement | HTMLVideoElement;
|
||||
/**
|
||||
* 设置专辑资源是否为视频
|
||||
*/
|
||||
albumIsVideo?: boolean;
|
||||
/**
|
||||
* 设置当前背景动画帧率,如果为 `undefined` 则默认为 `30`
|
||||
*/
|
||||
fps?: number;
|
||||
/**
|
||||
* 设置当前播放状态,如果为 `undefined` 则默认为 `true`
|
||||
*/
|
||||
playing?: boolean;
|
||||
/**
|
||||
* 设置当前动画流动速度,如果为 `undefined` 则默认为 `2`
|
||||
*/
|
||||
flowSpeed?: number;
|
||||
/**
|
||||
* 设置背景是否根据“是否有歌词”这个特征调整自身效果,例如有歌词时会变得更加活跃
|
||||
*
|
||||
* 部分渲染器会根据这个特征调整自身效果
|
||||
*
|
||||
* 如果不确定是否需要赋值或无法知晓是否包含歌词,请传入 true 或不做任何处理(默认值为 true)
|
||||
*/
|
||||
hasLyric?: boolean;
|
||||
/**
|
||||
* 设置低频的音量大小,范围在 80hz-120hz 之间为宜,取值范围在 [0.0-1.0] 之间
|
||||
*
|
||||
* 部分渲染器会根据音量大小调整背景效果(例如根据鼓点跳动)
|
||||
*
|
||||
* 如果无法获取到类似的数据,请传入 undefined 或 1.0 作为默认值,或不做任何处理(默认值即 1.0)
|
||||
*/
|
||||
lowFreqVolume?: number;
|
||||
/**
|
||||
* 设置当前渲染缩放比例,如果为 `undefined` 则默认为 `0.5`
|
||||
*/
|
||||
renderScale?: number;
|
||||
/**
|
||||
* 是否启用静态模式,即图片在更换后就会保持静止状态并禁用更新,以节省性能
|
||||
* 默认为 `false`
|
||||
*/
|
||||
staticMode?: boolean;
|
||||
/**
|
||||
* 设置渲染器,如果为 `undefined` 则默认为 `PixiRenderer`
|
||||
* 默认渲染器有可能会随着版本更新而更换
|
||||
*/
|
||||
renderer?: {
|
||||
new (...args: ConstructorParameters<typeof BaseRenderer>): BaseRenderer;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 背景渲染组件的引用
|
||||
*/
|
||||
export interface BackgroundRenderRef {
|
||||
/**
|
||||
* 背景渲染实例引用
|
||||
*/
|
||||
bgRender?: AbstractBaseRenderer;
|
||||
/**
|
||||
* 将背景渲染实例的元素包裹起来的 DIV 元素实例
|
||||
*/
|
||||
wrapperEl: HTMLDivElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流体背景渲染组件,通过提供图片链接可以显示出酷似 Apple Music 的流体背景效果
|
||||
*/
|
||||
export const BackgroundRender: ForwardRefExoticComponent<
|
||||
Omit<HTMLProps<HTMLDivElement> & BackgroundRenderProps, "ref"> &
|
||||
RefAttributes<BackgroundRenderRef>
|
||||
> = forwardRef<
|
||||
BackgroundRenderRef,
|
||||
HTMLProps<HTMLDivElement> & BackgroundRenderProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
album,
|
||||
albumIsVideo,
|
||||
fps,
|
||||
playing,
|
||||
flowSpeed,
|
||||
renderScale,
|
||||
staticMode,
|
||||
lowFreqVolume,
|
||||
hasLyric,
|
||||
renderer,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const coreBGRenderRef = useRef<AbstractBaseRenderer>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const lastRendererRef = useRef<{
|
||||
new (canvas: HTMLCanvasElement): BaseRenderer;
|
||||
}>(null);
|
||||
const curRenderer = renderer ?? MeshGradientRenderer;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
lastRendererRef.current !== curRenderer ||
|
||||
coreBGRenderRef.current === undefined
|
||||
) {
|
||||
lastRendererRef.current = curRenderer;
|
||||
coreBGRenderRef.current?.dispose();
|
||||
coreBGRenderRef.current = CoreBackgroundRender.new(curRenderer);
|
||||
}
|
||||
}, [curRenderer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (curRenderer && album)
|
||||
coreBGRenderRef.current?.setAlbum(album, albumIsVideo);
|
||||
}, [curRenderer, album, albumIsVideo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (curRenderer && fps) coreBGRenderRef.current?.setFPS(fps);
|
||||
}, [curRenderer, fps]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!curRenderer) return;
|
||||
if (playing === undefined) {
|
||||
coreBGRenderRef.current?.resume();
|
||||
} else if (playing) {
|
||||
coreBGRenderRef.current?.resume();
|
||||
} else {
|
||||
coreBGRenderRef.current?.pause();
|
||||
}
|
||||
}, [curRenderer, playing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!curRenderer) return;
|
||||
if (flowSpeed) coreBGRenderRef.current?.setFlowSpeed(flowSpeed);
|
||||
}, [curRenderer, flowSpeed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!curRenderer) return;
|
||||
coreBGRenderRef.current?.setStaticMode(staticMode ?? false);
|
||||
}, [curRenderer, staticMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (curRenderer && renderScale)
|
||||
coreBGRenderRef.current?.setRenderScale(renderScale ?? 0.5);
|
||||
}, [curRenderer, renderScale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (curRenderer && lowFreqVolume)
|
||||
coreBGRenderRef.current?.setLowFreqVolume(lowFreqVolume ?? 1.0);
|
||||
}, [curRenderer, lowFreqVolume]);
|
||||
|
||||
useEffect(() => {
|
||||
if (curRenderer && hasLyric !== undefined)
|
||||
coreBGRenderRef.current?.setHasLyric(hasLyric ?? true);
|
||||
}, [curRenderer, hasLyric]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: coreBGRenderRef.current
|
||||
useEffect(() => {
|
||||
if (coreBGRenderRef.current) {
|
||||
const el = coreBGRenderRef.current.getElement();
|
||||
el.style.width = "100%";
|
||||
el.style.height = "100%";
|
||||
el.style.minHeight = "0";
|
||||
el.style.minWidth = "0";
|
||||
el.style.overflow = "hidden";
|
||||
wrapperRef.current?.appendChild(el);
|
||||
}
|
||||
}, [coreBGRenderRef.current]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: wrapperRef.current, coreBGRenderRef.current
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
wrapperEl: wrapperRef.current,
|
||||
bgRender: coreBGRenderRef.current as AbstractBaseRenderer,
|
||||
}),
|
||||
[wrapperRef.current, coreBGRenderRef.current],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "contents",
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
ref={wrapperRef}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
2
amll-local/packages/react/src/index.ts
Normal file
2
amll-local/packages/react/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./bg-render";
|
||||
export * from "./lyric-player";
|
||||
385
amll-local/packages/react/src/lyric-player.tsx
Normal file
385
amll-local/packages/react/src/lyric-player.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
import type {
|
||||
LyricLine,
|
||||
LyricLineMouseEvent,
|
||||
LyricPlayerBase,
|
||||
OptimizeLyricOptions,
|
||||
spring,
|
||||
} from "@applemusic-like-lyrics/core";
|
||||
import {
|
||||
LyricPlayer as DefaultLyricPlayer,
|
||||
MaskObsceneWordsMode,
|
||||
} from "@applemusic-like-lyrics/core";
|
||||
import {
|
||||
type ForwardRefExoticComponent,
|
||||
forwardRef,
|
||||
type HTMLProps,
|
||||
type RefAttributes,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
/**
|
||||
* 歌词播放组件的属性
|
||||
*/
|
||||
export interface LyricPlayerProps {
|
||||
/**
|
||||
* 是否禁用歌词播放组件,默认为 `false`,歌词组件启用后将会开始逐帧更新歌词的动画效果,并对传入的其他参数变更做出反馈。
|
||||
*
|
||||
* 如果禁用了歌词组件动画,你也可以通过引用取得原始渲染组件实例,手动逐帧调用其 `update` 函数来更新动画效果。
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 是否演出部分效果,目前会控制播放间奏点的动画的播放暂停与否,默认为 `true`
|
||||
*/
|
||||
playing?: boolean;
|
||||
/**
|
||||
* 设置歌词行的对齐方式,如果为 `undefined` 则默认为 `center`
|
||||
*
|
||||
* - 设置成 `top` 的话将会向目标歌词行的顶部对齐
|
||||
* - 设置成 `bottom` 的话将会向目标歌词行的底部对齐
|
||||
* - 设置成 `center` 的话将会向目标歌词行的垂直中心对齐
|
||||
*/
|
||||
alignAnchor?: "top" | "bottom" | "center";
|
||||
/**
|
||||
* 设置默认的歌词行对齐位置,相对于整个歌词播放组件的大小位置,如果为 `undefined`
|
||||
* 则默认为 `0.5`
|
||||
*
|
||||
* 可以设置一个 `[0.0-1.0]` 之间的任意数字,代表组件高度由上到下的比例位置
|
||||
*/
|
||||
alignPosition?: number;
|
||||
/**
|
||||
* 设置是否使用物理弹簧算法实现歌词动画效果,默认启用
|
||||
*
|
||||
* 如果启用,则会通过弹簧算法实时处理歌词位置,但是需要性能足够强劲的电脑方可流畅运行
|
||||
*
|
||||
* 如果不启用,则会回退到基于 `transition` 的过渡效果,对低性能的机器比较友好,但是效果会比较单一
|
||||
*/
|
||||
enableSpring?: boolean;
|
||||
/**
|
||||
* 设置是否启用歌词行的模糊效果,默认为 `true`
|
||||
*/
|
||||
enableBlur?: boolean;
|
||||
/**
|
||||
* 设置是否使用物理弹簧算法实现歌词动画效果,默认启用
|
||||
*
|
||||
* 如果启用,则会通过弹簧算法实时处理歌词位置,但是需要性能足够强劲的电脑方可流畅运行
|
||||
*
|
||||
* 如果不启用,则会回退到基于 `transition` 的过渡效果,对低性能的机器比较友好,但是效果会比较单一
|
||||
*/
|
||||
enableScale?: boolean;
|
||||
/**
|
||||
* 设置是否隐藏已经播放过的歌词行,默认不隐藏
|
||||
*/
|
||||
hidePassedLines?: boolean;
|
||||
/**
|
||||
* 设置歌词中不雅用语的掩码模式,默认为 `MaskObsceneWordsMode.Disabled`,即不掩码
|
||||
*/
|
||||
maskObsceneWordsMode?: MaskObsceneWordsMode;
|
||||
/**
|
||||
* 设置不雅用语掩码使用的字符,默认为 `*`
|
||||
*/
|
||||
maskObsceneWordChar?: string;
|
||||
/**
|
||||
* 设置歌词优化选项
|
||||
*/
|
||||
optimizeOptions?: OptimizeLyricOptions;
|
||||
/**
|
||||
* 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
|
||||
*/
|
||||
lyricLines?: LyricLine[];
|
||||
/**
|
||||
* 设置当前播放进度,单位为毫秒且**必须是整数**,此时将会更新内部的歌词进度信息
|
||||
* 内部会根据调用间隔和播放进度自动决定如何滚动和显示歌词,所以这个的调用频率越快越准确越好
|
||||
*/
|
||||
currentTime?: number;
|
||||
isSeeking?: boolean;
|
||||
/**
|
||||
* 设置文字动画的渐变宽度,单位以歌词行的主文字字体大小的倍数为单位,默认为 0.5,即一个全角字符的一半宽度
|
||||
*
|
||||
* 如果要模拟 Apple Music for Android 的效果,可以设置为 1
|
||||
*
|
||||
* 如果要模拟 Apple Music for iPad 的效果,可以设置为 0.5
|
||||
*
|
||||
* 如果想要近乎禁用渐变效果,可以设置成非常接近 0 的小数(例如 `0.0001` ),但是**不可以为 0**
|
||||
*/
|
||||
wordFadeWidth?: number;
|
||||
/**
|
||||
* 设置所有歌词行在横坐标上的弹簧属性,包括重量、弹力和阻力。
|
||||
*
|
||||
* @param params 需要设置的弹簧属性,提供的属性将会覆盖原来的属性,未提供的属性将会保持原样
|
||||
*/
|
||||
linePosXSpringParams?: Partial<spring.SpringParams>;
|
||||
/**
|
||||
* 设置所有歌词行在纵坐标上的弹簧属性,包括重量、弹力和阻力。
|
||||
*
|
||||
* @param params 需要设置的弹簧属性,提供的属性将会覆盖原来的属性,未提供的属性将会保持原样
|
||||
*/
|
||||
linePosYSpringParams?: Partial<spring.SpringParams>;
|
||||
/**
|
||||
* 设置所有歌词行在缩放大小上的弹簧属性,包括重量、弹力和阻力。
|
||||
*
|
||||
* @param params 需要设置的弹簧属性,提供的属性将会覆盖原来的属性,未提供的属性将会保持原样
|
||||
*/
|
||||
lineScaleSpringParams?: Partial<spring.SpringParams>;
|
||||
/**
|
||||
* 在一个特殊的底栏元素内加入指定元素,默认是空白的,可以往内部添加任意元素
|
||||
*
|
||||
* 这个元素始终在歌词的底部,可以用于显示歌曲创作者等信息
|
||||
*/
|
||||
bottomLine?: Parameters<typeof createPortal>[0];
|
||||
/**
|
||||
* 需要用于创建歌词播放组件的类实例
|
||||
*/
|
||||
lyricPlayer?: {
|
||||
new (
|
||||
...args: ConstructorParameters<typeof LyricPlayerBase>
|
||||
): LyricPlayerBase;
|
||||
};
|
||||
/**
|
||||
* 当某个歌词行被左键点击时触发的事件
|
||||
* @param line 歌词行的事件对象,可以访问到对应的歌词行信息和歌词行索引
|
||||
*/
|
||||
onLyricLineClick?: (line: LyricLineMouseEvent) => void;
|
||||
/**
|
||||
* 当某个歌词行被右键点击时触发的事件
|
||||
* @param line 歌词行的事件对象,可以访问到对应的歌词行信息和歌词行索引
|
||||
*/
|
||||
onLyricLineContextMenu?: (line: LyricLineMouseEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌词播放组件的引用
|
||||
*/
|
||||
export interface LyricPlayerRef {
|
||||
/**
|
||||
* 歌词播放实例
|
||||
*/
|
||||
lyricPlayer?: LyricPlayerBase;
|
||||
/**
|
||||
* 将歌词播放实例的元素包裹起来的 DIV 元素实例
|
||||
*/
|
||||
wrapperEl: HTMLDivElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌词播放组件,本框架的核心组件
|
||||
*
|
||||
* 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
|
||||
*/
|
||||
export const LyricPlayer: ForwardRefExoticComponent<
|
||||
Omit<HTMLProps<HTMLDivElement> & LyricPlayerProps, "ref"> &
|
||||
RefAttributes<LyricPlayerRef>
|
||||
> = forwardRef<LyricPlayerRef, HTMLProps<HTMLDivElement> & LyricPlayerProps>(
|
||||
(
|
||||
{
|
||||
disabled,
|
||||
playing,
|
||||
alignAnchor,
|
||||
alignPosition,
|
||||
enableSpring,
|
||||
enableBlur,
|
||||
enableScale,
|
||||
maskObsceneWordsMode,
|
||||
maskObsceneWordChar,
|
||||
hidePassedLines,
|
||||
optimizeOptions,
|
||||
lyricLines,
|
||||
currentTime,
|
||||
isSeeking,
|
||||
wordFadeWidth,
|
||||
linePosXSpringParams,
|
||||
linePosYSpringParams,
|
||||
lineScaleSpringParams,
|
||||
bottomLine,
|
||||
lyricPlayer,
|
||||
onLyricLineClick,
|
||||
onLyricLineContextMenu,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// const corePlayerRef = useRef<LyricPlayerBase>();
|
||||
const [corePlayer, setCorePlayer] = useState<LyricPlayerBase>();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const currentTimeRef = useRef(currentTime);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const newPlayer = new (lyricPlayer ?? DefaultLyricPlayer)();
|
||||
setCorePlayer(newPlayer);
|
||||
wrapperRef.current?.appendChild(newPlayer.getElement());
|
||||
return () => {
|
||||
newPlayer?.dispose();
|
||||
setCorePlayer(undefined);
|
||||
};
|
||||
}, [lyricPlayer]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (optimizeOptions !== undefined) {
|
||||
corePlayer?.setOptimizeOptions(optimizeOptions);
|
||||
}
|
||||
|
||||
if (lyricLines !== undefined) {
|
||||
corePlayer?.setLyricLines(lyricLines, currentTimeRef.current);
|
||||
|
||||
if (currentTimeRef.current !== undefined) {
|
||||
corePlayer?.setCurrentTime(currentTimeRef.current, true);
|
||||
}
|
||||
|
||||
corePlayer?.update();
|
||||
} else {
|
||||
corePlayer?.setLyricLines([]);
|
||||
corePlayer?.update();
|
||||
}
|
||||
}, [corePlayer, lyricLines, optimizeOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled) {
|
||||
let canceled = false;
|
||||
let lastTime = -1;
|
||||
const onFrame = (time: number) => {
|
||||
if (canceled) return;
|
||||
if (lastTime === -1) {
|
||||
lastTime = time;
|
||||
}
|
||||
corePlayer?.update(time - lastTime);
|
||||
lastTime = time;
|
||||
requestAnimationFrame(onFrame);
|
||||
};
|
||||
corePlayer?.calcLayout();
|
||||
requestAnimationFrame(onFrame);
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [corePlayer, disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playing !== undefined) {
|
||||
if (playing) {
|
||||
corePlayer?.resume();
|
||||
} else {
|
||||
corePlayer?.pause();
|
||||
}
|
||||
} else corePlayer?.resume();
|
||||
}, [corePlayer, playing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (alignAnchor !== undefined) corePlayer?.setAlignAnchor(alignAnchor);
|
||||
}, [corePlayer, alignAnchor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hidePassedLines !== undefined)
|
||||
corePlayer?.setHidePassedLines(hidePassedLines);
|
||||
}, [corePlayer, hidePassedLines]);
|
||||
|
||||
useEffect(() => {
|
||||
if (alignPosition !== undefined)
|
||||
corePlayer?.setAlignPosition(alignPosition);
|
||||
}, [corePlayer, alignPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enableSpring !== undefined) corePlayer?.setEnableSpring(enableSpring);
|
||||
else corePlayer?.setEnableSpring(true);
|
||||
}, [corePlayer, enableSpring]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enableScale !== undefined) corePlayer?.setEnableScale(enableScale);
|
||||
else corePlayer?.setEnableScale(true);
|
||||
}, [corePlayer, enableScale]);
|
||||
|
||||
useEffect(() => {
|
||||
corePlayer?.setEnableBlur(enableBlur ?? true);
|
||||
}, [corePlayer, enableBlur]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (currentTime !== undefined) {
|
||||
corePlayer?.setCurrentTime(currentTime, isSeeking);
|
||||
currentTimeRef.current = currentTime;
|
||||
} else {
|
||||
corePlayer?.setCurrentTime(0);
|
||||
currentTimeRef.current = 0;
|
||||
}
|
||||
}, [corePlayer, currentTime, isSeeking]);
|
||||
|
||||
useEffect(() => {
|
||||
corePlayer?.setIsSeeking(!!isSeeking);
|
||||
}, [corePlayer, isSeeking]);
|
||||
|
||||
useEffect(() => {
|
||||
corePlayer?.setWordFadeWidth(wordFadeWidth);
|
||||
}, [corePlayer, wordFadeWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (linePosXSpringParams !== undefined)
|
||||
corePlayer?.setLinePosXSpringParams(linePosXSpringParams);
|
||||
}, [corePlayer, linePosXSpringParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (linePosYSpringParams !== undefined)
|
||||
corePlayer?.setLinePosYSpringParams(linePosYSpringParams);
|
||||
}, [corePlayer, linePosYSpringParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lineScaleSpringParams !== undefined)
|
||||
corePlayer?.setLineScaleSpringParams(lineScaleSpringParams);
|
||||
}, [corePlayer, lineScaleSpringParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (maskObsceneWordsMode !== undefined) {
|
||||
corePlayer?.setMaskObsceneWords(maskObsceneWordsMode);
|
||||
} else {
|
||||
corePlayer?.setMaskObsceneWords(MaskObsceneWordsMode.Disabled);
|
||||
}
|
||||
}, [corePlayer, maskObsceneWordsMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (maskObsceneWordChar !== undefined) {
|
||||
corePlayer?.setMaskObsceneWordChar(maskObsceneWordChar);
|
||||
}
|
||||
}, [corePlayer, maskObsceneWordChar]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onLyricLineClick) {
|
||||
const handler = (e: Event) =>
|
||||
onLyricLineClick(e as LyricLineMouseEvent);
|
||||
corePlayer?.addEventListener("line-click", handler);
|
||||
return () => corePlayer?.removeEventListener("line-click", handler);
|
||||
}
|
||||
return;
|
||||
}, [corePlayer, onLyricLineClick]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onLyricLineContextMenu) {
|
||||
const handler = (e: Event) =>
|
||||
onLyricLineContextMenu(e as LyricLineMouseEvent);
|
||||
corePlayer?.addEventListener("line-contextmenu", handler);
|
||||
return () =>
|
||||
corePlayer?.removeEventListener("line-contextmenu", handler);
|
||||
}
|
||||
return;
|
||||
}, [corePlayer, onLyricLineContextMenu]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
wrapperEl: wrapperRef.current,
|
||||
lyricPlayer: corePlayer,
|
||||
}),
|
||||
[corePlayer],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...props} ref={wrapperRef} />
|
||||
{corePlayer?.getBottomLineElement() && bottomLine
|
||||
? createPortal(bottomLine, corePlayer?.getBottomLineElement())
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
4
amll-local/packages/react/tsconfig.json
Normal file
4
amll-local/packages/react/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
7
amll-local/packages/react/tsdown.config.ts
Normal file
7
amll-local/packages/react/tsdown.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
import { baseConfig } from "../../tsdown.base.ts";
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
entry: { "amll-react": "./src/index.ts" },
|
||||
});
|
||||
Reference in New Issue
Block a user