Skip to content

Commit e98a4cc

Browse files
committed
core: 修正了少许排版问题,增加了阉割较多的精简版歌词组件类型
player: 增加了阉割版歌词组件类型,补充了一些译文词条
1 parent f29ba8b commit e98a4cc

File tree

17 files changed

+1684
-45
lines changed

17 files changed

+1684
-45
lines changed

packages/core/src/lyric-player/base.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export abstract class LyricPlayerBase
2222
protected element: HTMLElement = document.createElement("div");
2323

2424
protected currentTime = 0;
25-
private lyricLinesSize: WeakMap<LyricLineBase, [number, number]> =
25+
protected lyricLinesSize: WeakMap<LyricLineBase, [number, number]> =
2626
new WeakMap();
2727
protected currentLyricLines: LyricLine[] = [];
2828
// protected currentLyricLineObjects: LyricLineBase[] = [];
@@ -31,6 +31,7 @@ export abstract class LyricPlayerBase
3131
protected hotLines: Set<number> = new Set();
3232
protected bufferedLines: Set<number> = new Set();
3333
protected isNonDynamic = false;
34+
protected hasDuetLine = false;
3435
protected scrollToIndex = 0;
3536
protected disableSpring = false;
3637
protected interludeDotsSize: [number, number] = [0, 0];
@@ -89,6 +90,7 @@ export abstract class LyricPlayerBase
8990
this.onResize();
9091
}) as ResizeObserverCallback);
9192
protected wordFadeWidth = 0.5;
93+
protected targetAlignIndex = 0;
9294

9395
constructor() {
9496
super();
@@ -405,6 +407,8 @@ export abstract class LyricPlayerBase
405407
}
406408
}
407409

410+
this.hasDuetLine = this.processedLines.some((line) => line.isDuet);
411+
408412
// 将行间有较短空隙的两个歌词行的结束时间拉长,与下一行歌词行的开始时间一致,以便于更好的显示
409413
this.processedLines.forEach((line, i, lines) => {
410414
const nextLine = lines[i + 1];
@@ -466,7 +470,7 @@ export abstract class LyricPlayerBase
466470
// this.element.style.setProperty("--amll-player-time", `${time}`);
467471
// if (this.isScrolled) return;
468472

469-
if (!this.initialLayoutFinished) return;
473+
if (!this.initialLayoutFinished && !isSeek) return;
470474

471475
const removedHotIds = new Set<number>();
472476
const removedIds = new Set<number>();
@@ -633,6 +637,7 @@ export abstract class LyricPlayerBase
633637
curPos -= scrollOffset;
634638
curPos += this.size[1] * this.alignPosition;
635639
const curLine = this.currentLyricLineObjects[targetAlignIndex];
640+
this.targetAlignIndex = targetAlignIndex;
636641
if (curLine) {
637642
const lineHeight = this.lyricLinesSize.get(curLine)?.[1] ?? 0;
638643
switch (this.alignAnchor) {
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
给各个内部元素使用的样式
3+
*/
4+
5+
/* 给每行歌词应用的样式 */
6+
.lyricLine {
7+
width: var(--amll-lp-width, 100%);
8+
min-width: var(--amll-lp-width, 100%);
9+
max-width: var(--amll-lp-width, 100%);
10+
width: 100%;
11+
height: fit-content;
12+
padding: 2vh 1em;
13+
contain: content;
14+
transition: opacity 0.25s, filter 0.2s, background-color 0.25s, box-shadow
15+
0.25s;
16+
box-sizing: border-box;
17+
border-radius: 0.25em;
18+
19+
padding-left: 1em;
20+
padding-right: 1em;
21+
22+
&:has(> *):hover {
23+
background-color: var(--amll-lp-hover-bg-color, #fff1);
24+
/* box-shadow: 0 0 0 4px var(--amll-lp-hover-bg-color, #fff1); */
25+
}
26+
27+
&:has(> *):active {
28+
background-color: var(--amll-lp-hover-bg-color, #ffffff05);
29+
/* box-shadow: 0 0 0 var(--amll-lp-hover-bg-color, #fff1); */
30+
}
31+
}
32+
33+
.lyricBgLine {
34+
opacity: 0;
35+
font-size: max(calc(1em * var(--amll-lp-bg-line-scale, 0.7)), 10px);
36+
transition: opacity 0.25s, scale 0.5s, filter 0.2s, background-color 0.25s,
37+
box-shadow 0.25s;
38+
/* 因为字体大小缩小了,故内边距要和主行字体大小统一,行边距计算公式为 100% / font-size 转 em 单位 */
39+
padding: 1vh
40+
calc(var(--amll-lp-line-padding-x, 1em) / var(--amll-lp-bg-line-scale, 0.7));
41+
42+
&.active {
43+
transition:
44+
opacity 0.5s 0.25s, scale 1.5s cubic-bezier(0, 1, 0, 1) 0.25s, filter 0.2s, background-color 0.25s, box-shadow 0.25s;
45+
opacity: 0.4;
46+
}
47+
}
48+
49+
:global(.amll-lyric-player) {
50+
&:hover .lyricLine {
51+
filter: unset !important;
52+
}
53+
54+
&.hasDuetLine {
55+
.lyricLine:not(.lyricDuetLine) {
56+
padding-right: 15%;
57+
}
58+
.lyricDuetLine {
59+
padding-left: 15%;
60+
}
61+
}
62+
}
63+
64+
.lyricDuetLine {
65+
text-align: right;
66+
transform-origin: right;
67+
}
68+
69+
.lyricMainLine {
70+
transition: opacity 0.3s 0.1s;
71+
contain: content paint;
72+
73+
span {
74+
display: inline-block;
75+
white-space: pre;
76+
}
77+
}
78+
79+
.lyricSubLine {
80+
font-size: max(0.5em, 10px);
81+
line-height: 1.5em;
82+
transition: opacity 0.2s 0.25s;
83+
opacity: 0.3;
84+
}
85+
86+
.interludeDots {
87+
height: clamp(0.5em, 1vh, 3em);
88+
transition: opacity 0.25s;
89+
transform-origin: center;
90+
width: fit-content;
91+
padding: 2.5% 0.75em;
92+
display: flex;
93+
gap: 0.25em;
94+
left: 0;
95+
opacity: 0;
96+
97+
&.enabled {
98+
opacity: 1;
99+
}
100+
101+
& > * {
102+
width: clamp(0.5em, 1vh, 3em);
103+
height: clamp(0.5em, 1vh, 3em);
104+
display: inline-block;
105+
border-radius: 50%;
106+
aspect-ratio: 1 / 1;
107+
background-color: var(--amll-lp-color, white);
108+
margin-right: 4px;
109+
}
110+
111+
&.duet {
112+
right: 0;
113+
transform-origin: center;
114+
}
115+
}
116+
117+
@supports (mix-blend-mode: plus-lighter) {
118+
.lyricSubLine {
119+
opacity: 0.3;
120+
}
121+
}
122+
123+
.tmpDisableTransition {
124+
transition: none !important;
125+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**
2+
* @fileoverview
3+
* 一个播放歌词的组件,但是进行了部分效果的阉割精简,以尝试改善在低性能设备上的性能问题
4+
* @author SteveXMH
5+
*/
6+
7+
import type { LyricLine } from "../../interfaces.ts";
8+
import "../../styles/index.css";
9+
import { debounce } from "../../utils/debounce.ts";
10+
import { LyricPlayerBase } from "../base.ts";
11+
import { LyricLineMouseEvent } from "../dom/index.ts";
12+
import styles from "./index.module.css";
13+
import { LyricLineEl, type RawLyricLineMouseEvent } from "./lyric-line.ts";
14+
15+
/**
16+
* 歌词播放组件,本框架的核心组件
17+
*
18+
* 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
19+
*/
20+
export class DomSlimLyricPlayer extends LyricPlayerBase {
21+
override currentLyricLineObjects: LyricLineEl[] = [];
22+
23+
private debounceCalcLayout = debounce(
24+
() =>
25+
this.calcLayout(true, true).then(() =>
26+
this.currentLyricLineObjects.map(async (el, i) => {
27+
el.markMaskImageDirty("DomLyricPlayer onResize");
28+
await el.waitMaskImageUpdated();
29+
if (this.hotLines.has(i)) {
30+
el.enable(this.currentTime);
31+
el.resume();
32+
}
33+
}),
34+
),
35+
1000,
36+
);
37+
38+
override onResize(): void {
39+
const computedStyles = getComputedStyle(this.element);
40+
this._baseFontSize = Number.parseFloat(computedStyles.fontSize);
41+
const innerWidth =
42+
this.element.clientWidth -
43+
Number.parseFloat(computedStyles.paddingLeft) -
44+
Number.parseFloat(computedStyles.paddingRight);
45+
const innerHeight =
46+
this.element.clientHeight -
47+
Number.parseFloat(computedStyles.paddingTop) -
48+
Number.parseFloat(computedStyles.paddingBottom);
49+
this.innerSize[0] = innerWidth;
50+
this.innerSize[1] = innerHeight;
51+
this.rebuildStyle();
52+
// for (const obj of this.currentLyricLineObjects) {
53+
// if (!obj.getElement().classList.contains(styles.dirty))
54+
// obj.getElement().classList.add(styles.dirty);
55+
// }
56+
this.debounceCalcLayout();
57+
}
58+
59+
readonly supportPlusLighter = CSS.supports("mix-blend-mode", "plus-lighter");
60+
readonly supportMaskImage = CSS.supports("mask-image", "none");
61+
readonly innerSize: [number, number] = [0, 0];
62+
private readonly onLineClickedHandler = (e: RawLyricLineMouseEvent) => {
63+
const evt = new LyricLineMouseEvent(
64+
this.lyricLinesIndexes.get(e.line) ?? -1,
65+
e.line,
66+
e,
67+
);
68+
if (!this.dispatchEvent(evt)) {
69+
e.preventDefault();
70+
e.stopPropagation();
71+
e.stopImmediatePropagation();
72+
}
73+
};
74+
/**
75+
* 是否为非逐词歌词
76+
* @internal
77+
*/
78+
_getIsNonDynamic() {
79+
return this.isNonDynamic;
80+
}
81+
private _baseFontSize = Number.parseFloat(
82+
getComputedStyle(this.element).fontSize,
83+
);
84+
public get baseFontSize() {
85+
return this._baseFontSize;
86+
}
87+
constructor() {
88+
super();
89+
this.onResize();
90+
this.element.classList.add("amll-lyric-player", "dom-slim");
91+
if (this.disableSpring) {
92+
this.element.classList.add(styles.disableSpring);
93+
}
94+
}
95+
96+
private rebuildStyle() {
97+
const width = this.innerSize[0];
98+
const height = this.innerSize[1];
99+
this.element.style.setProperty("--amll-lp-width", `${width.toFixed(4)}px`);
100+
this.element.style.setProperty(
101+
"--amll-lp-height",
102+
`${height.toFixed(4)}px`,
103+
);
104+
}
105+
106+
override setWordFadeWidth(value = 0.5) {
107+
super.setWordFadeWidth(value);
108+
for (const el of this.currentLyricLineObjects) {
109+
el.markMaskImageDirty("DomLyricPlayer setWordFadeWidth");
110+
}
111+
}
112+
113+
/**
114+
* 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
115+
* @param lines 歌词数组
116+
* @param initialTime 初始时间,默认为 0
117+
*/
118+
override setLyricLines(lines: LyricLine[], initialTime = 0) {
119+
super.setLyricLines(lines, initialTime);
120+
if (this.hasDuetLine) {
121+
this.element.classList.add(styles.hasDuetLine);
122+
} else {
123+
this.element.classList.remove(styles.hasDuetLine);
124+
}
125+
126+
for (const line of this.currentLyricLineObjects) {
127+
line.removeMouseEventListener("click", this.onLineClickedHandler);
128+
line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
129+
line.dispose();
130+
}
131+
132+
// 创建新的歌词行元素
133+
this.currentLyricLineObjects = this.processedLines.map((line, i) => {
134+
const lineEl = new LyricLineEl(this, line);
135+
lineEl.addMouseEventListener("click", this.onLineClickedHandler);
136+
lineEl.addMouseEventListener("contextmenu", this.onLineClickedHandler);
137+
this.element.appendChild(lineEl.getElement());
138+
this.lyricLinesIndexes.set(lineEl, i);
139+
lineEl.markMaskImageDirty("DomLyricPlayer setLyricLines");
140+
return lineEl;
141+
});
142+
143+
this.setLinePosXSpringParams({});
144+
this.setLinePosYSpringParams({});
145+
this.setLineScaleSpringParams({});
146+
this.calcLayout(true, true).then(() => {
147+
this.initialLayoutFinished = true;
148+
});
149+
}
150+
151+
override pause() {
152+
super.pause();
153+
this.interludeDots.pause();
154+
for (const line of this.currentLyricLineObjects) {
155+
line.pause();
156+
}
157+
}
158+
159+
override resume() {
160+
super.resume();
161+
this.interludeDots.resume();
162+
for (const line of this.currentLyricLineObjects) {
163+
line.resume();
164+
}
165+
}
166+
167+
override update(delta = 0) {
168+
if (!this.initialLayoutFinished) return;
169+
super.update(delta);
170+
if (!this.isPageVisible) return;
171+
const deltaS = delta / 1000;
172+
this.interludeDots.update(delta);
173+
this.bottomLine.update(deltaS);
174+
for (const line of this.currentLyricLineObjects) {
175+
line.update(deltaS);
176+
}
177+
}
178+
179+
override async calcLayout(force?: boolean, reflow?: boolean): Promise<void> {
180+
await super.calcLayout(force, reflow);
181+
let scrollToPos = this.currentLyricLineObjects
182+
.slice(0, this.targetAlignIndex)
183+
.reduce(
184+
(acc, el) =>
185+
acc +
186+
(el.getLine().isBG ? 0 : (this.lyricLinesSize.get(el)?.[1] ?? 0)),
187+
0,
188+
);
189+
scrollToPos -= this.size[1] * this.alignPosition;
190+
const curLine = this.currentLyricLineObjects[this.targetAlignIndex];
191+
if (curLine) {
192+
const lineHeight = this.lyricLinesSize.get(curLine)?.[1] ?? 0;
193+
switch (this.alignAnchor) {
194+
case "bottom":
195+
scrollToPos += lineHeight;
196+
break;
197+
case "center":
198+
scrollToPos += lineHeight / 2;
199+
break;
200+
case "top":
201+
break;
202+
}
203+
}
204+
this.element.scrollTo({
205+
top: scrollToPos,
206+
behavior: "smooth",
207+
});
208+
}
209+
210+
override dispose(): void {
211+
super.dispose();
212+
this.element.remove();
213+
for (const el of this.currentLyricLineObjects) {
214+
el.dispose();
215+
}
216+
this.bottomLine.dispose();
217+
this.interludeDots.dispose();
218+
}
219+
}

0 commit comments

Comments
 (0)