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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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;
+}