diff --git a/public/src/css/debug.css b/public/src/css/debug.css index f9e1478..d87c209 100644 --- a/public/src/css/debug.css +++ b/public/src/css/debug.css @@ -123,6 +123,7 @@ } #debug .autoplay-label, -#debug .branch-hide{ +#debug .branch-hide, +#debug .lyrics-hide{ display: none; } diff --git a/public/src/css/game.css b/public/src/css/game.css index 69d1127..290c1eb 100644 --- a/public/src/css/game.css +++ b/public/src/css/game.css @@ -89,3 +89,38 @@ .fix-animations *{ animation: none !important; } +#song-lyrics{ + position: absolute; + right: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale)); + bottom: calc(44 / 720 * 100vh - 30px * var(--scale)); + left: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale)); + text-align: center; + font-family: Meiryo, sans-serif; + font-weight: bold; + font-size: calc(45px * var(--scale)); + line-height: 1.2; +} +#game.portrait{ + right: calc(20px * var(--scale)); + left: calc(20px * var(--scale)); +} +#song-lyrics .stroke, +#song-lyrics .fill{ + position: absolute; + right: 0; + bottom: 0; + left: 0; +} +#song-lyrics .stroke{ + -webkit-text-stroke: calc(7px * var(--scale)) #00a; +} +#song-lyrics .fill{ + color: #fff; +} +#song-lyrics ruby{ + display: inline-flex; + flex-direction: column-reverse; +} +#song-lyrics rt{ + line-height: 1; +} diff --git a/public/src/js/assets.js b/public/src/js/assets.js index 5230169..7d769ee 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -32,7 +32,8 @@ var assets = { "logo.js", "settings.js", "scorestorage.js", - "account.js" + "account.js", + "lyrics.js" ], "css": [ "main.css", diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 7c93cce..0447daf 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -57,6 +57,10 @@ class Controller{ if(song.id == this.selectedSong.folder){ this.mainAsset = song.sound this.volume = song.volume || 1 + if(song.lyricsData && !multiplayer && (!this.touchEnabled || this.autoPlayEnabled)){ + var lyricsDiv = document.getElementById("song-lyrics") + this.lyrics = new Lyrics(song.lyricsData, selectedSong.offset, lyricsDiv) + } } }) } @@ -316,5 +320,8 @@ class Controller{ debugObj.debug.updateStatus() } } + if(this.lyrics){ + this.lyrics.clean() + } } } diff --git a/public/src/js/debug.js b/public/src/js/debug.js index ffaa253..1b2aeb2 100644 --- a/public/src/js/debug.js +++ b/public/src/js/debug.js @@ -17,6 +17,8 @@ class Debug{ this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0] this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0] this.volumeDiv = this.byClass("music-volume") + this.lyricsHideDiv = this.byClass("lyrics-hide") + this.lyricsOffsetDiv = this.byClass("lyrics-offset") this.restartLabel = this.byClass("change-restart-label") this.restartCheckbox = this.byClass("change-restart") this.autoplayLabel = this.byClass("autoplay-label") @@ -50,6 +52,9 @@ class Debug{ this.volumeSlider.onchange(this.volumeChange.bind(this)) this.volumeSlider.set(1) + this.lyricsSlider = new InputSlider(this.lyricsOffsetDiv, -60, 60, 3) + this.lyricsSlider.onchange(this.lyricsChange.bind(this)) + this.moveTo(100, 100) this.restore() this.updateStatus() @@ -129,6 +134,9 @@ class Debug{ if(this.controller.parsedSongData.branches){ this.branchHideDiv.style.display = "block" } + if(this.controller.lyrics){ + this.lyricsHideDiv.style.display = "block" + } var selectedSong = this.controller.selectedSong this.defaultOffset = selectedSong.offset || 0 @@ -136,11 +144,13 @@ class Debug{ this.offsetChange(this.offsetSlider.get(), true) this.branchChange(null, true) this.volumeChange(this.volumeSlider.get(), true) + this.lyricsChange(this.lyricsSlider.get()) }else{ this.songHash = selectedSong.hash this.offsetSlider.set(this.defaultOffset) this.branchReset(null, true) this.volumeSlider.set(this.controller.volume) + this.lyricsSlider.set(this.controller.lyrics ? this.controller.lyrics.vttOffset / 1000 : 0) } var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => { @@ -174,6 +184,7 @@ class Debug{ this.restartBtn.style.display = "" this.autoplayLabel.style.display = "" this.branchHideDiv.style.display = "" + this.lyricsHideDiv.style.display = "" this.controller = null } this.stopMove() @@ -194,6 +205,9 @@ class Debug{ branch.ms = branch.originalMS + offset }) } + if(this.controller.lyrics){ + this.controller.lyrics.offsetChange(value * 1000) + } if(this.restartCheckbox.checked && !noRestart){ this.restartSong() } @@ -213,6 +227,14 @@ class Debug{ this.restartSong() } } + lyricsChange(value, noRestart){ + if(this.controller && this.controller.lyrics){ + this.controller.lyrics.offsetChange(undefined, value * 1000) + } + if(this.restartCheckbox.checked && !noRestart){ + this.restartSong() + } + } restartSong(){ if(this.controller){ this.controller.restartSong() @@ -259,6 +281,7 @@ class Debug{ this.offsetSlider.clean() this.measureNumSlider.clean() this.volumeSlider.clean() + this.lyricsSlider.clean() pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol) pageEvents.mouseRemove(this) @@ -285,6 +308,8 @@ class Debug{ delete this.branchSelect delete this.branchResetBtn delete this.volumeDiv + delete this.lyricsHideDiv + delete this.lyricsOffsetDiv delete this.restartCheckbox delete this.autoplayLabel delete this.autoplayCheckbox diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index 769d64e..a3e2485 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -142,6 +142,12 @@ class LoadSong{ this.addPromise(loader.ajax(url).then(data => { this.songData = data.replace(/\0/g, "").split("\n") }), url) + if(song.lyrics && !songObj.lyricsData){ + var url = this.getSongDir(song) + "main.vtt" + this.addPromise(loader.ajax(url).then(data => { + songObj.lyricsData = data + }), url) + } } if(this.touchEnabled && !assets.image["touch_drum"]){ let img = document.createElement("img") @@ -262,8 +268,11 @@ class LoadSong{ randInt(min, max){ return Math.floor(Math.random() * (max - min + 1)) + min } + getSongDir(selectedSong){ + return gameConfig.songs_baseurl + selectedSong.folder + "/" + } getSongPath(selectedSong){ - var directory = gameConfig.songs_baseurl + selectedSong.folder + "/" + var directory = this.getSongDir(selectedSong) if(selectedSong.type === "tja"){ return directory + "main.tja" }else{ diff --git a/public/src/js/lyrics.js b/public/src/js/lyrics.js new file mode 100644 index 0000000..122efa8 --- /dev/null +++ b/public/src/js/lyrics.js @@ -0,0 +1,231 @@ +class Lyrics{ + constructor(file, songOffset, div){ + this.div = div + this.stroke = document.createElement("div") + this.stroke.classList.add("stroke") + div.appendChild(this.stroke) + this.fill = document.createElement("div") + this.fill.classList.add("fill") + div.appendChild(this.fill) + this.current = 0 + this.shown = -1 + this.songOffset = songOffset || 0 + this.vttOffset = 0 + this.rLinebreak = /\n|\r\n/ + this.lines = this.parseFile(file) + this.length = this.lines.length + } + parseFile(file){ + var lines = [] + var commands = file.split(/\n\n|\r\n\r\n/) + var arrow = " --> " + for(var i in commands){ + var matches = commands[i].match(this.rLinebreak) + if(matches){ + var cmd = commands[i].slice(0, matches.index) + var value = commands[i].slice(matches.index + 1) + }else{ + var cmd = commands[i] + var value = "" + } + if(cmd.startsWith("WEBVTT")){ + var nameValue = cmd.slice(7).split(";") + for(var j in nameValue){ + var [name, value] = nameValue[j].split(":") + if(name.trim().toLowerCase() === "offset"){ + this.vttOffset = (parseFloat(value.trim()) || 0) * 1000 + } + } + }else{ + var time = null + var index = cmd.indexOf(arrow) + if(index !== -1){ + time = cmd + }else{ + var matches = value.match(rLinebreak) + if(matches){ + var value1 = value.slice(0, matches.index) + index = value1.indexOf(arrow) + if(index !== -1){ + time = value1 + value = value.slice(index) + } + } + } + if(time !== null){ + var start = time.slice(0, index) + var end = time.slice(index + arrow.length) + var index = end.indexOf(" ") + if(index !== -1){ + end = end.slice(0, index) + } + var text = value.trim() + var textLang = "" + var firstLang = -1 + var index2 = -1 + while(true){ + var index1 = text.indexOf("", index1 + 6) + if(index2 === -1){ + break + } + var lang = text.slice(index1 + 6, index2).toLowerCase() + if(strings.id === lang){ + var index3 = text.indexOf("= this.length){ + return + } + ms += this.songOffset + this.vttOffset + var currentLine = this.lines[this.current] + while(currentLine && ms > currentLine.end){ + currentLine = this.lines[++this.current] + } + if(this.shown !== this.current){ + if(currentLine && ms >= currentLine.start){ + this.setText(this.lines[this.current].text) + this.shown = this.current + }else if(this.shown !== -1){ + this.setText("") + this.shown = -1 + } + } + } + setText(text){ + this.stroke.innerHTML = this.fill.innerHTML = "" + var hasRuby = false + while(text){ + var matches = text.match(this.rLinebreak) + var index1 = matches ? matches.index : -1 + var index2 = text.indexOf("") + if(index1 !== -1 && (index2 === -1 || index2 > index1)){ + this.textNode(text.slice(0, index1)) + this.linebreakNode() + text = text.slice(index1 + matches[0].length) + }else if(index2 !== -1){ + hasRuby = true + this.textNode(text.slice(0, index2)) + text = text.slice(index2 + 6) + var index = text.indexOf("") + if(index !== -1){ + var ruby = text.slice(0, index) + text = text.slice(index + 7) + }else{ + var ruby = text + text = "" + } + var index = ruby.indexOf("") + if(index !== -1){ + var node1 = ruby.slice(0, index) + ruby = ruby.slice(index + 4) + var index = ruby.indexOf("") + if(index !== -1){ + var node2 = ruby.slice(0, index) + }else{ + var node2 = ruby + } + }else{ + var node1 = ruby + var node2 = "" + } + this.rubyNode(node1, node2) + }else{ + this.textNode(text) + break + } + } + } + insertNode(func){ + this.stroke.appendChild(func()) + this.fill.appendChild(func()) + } + textNode(text){ + this.insertNode(() => document.createTextNode(text)) + } + linebreakNode(){ + this.insertNode(() => document.createElement("br")) + } + rubyNode(node1, node2){ + this.insertNode(() => { + var ruby = document.createElement("ruby") + var rt = document.createElement("rt") + ruby.appendChild(document.createTextNode(node1)) + rt.appendChild(document.createTextNode(node2)) + ruby.appendChild(rt) + return ruby + }) + } + setScale(ratio){ + this.div.style.setProperty("--scale", ratio) + } + offsetChange(songOffset, vttOffset){ + if(typeof songOffset !== "undefined"){ + this.songOffset = songOffset + } + if(typeof vttOffset !== "undefined"){ + this.vttOffset = vttOffset + } + this.setText("") + this.current = 0 + this.shown = -1 + } + clean(){ + if(this.shown !== -1){ + this.setText("") + } + delete this.div + delete this.stroke + delete this.fill + delete this.lines + } +} diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 4704ccd..bf95241 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -127,7 +127,8 @@ class SongSelect{ maker: song.maker, canJump: true, hash: song.hash || song.title, - order: song.order + order: song.order, + lyrics: song.lyrics }) } this.songs.sort((a, b) => { @@ -802,7 +803,8 @@ class SongSelect{ "offset": selectedSong.offset, "songSkin": selectedSong.songSkin, "stars": selectedSong.courses[diff].stars, - "hash": selectedSong.hash + "hash": selectedSong.hash, + "lyrics": selectedSong.lyrics }, autoplay, multiplayer, touch) } toOptions(moveBy){ diff --git a/public/src/js/view.js b/public/src/js/view.js index ba7ece8..4326379 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -247,6 +247,9 @@ } this.fillComboCache() this.setDonBgHeight() + if(this.controller.lyrics){ + this.controller.lyrics.setScale(ratio / this.pixelRatio) + } resized = true }else if(this.controller.game.paused && !document.hasFocus()){ return @@ -283,6 +286,10 @@ this.setDonBgHeight() } + if(this.controller.lyrics){ + this.controller.lyrics.update(ms) + } + ctx.save() ctx.translate(0, frameTop) diff --git a/public/src/views/debug.html b/public/src/views/debug.html index 864e18f..14d6062 100644 --- a/public/src/views/debug.html +++ b/public/src/views/debug.html @@ -24,6 +24,12 @@
x-+
+
+
Lyrics offset:
+
+ x-+ +
+
diff --git a/public/src/views/game.html b/public/src/views/game.html index c9e3837..60c7908 100644 --- a/public/src/views/game.html +++ b/public/src/views/game.html @@ -8,6 +8,7 @@
+