初步完善歌词

This commit is contained in:
lyswhut
2021-03-02 08:32:26 +08:00
parent 9f72831e4a
commit 040fcfba4f
12 changed files with 779 additions and 120 deletions
@@ -0,0 +1,267 @@
const { getNow, TimeoutTools } = require('./utils')
// const fontFormateRxp = /(?=<\d+,\d+>).*?/g
const fontSplitRxp = /(?=<\d+,\d+>).*?/g
const timeRxp = /<(\d+),(\d+)>/
// Create animation
const createAnimation = (dom, duration) => new window.Animation(new window.KeyframeEffect(dom, [
{ 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({ lyric = '', className = '', shadowContent = false, shadowClassName = '' }) {
this.lyric = lyric
this.className = className
this.shadowContent = shadowContent
this.shadowClassName = shadowClassName
this.isPlay = false
this.curFontNum = 0
this.maxFontNum = 0
this._performanceTime = 0
this._performanceOffsetTime = 0
this.fontContent = null
this.timeoutTools = new TimeoutTools()
this.waitPlayTimeout = new TimeoutTools()
this._init()
}
_init() {
if (this.lyric == null) this.lyric = ''
this.isLineMode = false
this.fontContent = document.createElement('div')
this.fontContent.style = 'position:relative;'
this.fontContent.className = this.className
if (this.shadowContent) {
this.fontShadowContent = document.createElement('div')
this.fontShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
this.fontShadowContent.className = this.shadowClassName
this.fontContent.appendChild(this.fontShadowContent)
}
this._parseLyric()
}
_parseLyric() {
const fonts = this.lyric.split(fontSplitRxp)
// console.log(fonts)
this.maxFontNum = fonts.length - 1
this.fonts = []
let text
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')
let shadowDom
dom.textContent = text
const animation = createAnimation(dom, time)
this.fontContent.appendChild(dom)
if (this.shadowContent) {
shadowDom = document.createElement('span')
shadowDom.textContent = text
this.fontShadowContent.appendChild(shadowDom)
}
// dom.style = shadowDom.style = this.fontStyle
// dom.className = shadowDom.className = this.fontClassName
this.fonts.push({
text,
startTime: parseInt(RegExp.$1),
time,
dom,
shadowDom,
animation,
})
}
// console.log(this.fonts)
}
_handleLineParse() {
this.isLineMode = true
const dom = document.createElement('span')
let shadowDom
dom.textContent = this.lyric
if (this.shadowContent) {
shadowDom = document.createElement('span')
shadowDom.textContent = this.lyric
this.fontShadowContent.appendChild(shadowDom)
}
this.fontContent.appendChild(dom)
this.fonts.push({
text: this.lyric,
dom,
shadowDom,
})
}
_currentTime() {
return getNow() - this._performanceTime + this._performanceOffsetTime
}
_findcurFontNum(curTime) {
const length = this.fonts.length
for (let index = 0; 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.pause()
} else {
this._handlePlayFont(curFont, driftTime)
this.isPlay = false
}
}
_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
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]
let nextFont = this.fonts[this.curFontNum + 1]
// 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) {
this.delay = nextFont.startTime - curFont.startTime - driftTime
if (this.delay > 0) {
this._handlePlayFont(curFont, driftTime)
this.timeoutTools.start(() => {
this._refresh()
}, this.delay)
return
}
} else if (this.curFontNum == 0) {
this.curFontNum--
this.waitPlayTimeout.start(() => {
this._refresh()
}, -driftTime)
return
}
this.curFontNum = this._findcurFontNum(currentTime)
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.isPlay = true
this._performanceTime = getNow() - curTime
this._performanceOffsetTime = 0
if (this._performanceTime < 0) {
this._performanceOffsetTime = -this._performanceTime
this._performanceTime = 0
}
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)
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)
for (const font of this.fonts) {
font.animation.cancel()
font.dom.style.backgroundSize = '0 100%'
}
this.curFontNum = 0
}
}
@@ -0,0 +1,148 @@
const LinePlayer = require('./line-player')
const FontPlayer = require('./font-player')
const fontTimeExp = /<(\d+),(\d+)>/g
module.exports = class Lyric {
constructor({
lyric = '',
offset = 150,
className = '',
activeLineClassName = 'active',
shadowClassName = '',
shadowContent = false,
onPlay = function() { },
onSetLyric = function() { },
}) {
this.lyric = lyric
this.offset = offset
this.onPlay = onPlay
this.onSetLyric = onSetLyric
this.className = className
this.activeLineClassName = activeLineClassName
this.shadowClassName = shadowClassName
this.shadowContent = shadowContent
this.playingLineNum = -1
this.isLineMode = false
}
_init() {
this.playingLineNum = -1
this.isLineMode = false
if (this.linePlayer) {
this.linePlayer.setLyric(this.lyric)
} else {
this.linePlayer = new LinePlayer({
lyric: this.lyric,
offset: this.offset,
onPlay: this._handleLinePlayerOnPlay,
onSetLyric: this._handleLinePlayerOnSetLyric,
})
}
}
_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.fontContent.classList.remove(this.activeLineClassName)
}
} else if (num > this.playingLineNum + 1) {
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
const font = this._lineFonts[i]
font.reset()
font.fontContent.classList.remove(this.activeLineClassName)
}
} else if (this.playingLineNum > -1) {
const font = this._lineFonts[this.playingLineNum]
font.reset()
font.fontContent.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.fontContent.classList.remove(this.activeLineClassName)
font.reset()
}
} else if (num > this.playingLineNum + 1) {
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
const font = this._lineFonts[i]
font.fontContent.classList.remove(this.activeLineClassName)
font.finish()
}
} else if (this.playingLineNum > -1) {
const font = this._lineFonts[this.playingLineNum]
font.fontContent.classList.remove(this.activeLineClassName)
}
}
this.playingLineNum = num
const font = this._lineFonts[num]
font.fontContent.classList.add(this.activeLineClassName)
font.play(curTime - this._lines[num].time)
this.onPlay(num, this._lines[num].text)
}
_handleLinePlayerOnSetLyric = 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({
lyric: line.text,
className: this.className,
shadowClassName: this.shadowClassName,
shadowContent: this.shadowContent,
})
this._lineFonts.push(fontPlayer)
return {
text: line.text,
time: line.time,
dom_line: fontPlayer.fontContent,
}
})
} else {
this._lines = lyricLines.map(line => {
const fontPlayer = new FontPlayer({
lyric: line.text,
className: this.className,
shadowClassName: this.shadowClassName,
shadowContent: this.shadowContent,
})
this._lineFonts.push(fontPlayer)
return {
text: line.text.replace(fontTimeExp, ''),
time: line.time,
dom_line: fontPlayer.fontContent,
}
})
}
this.onSetLyric(this._lines)
}
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()
}
setLyric(lyric) {
this.lyric = lyric
this._init()
}
}
@@ -0,0 +1,153 @@
const { getNow, TimeoutTools } = require('./utils')
const timeExp = /^\[([\d:.]*)\]{1}/g
const tagRegMap = {
title: 'ti',
artist: 'ar',
album: 'al',
offset: 'offset',
by: 'by',
}
const timeoutTools = new TimeoutTools()
module.exports = class LinePlayer {
constructor({ lyric = '', offset = 0, onPlay = function() { }, onSetLyric = function() { } } = {}) {
this.lyric = lyric
this.tags = {}
this.lines = null
this.onPlay = onPlay
this.onSetLyric = onSetLyric
this.isPlay = false
this.curLineNum = 0
this.maxLine = 0
this.offset = offset
this.isOffseted = false
this._performanceTime = 0
this._performanceOffsetTime = 0
this._init()
}
_init() {
if (this.lyric == null) this.lyric = ''
this._initTag()
this._initLines()
this.onSetLyric(this.lines)
}
_initTag() {
for (let tag in tagRegMap) {
const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
this.tags[tag] = (matches && matches[1]) || ''
}
}
_initLines() {
this.lines = []
const lines = this.lyric.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
let result = timeExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
if (text) {
const timeArr = RegExp.$1.split(':')
if (timeArr.length < 3) timeArr.unshift(0)
if (timeArr[2].indexOf('.') > -1) {
timeArr.push(...timeArr[2].split('.'))
timeArr.splice(2, 1)
}
this.lines.push({
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
text,
})
}
}
}
this.lines.sort((a, b) => {
return a.time - b.time
})
this.maxLine = this.lines.length - 1
}
_currentTime() {
return getNow() - this._performanceTime + this._performanceOffsetTime
}
_findCurLineNum(curTime) {
const length = this.lines.length
for (let index = 0; 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)
let curLine = this.lines[this.curLineNum]
let nextLine = this.lines[this.curLineNum + 1]
const currentTime = this._currentTime()
const driftTime = currentTime - curLine.time
if (driftTime >= 0 || this.curLineNum === 0) {
if (this.curLineNum === this.maxLine) return this._handleMaxLine()
this.delay = nextLine.time - curLine.time - driftTime
if (this.delay > 0) {
if (!this.isOffseted && this.delay >= this.offset) {
this._performanceOffsetTime += this.offset
this.delay -= this.offset
this.isOffseted = true
}
timeoutTools.start(() => {
this._refresh()
}, this.delay)
this.onPlay(this.curLineNum, curLine.text, currentTime)
return
}
}
this.curLineNum = this._findCurLineNum(currentTime) - 1
this._refresh()
}
play(curTime = 0) {
if (!this.lines.length) return
this.pause()
this.isPlay = true
this._performanceOffsetTime = 0
this._performanceTime = getNow() - curTime
if (this._performanceTime < 0) {
this._performanceOffsetTime = -this._performanceTime
this._performanceTime = 0
}
this.curLineNum = this._findCurLineNum(curTime) - 1
this._refresh()
}
pause() {
if (!this.isPlay) return
this.isPlay = false
this.isOffseted = 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) {
if (this.isPlay) this.pause()
this.lyric = lyric
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() {
this.invokeTime = 0
this.animationFrameId = null
this.timeoutId = null
this.callback = null
this.thresholdTime = 200
}
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 = 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
}
}
}
+11 -5
View File
@@ -2,10 +2,13 @@ import { httpFetch } from '../../request'
import { decodeLyric } from './util'
import { decodeName } from '../..'
const headExp = /^.*\[id:\$00000000\]\n/
const parseLyric = str => {
str = str.replace(/(?:<\d+,\d+,\d+>|\r)/g, '')
if (str.startsWith('\ufeff[id:$00000000]')) str = str.replace('\ufeff[id:$00000000]\n', '')
str = str.replace(/\r/g, '')
if (headExp.test(str)) str = str.replace(headExp, '')
let trans = str.match(/\[language:([\w=\\/+]+)\]/)
let lyric
let tlyric
if (trans) {
str = str.replace(/\[language:[\w=\\/+]+\]\n/, '')
@@ -18,7 +21,7 @@ const parseLyric = str => {
}
}
let i = 0
let lyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
let result = str.match(/\[((\d+),\d+)\].*/)
let time = parseInt(result[2])
let ms = time % 1000
@@ -31,11 +34,14 @@ const parseLyric = str => {
return str.replace(result[1], time)
})
tlyric = tlyric ? tlyric.join('\n') : ''
lyric = decodeName(lyric)
lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>')
lxlyric = decodeName(lxlyric)
lyric = lxlyric.replace(/<\d+,\d+>/g, '')
tlyric = decodeName(tlyric)
return {
lyric,
tlyric,
lxlyric,
}
}
@@ -119,7 +125,7 @@ export default {
let requestObj = this.searchLyric(songInfo.name, songInfo.hash, songInfo._interval || this.getIntv(songInfo.interval))
requestObj.promise = requestObj.promise.then(result => {
if (!result) return { lyric: '', tlyric: '' }
if (!result) return { lyric: '', tlyric: '', lxlyric: '' }
let requestObj2 = this.getLyricDownload(result.id, result.accessKey)
+3 -1
View File
@@ -20,7 +20,9 @@ const wy = {
return getLyric(songInfo.songmid)
},
getPic(songInfo) {
return getMusicInfo(songInfo.songmid).then(info => info.al.picUrl)
const requestObj = getMusicInfo(songInfo.songmid)
requestObj.promise = requestObj.promise.then(info => info.al.picUrl)
return requestObj
},
getMusicDetailPageUrl(songInfo) {
return `https://music.163.com/#/song?id=${songInfo.songmid}`