diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..ba2a9c2 Binary files /dev/null and b/favicon.ico differ diff --git a/images/banner.png b/images/banner.png new file mode 100644 index 0000000..12fff4e Binary files /dev/null and b/images/banner.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..fc7960c --- /dev/null +++ b/index.html @@ -0,0 +1,94 @@ + + + + + Liricle + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

Liricle

+ + + + + +
+ +
+
+
+
+ +
+ + +
+
+
+ 00:00.00 + 00:00.00 +
+
+ +
+ + + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..45d1045 --- /dev/null +++ b/script.js @@ -0,0 +1,198 @@ +fetch("https://raw.githubusercontent.com/mcanam/liricle/main/package.json").then(async res => { + const { version } = await res.json(); + const script = document.createElement("script"); + + script.src = `https://cdn.jsdelivr.net/npm/liricle@${version}/dist/liricle.js`; + script.onload = () => main(window.Liricle); + + document.body.append(script); +}); + +function main(Liricle) { + window.liricle = new Liricle(); + + ////////////////////// PLAYER ////////////////////////////// + + let sliding = false; + + noUiSlider.create($player_slider, { + start: 0, + connect: "lower", + range: { min: 0, max: 100 }, + }); + + $player_slider.noUiSlider.on("slide", () => { + const value = parseFloat($player_slider.noUiSlider.get()); + sliding = true; + $player_audio.currentTime = value; + }); + + $player_slider.noUiSlider.on("change", () => { + sliding = false; + }); + + $player_play_button.addEventListener("click", () => { + if (isNaN($player_audio.duration)) return; + + if ($player_play_button.dataset.play == "false") { + $player_audio.play(); + $player_play_button.dataset.play = "true"; + } else { + $player_audio.pause(); + $player_play_button.dataset.play = "false"; + } + }); + + $player_audio.src = "https://raw.githubusercontent.com/mcanam/assets/main/liricle-demo/audio.mp3"; + + $player_audio.addEventListener("canplaythrough", () => { + const duration = $player_audio.duration; + + $player_slider.noUiSlider.updateOptions({ + range: { min: 0, max: duration }, + }); + + $player_time_total.innerText = timeToText(duration); + }); + + $player_audio.addEventListener("timeupdate", () => { + const time = $player_audio.currentTime; + + if (!sliding) $player_slider.noUiSlider.set(time); + $player_time_current.innerText = timeToText(time); + }); + + $player_audio.addEventListener("ended", () => { + $player_play_button.dataset.play = "false"; + }); + + function timeToText(time) { + let min = Math.floor(time / 60); + let sec = (time % 60).toFixed(2); + min = min < 10 ? "0" + min : min; + return min + ":" + sec; + } + + ////////////////////// MENU ////////////////////////////// + + let isShow = false; + + $menu_load_lyric.addEventListener("click", () => { + loadFile(dataURL => liricle.load({ url: dataURL })); + }); + + $menu_load_audio.addEventListener("click", () => { + loadFile(dataURL => ($player_audio.src = dataURL)); + }); + + $menu_lyric_offset.addEventListener("blur", () => { + liricle.offset = $menu_lyric_offset.value; + }); + + $menu_button.addEventListener("click", () => { + const rect = $menu_button.getBoundingClientRect(); + + $menu.style.top = rect.y - $menu.offsetHeight - 20 + "px"; + $menu.style.left = rect.x - $menu.offsetWidth + 50 + "px"; + $menu.classList[isShow ? "remove" : "add"]("show"); + + isShow = !isShow; + }); + + window.addEventListener("click", e => { + if (!isShow) return; + if (e.target.closest(".menu")) return; + if (e.target == $menu_button) return; + + $menu.classList.remove("show"); + isShow = !isShow; + }); + + function loadFile(callback) { + const input = document.createElement("input"); + const reader = new FileReader(); + + input.type = "file"; + input.multiple = false; + + input.onchange = () => { + const file = input.files[0]; + reader.readAsDataURL(file); + }; + + reader.onload = () => { + callback(reader.result); + }; + + input.click(); + } + + ////////////////////// LYRIC ////////////////////////////// + + let $lines = []; + let $activeLine = null; + + liricle.load({ + url: "https://raw.githubusercontent.com/mcanam/assets/main/liricle-demo/lyric-enhanced.lrc" + }); + + liricle.on("load", ({ tags, lines, enhanced }) => { + $lines = []; + $activeLine = null; + $lyric_content.innerHTML = ""; + + // set default offset + if ("offset" in tags) { + liricle.offset = tags.offset; + $menu_lyric_offset.value = tags.offset; + } + + lines.forEach(line => { + const $line = document.createElement("div"); + $line.className = "lyric__line"; + $line.innerHTML = line.words ? "" : line.text; + + if (enhanced && line.words) { + line.words.forEach(word => { + const $word = document.createElement("span"); + $word.className = "lyric__word"; + $word.innerHTML = word.text + " "; + + $line.append($word); + }); + } + + $lines.push($line); + $lyric_content.append(...$lines); + }); + + if (enhanced) $lyric_cursor.style.display = "block"; + }); + + liricle.on("sync", (line, word) => { + if ($activeLine) { + $activeLine.classList.remove("active"); + } + + $activeLine = $lines[line.index]; + $activeLine.classList.add("active"); + + const oh1 = $lyric.offsetHeight / 2; + const oh2 = $activeLine.offsetHeight / 2; + + $lyric.scrollTop = $activeLine.offsetTop - (oh1 - oh2); + + if (word) { + const $word = $activeLine.children[word.index]; + + $lyric_cursor.style.width = $word.offsetWidth + "px"; + $lyric_cursor.style.top = ($word.offsetTop + $word.offsetHeight) + "px"; + $lyric_cursor.style.left = $word.offsetLeft + "px"; + } + }); + + $player_audio.addEventListener("timeupdate", () => { + const time = $player_audio.currentTime; + liricle.sync(time); + }); +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..8bb4b85 --- /dev/null +++ b/style.css @@ -0,0 +1,225 @@ +*, +*::before, +*::after { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +html { + font-family: "Poppins", sans-serif; + font-size: 16px; + color: #ededf8; +} + +body { + position: relative; + width: 100vw; + height: 100vh; + background-color: #00001b; + overflow: hidden; +} + +a { + text-decoration: none; + color: inherit; +} + +button, +input { + font-family: sans-serif; + font-size: 1rem; + color: inherit; + border: none; + outline: none; + background-color: transparent; +} + +button { + user-select: none; + cursor: pointer; +} + +.bi { + width: 1.2rem; + height: auto; + pointer-events: none; +} + +.topbar, +.player { + position: fixed; + z-index: 2; + width: 100%; + padding: 20px; + display: flex; + align-items: center; + background-color: rgba(0, 0, 27, 0.2); + backdrop-filter: blur(4px); +} + +.topbar { + top: 0; + left: 0; +} + +.topbar__logo { + width: 0.9rem; + height: auto; + margin-right: 10px; +} + +.topbar__title { + font-size: 1rem; + font-weight: 600; + margin-right: auto; +} + +.player { + bottom: 0; + left: 0; +} + +.player__button { + line-height: 0; + /* centering icon */ + width: 50px; + height: 50px; + border-radius: 10px; + background-color: rgba(255, 255, 255, 0.1); +} + +.player__button[data-play="true"] .bi:nth-child(1) { + display: none; +} + +.player__button[data-play="true"] .bi:nth-child(2) { + display: inline-block; +} + +.player__button[data-play="false"] .bi:nth-child(1) { + display: inline-block; +} + +.player__button[data-play="false"] .bi:nth-child(2) { + display: none; +} + +.player__progress { + flex-grow: 1; + margin: 0 20px; +} + +.player__slider { + width: 100%; + height: 2px; + margin-bottom: 20px; + border: none; + box-shadow: unset; + background-color: rgba(255, 255, 255, 0.1); +} + +.player__slider .noUi-connect { + background-color: #0277ef; +} + +.player__slider .noUi-handle { + width: 10px; + height: 10px; + right: -5px; + top: -4px; + border: none; + border-radius: 50%; + box-shadow: unset; + background-color: #0277ef; +} + +.player__slider .noUi-handle::before, +.player__slider .noUi-handle::after { + display: none; +} + +.player__time { + font-size: 0.6rem; + line-height: 0; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; +} + +.menu { + position: absolute; + bottom: 0; + right: 0; + z-index: 3; + width: 250px; + height: fit-content; + padding: 10px 0; + border-radius: 20px; + background-color: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(8px); + pointer-events: none; + opacity: 0; + transition: 0.2s; +} + +.menu.show { + pointer-events: unset; + opacity: 1; +} + +.menu__item { + width: 100%; + padding: 10px 20px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.menu__input { + width: 100px; + padding: 15px; + font-size: 0.8rem; + font-weight: 500; + text-align: center; + border-radius: 10px; + background-color: rgba(255, 255, 255, 0.1); +} + +.lyric { + position: relative; + width: 100%; + height: 100vh; + padding: 300px 40px; + overflow: auto; + scroll-behavior: smooth; +} + +.lyric::-webkit-scrollbar { + display: none; +} + +.lyric__line { + text-align: center; + font-size: 1.5rem; + font-weight: 500; + margin-bottom: 20px; + opacity: 0.2; + transition: 0.2s; +} + +.lyric__line.active { + opacity: 1; +} + +.lyric__cursor { + position: absolute; + width: 4px; + height: 4px; + border-radius: 5px; + background-color: currentColor; + display: none; + opacity: 0.2; + transition: 0.3s; +}