v2.0.0
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
const { getNow, TimeoutTools } = require('./utils')
|
||||
|
||||
// const fontFormateRxp = /(?=<\d+,\d+>).*?/g
|
||||
const fontSplitRxp = /(?=<\d+,\d+>).*?/g
|
||||
const timeRxpAll = /<(\d+),(\d+)>/g
|
||||
const timeRxp = /<(\d+),(\d+)>/
|
||||
|
||||
|
||||
// Create animation
|
||||
const createAnimation = (dom, duration, isVertical) => new window.Animation(new window.KeyframeEffect(dom, isVertical
|
||||
? [
|
||||
{ backgroundSize: '100% 0' },
|
||||
{ backgroundSize: '100% 100%' },
|
||||
]
|
||||
: [
|
||||
{ backgroundSize: '0 100%' },
|
||||
{ backgroundSize: '100% 100%' },
|
||||
], {
|
||||
duration,
|
||||
easing: 'linear',
|
||||
},
|
||||
), document.timeline)
|
||||
|
||||
|
||||
// https://jsfiddle.net/ceqpnbky/
|
||||
// https://jsfiddle.net/ceqpnbky/1/
|
||||
|
||||
module.exports = class FontPlayer {
|
||||
constructor({
|
||||
time = 0,
|
||||
lyric = '',
|
||||
lineContentClassName = 'line-content',
|
||||
lineClassName = 'line',
|
||||
shadowClassName = 'shadow',
|
||||
fontModeClassName = 'font-mode',
|
||||
lineModeClassName = 'line-mode',
|
||||
fontLrcClassName = 'font-lrc',
|
||||
extendedLrcClassName = 'extended',
|
||||
shadowContent = false,
|
||||
extendedLyrics = [],
|
||||
isVertical = false,
|
||||
}) {
|
||||
this.time = time
|
||||
this.lyric = lyric
|
||||
|
||||
this.isVertical = isVertical
|
||||
|
||||
this.lineContentClassName = lineContentClassName
|
||||
this.lineClassName = lineClassName
|
||||
|
||||
this.shadowContent = shadowContent
|
||||
this.shadowClassName = shadowClassName
|
||||
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this.fontModeClassName = fontModeClassName
|
||||
this.fontLrcClassName = fontLrcClassName
|
||||
this.extendedLrcClassName = extendedLrcClassName
|
||||
this.lineModeClassName = lineModeClassName
|
||||
|
||||
|
||||
this.isPlay = false
|
||||
this.curFontNum = 0
|
||||
this.maxFontNum = 0
|
||||
this._performanceTime = 0
|
||||
this._startTime = 0
|
||||
|
||||
this.lineContent = null
|
||||
|
||||
this.timeoutTools = new TimeoutTools(80)
|
||||
this.waitPlayTimeout = new TimeoutTools(80)
|
||||
|
||||
this._init()
|
||||
}
|
||||
|
||||
_init() {
|
||||
if (this.lyric == null) this.lyric = ''
|
||||
|
||||
this.isLineMode = false
|
||||
|
||||
this.lineContent = document.createElement('div')
|
||||
this.lineContent.time = this.time
|
||||
this.lineContent.className = this.lineContentClassName
|
||||
|
||||
this.line = document.createElement('div')
|
||||
this.line.style = 'position:relative;display:inline-block;'
|
||||
this.line.className = this.lineClassName
|
||||
this.lineContent.appendChild(this.line)
|
||||
|
||||
this.lrcContent = document.createElement('div')
|
||||
this.lrcContent.className = this.fontLrcClassName
|
||||
// if (this.shadowContent) {
|
||||
// this.lrcShadowContent = document.createElement('div')
|
||||
// this.lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
|
||||
// this.lrcShadowContent.className = this.shadowClassName
|
||||
// this.line.appendChild(this.lrcShadowContent)
|
||||
// }
|
||||
this.line.appendChild(this.lrcContent)
|
||||
|
||||
for (const lrc of this.extendedLyrics) {
|
||||
const extendedLrcContent = document.createElement('div')
|
||||
extendedLrcContent.style = 'position:relative;display:inline-block;'
|
||||
extendedLrcContent.className = this.extendedLrcClassName
|
||||
this.lineContent.appendChild(document.createElement('br'))
|
||||
this.lineContent.appendChild(extendedLrcContent)
|
||||
|
||||
|
||||
// if (this.shadowContent) {
|
||||
// const extendedLrcShadowContent = document.createElement('div')
|
||||
// extendedLrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
|
||||
// extendedLrcShadowContent.className = this.shadowClassName
|
||||
// extendedLrcShadowContent.textContent = lrc
|
||||
// extendedLrcContent.appendChild(extendedLrcShadowContent)
|
||||
// }
|
||||
|
||||
const lineContent = document.createElement('div')
|
||||
lineContent.className = this.fontLrcClassName
|
||||
lineContent.textContent = lrc.replace(timeRxpAll, '')
|
||||
extendedLrcContent.appendChild(lineContent)
|
||||
}
|
||||
this._parseLyric()
|
||||
}
|
||||
|
||||
_parseLyric() {
|
||||
const fonts = this.lyric.split(fontSplitRxp)
|
||||
// console.log(fonts)
|
||||
|
||||
this.maxFontNum = fonts.length - 1
|
||||
this.fonts = []
|
||||
let text
|
||||
// let lineText = ''
|
||||
let lrcShadowContent
|
||||
for (const font of fonts) {
|
||||
text = font.replace(timeRxp, '')
|
||||
if (RegExp.$2 == '') return this._handleLineParse()
|
||||
const time = parseInt(RegExp.$2)
|
||||
|
||||
const dom = document.createElement('span')
|
||||
dom.textContent = text
|
||||
const animation = createAnimation(dom, time, this.isVertical)
|
||||
this.lrcContent.appendChild(dom)
|
||||
// lineText += text
|
||||
|
||||
if (this.shadowContent) {
|
||||
if (!lrcShadowContent) lrcShadowContent = document.createElement('div')
|
||||
const shadowDom = document.createElement('span')
|
||||
shadowDom.textContent = text
|
||||
lrcShadowContent.appendChild(shadowDom)
|
||||
}
|
||||
// dom.style = shadowDom.style = this.fontStyle
|
||||
// dom.className = shadowDom.className = this.fontClassName
|
||||
|
||||
this.fonts.push({
|
||||
text,
|
||||
startTime: parseInt(RegExp.$1),
|
||||
time,
|
||||
dom,
|
||||
animation,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.shadowContent && lrcShadowContent) {
|
||||
lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
|
||||
lrcShadowContent.className = this.shadowClassName
|
||||
this.line.appendChild(lrcShadowContent)
|
||||
}
|
||||
|
||||
this.line.appendChild(this.lrcContent)
|
||||
this.fonts.at(-1)?.animation.addEventListener('finish', () => {
|
||||
this.lineContent.classList.add('played')
|
||||
this.isPlay = false
|
||||
})
|
||||
this.lineContent.classList.add(this.fontModeClassName)
|
||||
// if (this.shadowContent) this.lrcShadowContent.textContent = lineText
|
||||
// console.log(this.fonts)
|
||||
}
|
||||
|
||||
_handleLineParse() {
|
||||
this.isLineMode = true
|
||||
this.lineContent.classList.add(this.lineModeClassName)
|
||||
this.lrcContent.textContent = this.lyric
|
||||
|
||||
// if (this.shadowContent) this.lrcShadowContent.textContent = this.lyric
|
||||
this.fonts.push({
|
||||
text: this.lyric,
|
||||
})
|
||||
}
|
||||
|
||||
_currentTime() {
|
||||
return getNow() - this._performanceTime + this._startTime
|
||||
}
|
||||
|
||||
_findcurFontNum(curTime, startIndex = 0) {
|
||||
const length = this.fonts.length
|
||||
for (let index = startIndex; index < length; index++) if (curTime < this.fonts[index].startTime) return index == 0 ? 0 : index - 1
|
||||
return length - 1
|
||||
}
|
||||
|
||||
_handlePlayMaxFontNum() {
|
||||
let curFont = this.fonts[this.curFontNum]
|
||||
// console.log(curFont.text)
|
||||
const currentTime = this._currentTime()
|
||||
const driftTime = currentTime - curFont.startTime
|
||||
if (currentTime > curFont.startTime + curFont.time) {
|
||||
this._handlePlayFont(curFont, driftTime, true)
|
||||
this.lineContent.classList.add('played')
|
||||
this.isPlay = false
|
||||
this.pause()
|
||||
} else {
|
||||
this._handlePlayFont(curFont, driftTime)
|
||||
}
|
||||
}
|
||||
|
||||
_handlePlayFont(font, currentTime, toFinishe) {
|
||||
switch (font.animation.playState) {
|
||||
case 'finished':
|
||||
break
|
||||
case 'idle':
|
||||
font.dom.style.backgroundSize = '100% 100%'
|
||||
if (!toFinishe) font.animation.play()
|
||||
break
|
||||
default:
|
||||
if (toFinishe) {
|
||||
font.animation.cancel()
|
||||
} else {
|
||||
font.animation.currentTime = currentTime
|
||||
font.animation.play()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_handlePlayLine(isPlayed) {
|
||||
this.isPlay = false
|
||||
if (isPlayed) {
|
||||
this.lineContent.classList.add('played')
|
||||
} else {
|
||||
this.lineContent.classList.remove('played')
|
||||
}
|
||||
// this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
|
||||
}
|
||||
|
||||
_handlePauseFont(font) {
|
||||
if (font.animation.playState == 'running') font.animation.pause()
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.curFontNum++
|
||||
// console.log('curFontNum time', this.fonts[this.curFontNum].time)
|
||||
if (this.curFontNum >= this.maxFontNum) return this._handlePlayMaxFontNum()
|
||||
let curFont = this.fonts[this.curFontNum]
|
||||
// console.log(curFont, nextFont, this.curFontNum, this.maxFontNum)
|
||||
const currentTime = this._currentTime()
|
||||
// console.log(curFont.text)
|
||||
const driftTime = currentTime - curFont.startTime
|
||||
|
||||
// console.log(currentTime, driftTime)
|
||||
|
||||
if (driftTime >= 0 || this.curFontNum == 0) {
|
||||
let nextFont = this.fonts[this.curFontNum + 1]
|
||||
this.delay = nextFont.startTime - curFont.startTime - driftTime
|
||||
if (this.delay > 0) {
|
||||
if (this.isPlay) {
|
||||
this.timeoutTools.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, this.delay)
|
||||
}
|
||||
this._handlePlayFont(curFont, driftTime)
|
||||
return
|
||||
} else {
|
||||
let newCurLineNum = this._findcurFontNum(currentTime, this.curFontNum + 1)
|
||||
if (newCurLineNum > this.curFontNum) this.curFontNum = newCurLineNum - 1
|
||||
for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
this._refresh()
|
||||
return
|
||||
}
|
||||
} else if (this.curFontNum == 0) {
|
||||
this.curFontNum--
|
||||
if (this.isPlay) {
|
||||
this.waitPlayTimeout.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, -driftTime)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.curFontNum = this._findcurFontNum(currentTime, this.curFontNum) - 1
|
||||
for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
// this.curFontNum--
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
play(curTime = 0) {
|
||||
// console.log('play', curTime)
|
||||
if (!this.fonts.length) return
|
||||
this.pause()
|
||||
|
||||
if (this.isLineMode) return this._handlePlayLine(true)
|
||||
this.lineContent.classList.remove('played')
|
||||
this.isPlay = true
|
||||
this._performanceTime = getNow()
|
||||
this._startTime = curTime
|
||||
|
||||
this.curFontNum = this._findcurFontNum(curTime)
|
||||
|
||||
for (let i = this.curFontNum; i > -1; i--) {
|
||||
this._handlePlayFont(this.fonts[i], 0, true)
|
||||
}
|
||||
for (let i = this.curFontNum, len = this.fonts.length; i < len; i++) {
|
||||
let font = this.fonts[i]
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '0 100%'
|
||||
}
|
||||
|
||||
this.curFontNum--
|
||||
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.isPlay) return
|
||||
this.isPlay = false
|
||||
this.timeoutTools.clear()
|
||||
this.waitPlayTimeout.clear()
|
||||
this._handlePauseFont(this.fonts[this.curFontNum])
|
||||
if (this.curFontNum === this.maxLine) return
|
||||
const curFontNum = this._findcurFontNum(this._currentTime())
|
||||
if (this.curFontNum === curFontNum) return
|
||||
for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.pause()
|
||||
if (this.isLineMode) return this._handlePlayLine(true)
|
||||
this.lineContent.classList.add('played')
|
||||
|
||||
for (const font of this.fonts) {
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '100% 100%'
|
||||
}
|
||||
this.curFontNum = this.maxFontNum
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.pause()
|
||||
if (this.isLineMode) return this._handlePlayLine(false)
|
||||
this.lineContent.classList.remove('played')
|
||||
for (const font of this.fonts) {
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '0 100%'
|
||||
}
|
||||
this.curFontNum = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
const LinePlayer = require('./line-player')
|
||||
const FontPlayer = require('./font-player')
|
||||
|
||||
const fontTimeExp = /<(\d+),(\d+)>/g
|
||||
|
||||
module.exports = class Lyric {
|
||||
constructor({
|
||||
lyric = '',
|
||||
extendedLyrics = [],
|
||||
offset = 0,
|
||||
lineContentClassName = 'line-content',
|
||||
lineClassName = 'line',
|
||||
shadowClassName = 'shadow',
|
||||
fontModeClassName = 'font-mode',
|
||||
lineModeClassName = 'line-mode',
|
||||
fontLrcClassName = 'font-lrc',
|
||||
extendedLrcClassName = 'extended',
|
||||
activeLineClassName = 'active',
|
||||
shadowContent = false,
|
||||
isVertical = false,
|
||||
onPlay = function(line, text) { },
|
||||
onSetLyric = function(lines, offset) { },
|
||||
onUpdateLyric = function(lines) { },
|
||||
}) {
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this.offset = offset
|
||||
this.onPlay = onPlay
|
||||
this.onSetLyric = onSetLyric
|
||||
this.onUpdateLyric = onUpdateLyric
|
||||
|
||||
this.lineContentClassName = lineContentClassName
|
||||
this.lineClassName = lineClassName
|
||||
this.shadowClassName = shadowClassName
|
||||
this.fontModeClassName = fontModeClassName
|
||||
this.lineModeClassName = lineModeClassName
|
||||
this.fontLrcClassName = fontLrcClassName
|
||||
this.extendedLrcClassName = extendedLrcClassName
|
||||
this.activeLineClassName = activeLineClassName
|
||||
this.shadowContent = shadowContent
|
||||
|
||||
this.isVertical = isVertical
|
||||
this.playingLineNum = -1
|
||||
this.isLineMode = false
|
||||
|
||||
this.initInfo = {
|
||||
lines: [],
|
||||
offset: 0,
|
||||
}
|
||||
|
||||
this.linePlayer = new LinePlayer({
|
||||
offset: this.offset,
|
||||
onPlay: this._handleLinePlayerOnPlay,
|
||||
onSetLyric: this._handleLinePlayerOnSetLyric,
|
||||
})
|
||||
}
|
||||
|
||||
_init() {
|
||||
this.playingLineNum = -1
|
||||
this.isLineMode = false
|
||||
|
||||
this.linePlayer.setLyric(this.lyric, this.extendedLyrics)
|
||||
}
|
||||
|
||||
_handleLinePlayerOnPlay = (num, text, curTime) => {
|
||||
if (this.isLineMode) {
|
||||
if (num < this.playingLineNum + 1) {
|
||||
for (let i = this.playingLineNum; i > num - 1; i--) {
|
||||
const font = this._lineFonts[i]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else if (num > this.playingLineNum) {
|
||||
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
|
||||
const font = this._lineFonts[i]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else if (this.playingLineNum > -1) {
|
||||
const font = this._lineFonts[this.playingLineNum]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else {
|
||||
if (num < this.playingLineNum + 1) {
|
||||
for (let i = this.playingLineNum; i > num - 1; i--) {
|
||||
const font = this._lineFonts[i]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
font.reset()
|
||||
}
|
||||
} else if (num > this.playingLineNum) {
|
||||
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
|
||||
const font = this._lineFonts[i]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
font.finish()
|
||||
}
|
||||
} else if (this.playingLineNum > -1) {
|
||||
const font = this._lineFonts[this.playingLineNum]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
}
|
||||
this.playingLineNum = num
|
||||
const font = this._lineFonts[num]
|
||||
font.lineContent.classList.add(this.activeLineClassName)
|
||||
font.play(curTime - this._lines[num].time)
|
||||
this.onPlay(num, this._lines[num].text)
|
||||
}
|
||||
|
||||
_initLines = (lyricLines, offset, isUpdate) => {
|
||||
// console.log(lyricLines)
|
||||
// this._lines = lyricsLines
|
||||
this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text)
|
||||
|
||||
this._lineFonts = []
|
||||
if (this.isLineMode) {
|
||||
this._lines = lyricLines.map(line => {
|
||||
const fontPlayer = new FontPlayer({
|
||||
time: line.time,
|
||||
lyric: line.text,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
lineContentClassName: this.lineContentClassName,
|
||||
lineClassName: this.lineClassName,
|
||||
shadowClassName: this.shadowClassName,
|
||||
fontModeClassName: this.fontModeClassName,
|
||||
lineModeClassName: this.lineModeClassName,
|
||||
fontLrcClassName: this.fontLrcClassName,
|
||||
extendedLrcClassName: this.extendedLrcClassName,
|
||||
shadowContent: this.shadowContent,
|
||||
isVertical: this.isVertical,
|
||||
})
|
||||
|
||||
this._lineFonts.push(fontPlayer)
|
||||
return {
|
||||
text: line.text,
|
||||
time: line.time,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
dom_line: fontPlayer.lineContent,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this._lines = lyricLines.map(line => {
|
||||
const fontPlayer = new FontPlayer({
|
||||
time: line.time,
|
||||
lyric: line.text,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
lineContentClassName: this.lineContentClassName,
|
||||
lineClassName: this.lineClassName,
|
||||
shadowClassName: this.shadowClassName,
|
||||
fontModeClassName: this.fontModeClassName,
|
||||
lineModeClassName: this.lineModeClassName,
|
||||
fontLrcClassName: this.fontLrcClassName,
|
||||
extendedLrcClassName: this.extendedLrcClassName,
|
||||
shadowContent: this.shadowContent,
|
||||
isVertical: this.isVertical,
|
||||
})
|
||||
|
||||
this._lineFonts.push(fontPlayer)
|
||||
return {
|
||||
text: line.text.replace(fontTimeExp, ''),
|
||||
time: line.time,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
dom_line: fontPlayer.lineContent,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果是逐行歌词,则添加 60ms 的偏移
|
||||
let newOffset = this.isLineMode ? this.offset + 60 : this.offset
|
||||
offset = offset - this.linePlayer.offset + newOffset
|
||||
this.linePlayer.offset = newOffset
|
||||
if (isUpdate) this.onUpdateLyric(this._lines)
|
||||
else this.onSetLyric(this._lines, offset)
|
||||
}
|
||||
|
||||
_handleLinePlayerOnSetLyric = (lyricLines, offset) => {
|
||||
this._initLines(lyricLines, offset, false)
|
||||
this.playingLineNum = 0
|
||||
this.initInfo.lines = lyricLines
|
||||
this.initInfo.offset = offset
|
||||
}
|
||||
|
||||
play(curTime) {
|
||||
if (!this.linePlayer) return
|
||||
this.linePlayer.play(curTime)
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.linePlayer) return
|
||||
this.linePlayer.pause()
|
||||
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum]?.pause()
|
||||
}
|
||||
|
||||
setOffset(offset) {
|
||||
this.linePlayer.offset = offset
|
||||
}
|
||||
|
||||
setLyric(lyric, extendedLyrics) {
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this._init()
|
||||
}
|
||||
|
||||
setVertical(isVertical) {
|
||||
this.isVertical = isVertical
|
||||
this._initLines(this.initInfo.lines, this.initInfo.offset, true)
|
||||
if (this.linePlayer.isPlay) {
|
||||
const num = this.playingLineNum
|
||||
this.playingLineNum = 0
|
||||
this._handleLinePlayerOnPlay(num, '', this.linePlayer._currentTime())
|
||||
} else this.playingLineNum = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
const { getNow, TimeoutTools } = require('./utils')
|
||||
|
||||
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
|
||||
const timeExp = /[\d:.]+/g
|
||||
const tagRegMap = {
|
||||
title: 'ti',
|
||||
artist: 'ar',
|
||||
album: 'al',
|
||||
offset: 'offset',
|
||||
by: 'by',
|
||||
}
|
||||
|
||||
const timeoutTools = new TimeoutTools()
|
||||
|
||||
const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
|
||||
const extendedLines = extendedLyric.split(/\r\n|\n|\r/)
|
||||
for (let i = 0; i < extendedLines.length; i++) {
|
||||
const line = extendedLines[i].trim()
|
||||
let result = timeFieldExp.exec(line)
|
||||
if (result) {
|
||||
const timeField = result[0]
|
||||
const text = line.replace(timeFieldExp, '').trim()
|
||||
if (text) {
|
||||
const times = timeField.match(timeExp)
|
||||
if (times == null) continue
|
||||
for (const time of times) {
|
||||
const timeStr = time.replace(/(\.\d\d)0$/, '$1')
|
||||
const targetLine = lrcLinesMap[timeStr]
|
||||
if (targetLine) targetLine.extendedLyrics.push(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = class LinePlayer {
|
||||
constructor({ offset = 0, onPlay = function() { }, onSetLyric = function() { } } = {}) {
|
||||
this.tags = {}
|
||||
this.lines = null
|
||||
this.onPlay = onPlay
|
||||
this.onSetLyric = onSetLyric
|
||||
this.isPlay = false
|
||||
this.curLineNum = 0
|
||||
this.maxLine = 0
|
||||
this.offset = offset
|
||||
this._performanceTime = 0
|
||||
this._startTime = 0
|
||||
}
|
||||
|
||||
_init() {
|
||||
if (this.lyric == null) this.lyric = ''
|
||||
if (this.extendedLyrics == null) this.extendedLyrics = []
|
||||
this._initTag()
|
||||
this._initLines()
|
||||
this.onSetLyric(this.lines, this.tags.offset + this.offset)
|
||||
}
|
||||
|
||||
_initTag() {
|
||||
this.tags = {}
|
||||
for (let tag in tagRegMap) {
|
||||
const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
|
||||
this.tags[tag] = (matches && matches[1]) || ''
|
||||
}
|
||||
if (this.tags.offset) {
|
||||
let offset = parseInt(this.tags.offset)
|
||||
this.tags.offset = Number.isNaN(offset) ? 0 : offset
|
||||
} else {
|
||||
this.tags.offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
_initLines() {
|
||||
this.lines = []
|
||||
const lines = this.lyric.split(/\r\n|\r|\n/)
|
||||
const linesMap = {}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
let result = timeFieldExp.exec(line)
|
||||
if (result) {
|
||||
const timeField = result[0]
|
||||
const text = line.replace(timeFieldExp, '').trim()
|
||||
if (text) {
|
||||
const times = timeField.match(timeExp)
|
||||
if (times == null) continue
|
||||
for (const time of times) {
|
||||
const timeStr = time.replace(/(\.\d\d)0$/, '$1')
|
||||
if (linesMap[timeStr]) {
|
||||
linesMap[timeStr].extendedLyrics.push(text)
|
||||
continue
|
||||
}
|
||||
const timeArr = timeStr.split(':')
|
||||
if (timeArr.length < 3) timeArr.unshift(0)
|
||||
if (timeArr[2].indexOf('.') > -1) {
|
||||
timeArr.push(...timeArr[2].split('.'))
|
||||
timeArr.splice(2, 1)
|
||||
}
|
||||
linesMap[timeStr] = {
|
||||
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
|
||||
text,
|
||||
extendedLyrics: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const lrc of this.extendedLyrics) parseExtendedLyric(linesMap, lrc)
|
||||
this.lines = Object.values(linesMap)
|
||||
this.lines.sort((a, b) => {
|
||||
return a.time - b.time
|
||||
})
|
||||
this.maxLine = this.lines.length - 1
|
||||
}
|
||||
|
||||
_currentTime() {
|
||||
return getNow() - this._performanceTime + this._startTime
|
||||
}
|
||||
|
||||
_findCurLineNum(curTime, startIndex = 0) {
|
||||
if (curTime <= 0) return 0
|
||||
const length = this.lines.length
|
||||
for (let index = startIndex; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1
|
||||
return length - 1
|
||||
}
|
||||
|
||||
_handleMaxLine() {
|
||||
this.onPlay(this.curLineNum, this.lines[this.curLineNum].text, this._currentTime())
|
||||
this.pause()
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.curLineNum++
|
||||
// console.log('curLineNum time', this.lines[this.curLineNum].time)
|
||||
if (this.curLineNum >= this.maxLine) return this._handleMaxLine()
|
||||
|
||||
let curLine = this.lines[this.curLineNum]
|
||||
|
||||
const currentTime = this._currentTime()
|
||||
const driftTime = currentTime - curLine.time
|
||||
|
||||
if (driftTime >= 0 || this.curLineNum === 0) {
|
||||
let nextLine = this.lines[this.curLineNum + 1]
|
||||
this.delay = nextLine.time - curLine.time - driftTime
|
||||
|
||||
if (this.delay > 0) {
|
||||
if (this.isPlay) {
|
||||
timeoutTools.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, this.delay)
|
||||
}
|
||||
this.onPlay(this.curLineNum, curLine.text, currentTime)
|
||||
return
|
||||
} else {
|
||||
let newCurLineNum = this._findCurLineNum(currentTime, this.curLineNum + 1)
|
||||
if (newCurLineNum > this.curLineNum) this.curLineNum = newCurLineNum - 1
|
||||
this._refresh()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.curLineNum = this._findCurLineNum(currentTime, this.curLineNum) - 1
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
play(curTime = 0) {
|
||||
if (!this.lines.length) return
|
||||
this.pause()
|
||||
this.isPlay = true
|
||||
|
||||
this._performanceTime = getNow() - parseInt(this.tags.offset + this.offset)
|
||||
this._startTime = curTime
|
||||
|
||||
this.curLineNum = this._findCurLineNum(this._currentTime()) - 1
|
||||
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.isPlay) return
|
||||
this.isPlay = false
|
||||
timeoutTools.clear()
|
||||
if (this.curLineNum === this.maxLine) return
|
||||
const currentTime = this._currentTime()
|
||||
const curLineNum = this._findCurLineNum(currentTime)
|
||||
if (this.curLineNum !== curLineNum) {
|
||||
this.curLineNum = curLineNum
|
||||
this.onPlay(curLineNum, this.lines[curLineNum].text, currentTime)
|
||||
}
|
||||
}
|
||||
|
||||
setLyric(lyric, extendedLyrics) {
|
||||
// console.log(extendedLyrics)
|
||||
if (this.isPlay) this.pause()
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this._init()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
const getNow = exports.getNow = typeof performance == 'object' && window.performance.now ? window.performance.now.bind(window.performance) : Date.now.bind(Date)
|
||||
|
||||
exports.TimeoutTools = class TimeoutTools {
|
||||
constructor(thresholdTime = 200) {
|
||||
this.invokeTime = 0
|
||||
this.animationFrameId = null
|
||||
this.timeoutId = null
|
||||
this.callback = null
|
||||
this.thresholdTime = thresholdTime
|
||||
}
|
||||
|
||||
run() {
|
||||
this.animationFrameId = window.requestAnimationFrame(() => {
|
||||
this.animationFrameId = null
|
||||
let diff = this.invokeTime - getNow()
|
||||
// console.log('diff', diff)
|
||||
if (diff > 0) {
|
||||
if (diff < this.thresholdTime) return this.run()
|
||||
return this.timeoutId = window.setTimeout(() => {
|
||||
this.timeoutId = null
|
||||
this.run()
|
||||
}, diff - this.thresholdTime)
|
||||
}
|
||||
|
||||
// console.log('diff', diff)
|
||||
this.callback(diff)
|
||||
})
|
||||
}
|
||||
|
||||
start(callback = () => {}, timeout = 0) {
|
||||
// console.log(timeout)
|
||||
this.callback = callback
|
||||
this.invokeTime = getNow() + timeout
|
||||
|
||||
this.run()
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.animationFrameId) {
|
||||
window.cancelAnimationFrame(this.animationFrameId)
|
||||
this.animationFrameId = null
|
||||
}
|
||||
if (this.timeoutId) {
|
||||
window.clearTimeout(this.timeoutId)
|
||||
this.timeoutId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user