歌词模式转换
本帖最后由 亚伦影音工作室 于 2025-12-11 09:00 编辑 <br /><br /><style>#bj {
position: relative;
width: 1264px;
height: 680px;
margin-left: -300px;
margin-top: 10px;
overflow: hidden;
background: url(https://pic1.imgdb.cn/item/658d7b30c458853aefc07335.jpg);
background-size: cover;
}
.lyrics{margin: 0;z-index: 20;
top: 89%;display: none;
left: 50%;
transform: translate(-50%, -50%);
height: 100px; /* 调整高度,只容纳当前歌词 */
text-align: center;
position: absolute;animation:flash 1s infinite alternate;
}
.lyric-line{
width: 100%;
position: relative;
height: 60px;
overflow: visible;
font: 100 50px '华文隶书', sans-serif;
line-height: 60px;
text-align: left;
white-space: nowrap; /* 禁止换行 */
filter: drop-shadow(#fff 1px 0 0) drop-shadow(#fff 0 1px 0) drop-shadow(#fff -1px 0 0) drop-shadow(#fff 0 -1px 0);
}
.lyric-mask {
position: absolute;
top: 0;
left: 0;
width: 0;
overflow: hidden;
color: #ff0000;
height: 100%;
white-space: nowrap;
}
.lyric-original {
color: #00aa00;
white-space: nowrap;
}
#wrapper {z-index: 9;
position: absolute; writing-mode: vertical-lr;white-space: pre; width: 60px;height: 90%;top: 50px; left: 88%;
padding: 0px;
font:normal 2.6em 华文隶书;
filter: drop-shadow(#fff 1px 0 0) drop-shadow(#fff 0 1px 0) drop-shadow(#fff -1px 0 0) drop-shadow(#fff 0 -1px 0);
}
.char {
display: inline-block;
padding: 2px 2px;
opacity: 0;
transform: translate(var(--x), var(--y));
animation: fadeIn 1.5s var(--delay) forwards, flash 1s infinite alternate;
}
@keyframes fadeIn {
to {
transform:scale(1);
opacity: 1;
}
}
@keyframes flash {
to { filter: hue-rotate(360deg)brightness(60%); }
}
#g1,#g2,#fullscreen{border-radius: 4px;position: relative;
color:#fff;background:#0000;box-shadow:0px 0px 0px 1px #fff;
padding: 4px 8px;
font-size: 12px;
border: none;
cursor: pointer;margin: 8px 5px;left: 6%;
}
#player {position:absolute;top: 60%;z-index: 99;
left: 7%;
width: 300px;
height: 300px;
display: grid;
place-items: center;
animation: rot 8s linear infinite ;
}
@keyframes rot {to { transform: rotate(360deg);filter: hue-rotate(360deg); }}
#rect {position: absolute;
width: 30%;
height: 30%;
display: grid;
place-items: center;
clip-path: polygon(60% 0, 100% 0, 50% 50%, 40% 100%, 0 100%, 50% 50%);
}
#rect:nth-of-type(1) { background:#5500ff;transform: rotate(0deg);
}
#rect:nth-of-type(2) { background:#ff0000;transform: rotate(45deg);
}
#rect:nth-of-type(3) { background:#00aa00;transform: rotate(90deg);
}
#rect:nth-of-type(4) { background:#fff000;transform: rotate(135deg);
}
</style>
<div id="bj">
<div id="wrapper">红蔷薇 - 走不完的路</div>
<div class="lyrics" >
<div class="lyric-line">
<div class="lyric-mask"></div>
<div class="lyric-original"></div>
</div>
</div>
<div>
<button id="g1" onclick="btn1()"title="竖排歌词">竖排歌词</button>
<button id="g2" onclick="btn2()"title="横排歌词">横排歌词</button>
<span id="fullscreen" title="屏展模式">全屏欣赏</span>
</div>
<div id="player">
<spanid="rect"></span>
<spanid="rect"></span>
<spanid="rect"></span>
<spanid="rect"></span>
</div>
</div>
<audio id="aud" src="https://s2.ananas.chaoxing.com/sv-w9/audio/96/ad/79/969030f3e99e465e22e7d73f15a2bb19/audio.mp3" loop autoplay></audio>
<script>
const lrc = `红蔷薇 - 走不完的路
出品 亚伦影音工作室
人生就像趟不完的河
无论多少曲折都要过
风一程雨一程艰难跋涉
所有苦累全都埋心窝
人生就像唱不完的歌
酸甜苦辣又能跟谁说
爱一场痛一场历尽苦涩
受过的伤今生难愈合
人的这一生起起落落
不知要面对多少坎坷
走不完的路爬不完的坡
无论多难都不能退缩
人的这一生对对错错
谁能参透这世间因果
流不完的泪受不完寂寞
总有挥之不去的困惑
人生就像唱不完的歌
酸甜苦辣又能跟谁说
爱一场痛一场历尽苦涩
受过的伤今生难愈合
人的这一生起起落落
不知要面对多少坎坷
走不完的路爬不完的坡
无论多难都不能退缩
人的这一生对对错错
谁能参透这世间因果
流不完的泪受不完寂寞
总有挥之不去的困惑
人的这一生起起落落
不知要面对多少坎坷
走不完的路爬不完的坡
无论多难都不能退缩
人的这一生对对错错
谁能参透这世间因果
流不完的泪受不完寂寞
总有挥之不去的困惑
总有挥之不去的困惑
`;
const aud= document.getElementById('aud');
const lyrics = parseLyrics(lrc);
const lyricMask = document.querySelector('.lyric-mask');
const lyricOriginal = document.querySelector('.lyric-original');
let currentIndex = -1;
let currentLyric = null;
// 解析歌词(支持两种格式)
function parseLyrics(lrcText) {
const lyrics = [];
if (lrcText.includes('karaoke.add')) {
const lineRegex = /karaoke\.add\('([^']+)', '([^']+)', '([^']+)', '([^']+)'\);/g;
let match;
while ((match = lineRegex.exec(lrcText)) !== null) {
const startTime = timeToMs(match);
const endTime = timeToMs(match);
const text = match.replace(/\[|\]/g, '').trim();
const durations = match.split(',').map(Number);
if (text) {
lyrics.push({startTime, endTime, text, durations});
}
}
}
else if (lrcText.includes('[')) {
const lines = lrcText.split('\n').filter(line => line.trim());
lines.forEach((line, index) => {
const timeMatch = line.match(/\[(\d+:\d+\.\d+)\]/);
if (timeMatch) {
const timeStr = timeMatch;
const text = line.replace(/\[.*?\]/, '').trim();
if (text) {
const startTime = timeToMs(timeStr);
const nextLine = lines;
const nextTimeMatch = nextLine ? nextLine.match(/\[(\d+:\d+\.\d+)\]/) : null;
const endTime = nextTimeMatch ? timeToMs(nextTimeMatch) : startTime + 5000;
lyrics.push({
startTime,
endTime,
text,
durations: calculateCharDurations(text, startTime, endTime)
});
}
}
});
}
return lyrics;
}
function calculateCharDurations(text, startTime, endTime) {
const totalDuration = endTime - startTime;
const charCount = text.length;
const baseDur = Math.floor(totalDuration / charCount);
const durations = new Array(charCount).fill(baseDur);
const remainder = totalDuration % charCount;
for (let i = 0; i < remainder; i++) {
durations++;
}
return durations;
}
function timeToMs(timeStr) {
const parts = timeStr.split(':');
const minutes = parseInt(parts, 10);
const secondsAndMs = parts.split('.');
const seconds = parseInt(secondsAndMs, 10);
const ms = parseInt(secondsAndMs || 0, 10);
return minutes * 60 * 1000 + seconds * 1000 + ms;
}
function getCurrentLyricIndex(lyrics, currentTimeMs) {
for (let i = 0; i < lyrics.length; i++) {
if (currentTimeMs >= lyrics.startTime && currentTimeMs <= lyrics.endTime) {
return i;
}
}
return -1;
}
function updateLyricDisplay(index) {
if (index < 0 || index >= lyrics.length) return;
currentIndex = index;
currentLyric = lyrics;
lyricOriginal.textContent = currentLyric.text;
lyricMask.textContent = currentLyric.text;
lyricMask.style.width = '0%';
}
function updateLyricMask(currentTimeMs) {
if (!currentLyric) return;
const lyricStartTime = currentLyric.startTime;
const elapsed = currentTimeMs - lyricStartTime;
const totalDuration = currentLyric.durations.reduce((sum, d) => sum + d, 0);
let charIndex = 0;
let accumulatedTime = 0;
for (let i = 0; i < currentLyric.durations.length; i++) {
accumulatedTime += currentLyric.durations;
if (elapsed <= accumulatedTime) {
charIndex = i + 1;
break;
}
}
if (elapsed >= totalDuration) {
charIndex = currentLyric.text.length;
}
charIndex = Math.min(charIndex, currentLyric.text.length);
const tempSpan = document.createElement('span');
tempSpan.style.visibility = 'hidden';
tempSpan.style.position = 'absolute';
tempSpan.style.fontSize = '50px';
tempSpan.style.fontWeight = '300';
document.body.appendChild(tempSpan);
const visibleText = currentLyric.text.substring(0, charIndex);
tempSpan.textContent = visibleText;
const width = tempSpan.offsetWidth;
document.body.removeChild(tempSpan);
lyricMask.style.width = `${width}px`;
}
// 监听更新歌词
aud.addEventListener('timeupdate', () => {
const currentTimeMs = aud.currentTime * 1000;
const index = getCurrentLyricIndex(lyrics, currentTimeMs);
if (index !== currentIndex) {
updateLyricDisplay(index);
}
updateLyricMask(currentTimeMs);
});
updateLyricDisplay(0);
const gcAr = lrc2HC(lrc);
let curkey = 0, isSeeking = false;
aud.ontimeupdate = () => {
if(curkey > gcAr.length - 1) return;
if(aud.currentTime >= gcAr) {
const gap = gcAr?. ?? 0 - gcAr;
showLrc(gcAr, wrapper, gap);
}
};
aud.onended = () => {
curkey = 0;
aud.play();
}
aud.onseeked = () => calcKey();
function lrc2HC(text) {
let lrcAr = [];
let ar = text.trim().split('\n');
ar.sort();
let reg = /\[(\d+)[.:](\d+)[.:](\d+)\](.*)/;
ar.forEach(item => {
if(reg.test(item)) {
let result = item.match(reg);
let tmsg = parseInt(result) * 60 + parseInt(result) + parseInt(result) / 1000;
lrcAr.push(.trim()]);
}
});
return lrcAr ? lrcAr : ;
};
function calcKey() {
for (let j = 0; j < gcAr.length; j++) {
if (aud.currentTime <= gcAr) {
curkey = j - 1;
break;
}
}
if (curkey < 0) curkey = 0;
if (curkey > gcAr.length - 1) curkey = gcAr.length - 1;
let time = gcAr?. ?? 0 - gcAr;
isSeeking = false;
showLrc(gcAr, wrapper, time);
}
function showLrc(str, targetElm, time) {
if(isSeeking) return;
targetElm.innerHTML = '';
const chars = str.split('').map(c => c === ' ' ? ' ' : c);
const frg = document.createDocumentFragment();
chars.forEach((char, idx) => {
const span = document.createElement('span');
span.innerHTML = char;
span.classList.add('char');
const x = Math.random() * (Math.random() > 0.5 ? 600 : -300);
const y = Math.random() * (Math.random() > 0.5 ? 600 : -300);
span.style.cssText += `
color: #${Math.random().toString(16).substring(2,8)};
--x: ${x}px;
--y: ${y}px;
--delay: ${Math.random() * 0.5}s;
`;
frg.appendChild(span);
});
targetElm.appendChild(frg);
curkey ++;
setTimeout(() =>isSeeking = false, time);
}
</script>
<script>
var g1=document.getElementById("wrapper");
const g2 = document.querySelector('.lyrics');
function btn1() {
g1.style.display = 'block';
g2.style.display = 'none';
}
function btn2() {
g1.style.display = 'none';
g2.style.display = 'block';
}
let fs = true;
fullscreen.onclick = () => {
if (fs) {
fullscreen.innerText = '退出全屏';
bj.requestFullscreen();
} else {
fullscreen.innerText = '全屏欣赏';
document.exitFullscreen();
}
fs = !fs;
};
player.onclick = () => aud.paused ? (aud.play(),player.style.animationPlayState = 'running',background.style.animationPlayState = 'running') :( aud.pause(),player.style.animationPlayState = 'paused',background.style.animationPlayState = 'paused');
</script> 唯美,技术含量高 横排可以考虑同样放在右上 马黑黑 发表于 2025-12-9 21:42
横排可以考虑同样放在右上
谢谢老师指教! 亚伦影音工作室 发表于 2025-12-9 21:47
谢谢老师指教!
{:4_191:} 真漂亮的制作,歌词还能切换呢。
欣赏亚伦老师好帖{:4_199:} 歌词横、竖排自如,谢谢亚伦老师精彩分享{:4_191:} 老师创意十足,欣赏点赞!这样的好作品可惜在离退休论坛就无法发表,真的希望老师能到离退休发帖指导,谢谢!
页:
[1]