-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
219 additions
and
2 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
|
||
@font-face{font-family:'icon';src:url('//s4.zstatic.net/ajax/libs/remixicon/4.2.0/remixicon.woff2');} | ||
html{background:#F9F9FB;} | ||
body{margin:0;overflow:hidden;} | ||
font{font-family:icon;} | ||
*{scrollbar-width:none;box-sizing:border-box;user-select:none;outline:none;} | ||
::-webkit-scrollbar{display:none;} | ||
|
||
button{background:#0292FE;color:white;border:0;border-radius:5px;padding:5px 20px;font-size:1rem;transition:filter .2s;font-family:inherit;height:40px;} | ||
button:hover{filter:brightness(.95);} | ||
button:active{filter:brightness(.9);} | ||
audio{margin-bottom:10px;width:100%;} | ||
textarea{background:white;border-radius:5px;border:0;margin-bottom:10px;height:100%;resize:none;padding:5px 10px;white-space:nowrap;} | ||
|
||
#input{position:fixed;left:10px;top:10px;height:calc(100% - 20px);width:calc(50% - 15px);display:flex;flex-direction:column;} | ||
#render{position:fixed;right:10px;top:10px;height:calc(100% - 20px);width:calc(50% - 15px);background:white;} | ||
#container{width:100%;height:100%;} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
|
||
let lrcInstance; | ||
|
||
function render() { | ||
let options = {callback: text => {document.title = text + " - 风吹故里";}}; | ||
try { options = JSON.parse(document.getElementById("confInput").value); } | ||
catch (_ignore) {} | ||
lrcInstance = new SimLRC(document.getElementById("lrcInput").value); | ||
lrcInstance.render(document.getElementById("container"), document.getElementById("audio"), options); | ||
} |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"> | ||
<link rel="stylesheet" href="demo.css"> | ||
<link rel="stylesheet" href="../simlrc.css"> | ||
<title>SimLRC Demo</title> | ||
</head> | ||
<body> | ||
|
||
<div id="input"> | ||
<textarea id="lrcInput" placeholder="歌词文本(LRC 格式)...">[00:00.00]风吹故里 | ||
[00:16.41]风声静谧 秋雨依依 | ||
[00:22.53]时光又锁住一年的雨季 | ||
[00:31.11]潮湿空气 几分熟悉 | ||
[00:37.56]像那时相遇 你身上的气息 | ||
[00:44.83][01:40.17]黑白记忆 如同电影 | ||
[00:49.64][01:44.89]遗憾的剧情用离别布景 | ||
[00:53.09][01:48.39]漫长流年 独你缺席 | ||
[00:56.30][01:51.53]残缺的结局 是我一个人的独角戏 | ||
[00:59.73][01:54.91][02:24.42]我把思念写尽风中 吹过故里 | ||
[01:03.99][01:59.22][02:28.73]多想能落在你怀里 片刻光景 | ||
[01:07.67][02:02.91][02:32.61]可数不清的四季 已抹去爱的痕迹 | ||
[01:14.31][02:09.69][02:39.18]我把回忆轻轻拾起 放在掌心 | ||
[01:18.69][02:13.96][02:43.56]可惜你模糊的身影 无法靠近 | ||
[01:22.39][02:17.77][02:47.30]无数个失眠夜里 伤痛又会被唤醒 | ||
这是一行没啥用的文本,不会被渲染在歌词中 | ||
你可以尝试编辑或打乱歌词后再次点击「渲染」按钮查看效果 | ||
默认配置下,时间标签相同的不同歌词将作为多语言翻译渲染 | ||
</textarea> | ||
<textarea id="confInput" placeholder="配置信息(JSON 格式),详情查看文档 ..."></textarea> | ||
<audio src="demo.webm" controls id="audio"></audio> | ||
<button onclick="render()">渲染 <font></font></button> | ||
</div> | ||
|
||
<div id="render"> | ||
<div id="container"> | ||
<div style="position:absolute;opacity:.5;inset:0;margin:auto;width:fit-content;height:fit-content;">按「渲染」以开始</div> | ||
</div> | ||
</div> | ||
|
||
|
||
|
||
<script src="../simlrc.js"></script> | ||
<script src="demo.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
|
||
.SimLRC{position:relative;overflow-x:hidden;overflow-y:scroll;text-align:var(--align);} | ||
.SimLRC::before,.SimLRC::after{content:"";display:block;height:50%;} | ||
.SimLRC>div{color:var(--normalColor);margin:calc(var(--lineSpace) * var(--inactiveZoom)) .2em;font-size:1.5em;transform:scale(var(--inactiveZoom));transform-origin:center var(--align);transition:all .3s;} | ||
.SimLRC.scrolling>div{filter:none!important;} | ||
.SimLRC>div.active{color:var(--activeColor);transform:scale(1);margin:var(--lineSpace) .2em;} | ||
.SimLRC>div:hover{color:var(--hoverColor);} | ||
.SimLRC>div>span,.SimLRC>div>small{display:block;} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
|
||
class SimLRC { | ||
constructor(lrc, container = null) { | ||
// 解析歌词 | ||
const lrcSpilitted = lrc.split("\n"); | ||
this.lrcParsed = {}; | ||
for (let lineNum in lrcSpilitted) { | ||
const line = lrcSpilitted[lineNum]; | ||
const regex = /\[\d+\:\d+\.\d+\]/g; | ||
const tags = (line.match(regex) || []).map(match => match.slice(1, -1)); | ||
const text = line.replace(regex, "").trim(); | ||
if (!tags || !text) continue; | ||
tags.forEach(tag => { | ||
const [minutes, seconds] = tag.split(':').map(Number); | ||
const msTime = minutes * 60000 + seconds * 1000; | ||
if (msTime || msTime === 0) { | ||
if (!this.lrcParsed[msTime]) this.lrcParsed[msTime] = []; | ||
this.lrcParsed[msTime].push(text); | ||
} | ||
}); | ||
} | ||
if (!Object.keys(this.lrcParsed).length) this.lrcParsed = {0: ["暂无歌词"]}; | ||
} | ||
|
||
render(container, audio, options = {}) { | ||
if (!container || !audio) return; | ||
// 初始化配置项 | ||
const defaultOptions = { | ||
blurStep: 1, | ||
blurMin: 2, | ||
blurMax: 5, | ||
normalColor: "#00000088", | ||
activeColor: "#000000", | ||
clickUpdate: true, | ||
multiLangSupport: true, | ||
align: "center", | ||
inactiveZoom: .8, | ||
lineSpace: .8, | ||
scrollTimeout: 3000, | ||
}; | ||
options = Object.assign(defaultOptions, options); | ||
console.log(options) | ||
// 渲染歌词HTML | ||
container.innerHTML = ""; | ||
for (let timestamp in this.lrcParsed) { | ||
const currentLrc = this.lrcParsed[timestamp]; | ||
if (options.multiLangSupport) { | ||
// 启用多语言支持,则同时间戳不同歌词在同一个div渲染 | ||
const lrcDiv = document.createElement("div"); | ||
lrcDiv.dataset.stamp = timestamp; | ||
currentLrc.forEach((text, index) => { | ||
const textElement = document.createElement(index ? "small" : "span"); | ||
textElement.innerText = text; | ||
lrcDiv.appendChild(textElement); | ||
}); | ||
container.appendChild(lrcDiv); | ||
} else { | ||
// 禁用多语言支持,则同时间戳不同歌词分开渲染 | ||
currentLrc.forEach(text => { | ||
const lrcDiv = document.createElement("div"); | ||
lrcDiv.dataset.stamp = timestamp; | ||
lrcDiv.innerText = text; | ||
container.appendChild(lrcDiv); | ||
}); | ||
} | ||
} | ||
// 设置样式 | ||
container.classList.add("SimLRC"); | ||
container.style.setProperty("--align", options.align); | ||
container.style.setProperty("--normalColor", options.normalColor); | ||
container.style.setProperty("--activeColor", options.activeColor); | ||
container.style.setProperty("--hoverColor", options.clickUpdate ? options.activeColor : options.normalColor); | ||
container.style.setProperty("--inactiveZoom", options.inactiveZoom); | ||
container.style.setProperty("--lineSpace", options.lineSpace + "em"); | ||
// 监听事件 | ||
const refreshLrcProgress = forceScroll => { | ||
const currentTime = audio.currentTime * 1000; | ||
let lrcEles = Array.from(container.getElementsByTagName("div")); | ||
for (let index in lrcEles) { | ||
let div = lrcEles[index]; | ||
if (div.dataset.stamp <= currentTime && (!div.nextElementSibling || div.nextElementSibling.dataset.stamp > currentTime)) { | ||
// 执行回调 | ||
if (!div.classList.contains("active") && options.callback) options.callback(div.innerText); | ||
if (!div.classList.contains("active") || forceScroll) { | ||
// 取消用户滚动模式 | ||
if (forceScroll) container.classList.remove("scrolling"); | ||
// 设置为当前歌词并滚动 | ||
div.classList.add("active"); | ||
if (!container.classList.contains("scrolling")) div.scrollIntoView({ behavior: "smooth", block: "center" }); | ||
// 渲染歌词模糊效果 | ||
if (options.blurStep && options.blurMax) { | ||
div.style.filter = "none"; | ||
const prevSiblings = []; | ||
let prev = div.previousElementSibling; | ||
while (prev) { | ||
prevSiblings.push(prev); | ||
prev = prev.previousElementSibling; | ||
} | ||
let next = div.nextElementSibling; | ||
const nextSiblings = []; | ||
while (next) { | ||
nextSiblings.push(next); | ||
next = next.nextElementSibling; | ||
} | ||
for (let index = 0; index <= Math.max(prevSiblings.length, nextSiblings.length); index++) { | ||
const blurPixel = Math.min(options.blurMin + options.blurStep * index, options.blurMax); | ||
if (prevSiblings[index]) prevSiblings[index].style.filter = `blur(${blurPixel}px)`; | ||
if (nextSiblings[index]) nextSiblings[index].style.filter = `blur(${blurPixel}px)`; | ||
} | ||
} | ||
} | ||
} else div.classList.remove("active"); | ||
} | ||
}; | ||
audio.addEventListener("timeupdate", () => { refreshLrcProgress(); }); | ||
if (options.clickUpdate) { | ||
Array.from(container.getElementsByTagName("div")).forEach(div => { | ||
div.onclick = () => { audio.currentTime = div.dataset.stamp / 1000; refreshLrcProgress(true); }; | ||
}); | ||
} | ||
refreshLrcProgress(); | ||
// 处理用户滚动 | ||
const handleUserScroll = () => { | ||
clearTimeout(this.scrollTimeoutId); | ||
this.scrollTimeoutId = setTimeout(() => { | ||
container.classList.remove("scrolling"); | ||
refreshLrcProgress(true); | ||
}, options.scrollTimeout); | ||
container.classList.add("scrolling"); | ||
} | ||
container.onwheel = handleUserScroll; | ||
container.onpointerdown = () => { this.pointerDown = true; }; | ||
container.onpointerup = () => { this.pointerDown = false; }; | ||
container.onpointerout = () => { this.pointerDown = false; }; | ||
container.onpointermove = () => { if (this.pointerDown) handleUserScroll(); }; | ||
} | ||
} |