diff --git a/public/src/css/main.css b/public/src/css/main.css index 83e4d42..131c5b0 100644 --- a/public/src/css/main.css +++ b/public/src/css/main.css @@ -19,7 +19,8 @@ body{ background-size: 30vh; font-family: TnT, Meiryo, sans-serif; } -#assets{ +#assets, +#browse{ display: none; } .window{ diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index 56fb806..4ba5813 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -51,8 +51,8 @@ ideographicComma: /[、。]/, apostrophe: /['']/, degree: /[゚°]/, - brackets: /[\((\))「」『』]/, - tilde: /[\--~~〜]/, + brackets: /[\((\))\[\]「」『』【】]/, + tilde: /[\--~~〜_]/, tall: /[bbddffgghhj-lj-ltt♪]/, i: /[ii]/, uppercase: /[A-ZA-Z]/, @@ -68,7 +68,8 @@ em: /[mwmw]/, emCap: /[MWMW]/, rWidth: /[abdfIjo-rtvabdfIjo-rtv]/, - lWidth: /[ilil]/ + lWidth: /[ilil]/, + ura: /\s*[\((]裏[\))]$/ } var numbersFull = "0123456789" @@ -276,13 +277,18 @@ var ctx = config.ctx var inputText = config.text var mul = config.fontSize / 40 + var ura = false + var r = this.regex + + var matches = inputText.match(r.ura) + if(matches){ + inputText = inputText.slice(0, matches.index) + ura = matches[0] + } var string = inputText.split("") var drawn = [] - var r = this.regex - var previousSymbol = "" - for(var i = 0; i < string.length; i++){ let symbol = string[i] if(symbol === " "){ @@ -297,6 +303,8 @@ drawn.push({text: symbol, x: 0, y: 12, h: 45}) }else if(symbol === "."){ drawn.push({realText: symbol, text: ".", x: 13, y: -7, h: 15, scale: [1.2, 0.7]}) + }else if(symbol === "…"){ + drawn.push({text: symbol, x: 0, y: 5, h: 25, rotate: true}) }else if(r.comma.test(symbol)){ // Comma, full stop drawn.push({text: symbol, x: 13, y: -7, h: 15, scale: [1.2, 0.7]}) @@ -408,22 +416,28 @@ } var scaling = 1 - if(config.height && drawnHeight > config.height){ + var height = config.height - (ura ? 52 * mul : 0) + if(height && drawnHeight > height){ if(config.align === "bottom"){ - scaling = Math.max(0.6, config.height / drawnHeight) + scaling = Math.max(0.6, height / drawnHeight) ctx.translate(40 * mul, 0) - ctx.scale(scaling, config.height / drawnHeight) + ctx.scale(scaling, height / drawnHeight) ctx.translate(-40 * mul, 0) }else{ - scaling = config.height / drawnHeight + scaling = height / drawnHeight ctx.scale(1, scaling) } if(config.selectable){ style.transform = "scale(1, " + scaling + ")" - style.top = (config.y + (config.height - drawnHeight) / 2 - 15 / 2 * scaling) * scale + "px" + style.top = (config.y + (height - drawnHeight) / 2 - 15 / 2 * scaling) * scale + "px" } } + if(ura){ + // Circled ura + drawn.push({realText: ura, text: "裏", x: 0, y: 18, h: 52, ura: true, scale: [1, 1 / scale]}) + } + var actions = [] if(config.outline){ actions.push("stroke") @@ -492,7 +506,7 @@ config.selectable.appendChild(div) continue } - if(symbol.rotate || symbol.scale || symbol.svg){ + if(symbol.rotate || symbol.scale || symbol.svg || symbol.ura){ saved = true ctx.save() @@ -517,7 +531,23 @@ }else{ ctx.textAlign = "center" } - ctx[action + "Text"](symbol.text, currentX, currentY) + if(symbol.ura){ + ctx.font = (30 * mul) + "px Meiryo, sans-serif" + ctx.textBaseline = "center" + ctx.beginPath() + ctx.arc(currentX, currentY + (21.5 * mul), (18 * mul), 0, Math.PI * 2) + if(action === "stroke"){ + ctx.fillStyle = config.outline + ctx.fill() + }else if(action === "fill"){ + ctx.strokeStyle = config.fill + ctx.lineWidth = 2.5 * mul + ctx.fillText(symbol.text, currentX, currentY) + } + ctx.stroke() + }else{ + ctx[action + "Text"](symbol.text, currentX, currentY) + } } if(saved){ ctx.restore() diff --git a/public/src/js/loader.js b/public/src/js/loader.js index ac0e7d1..07efdf1 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -78,7 +78,8 @@ class Loader{ }) this.promises.push(this.ajax("/api/songs").then(songs => { - assets.songs = JSON.parse(songs) + assets.songsDefault = JSON.parse(songs) + assets.songs = assets.songsDefault })) assets.views.forEach(name => { diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index 6b0eae2..5317bbb 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -66,16 +66,17 @@ class loadSong{ } promises.push(this.loadSongBg(id)) + var songObj = assets.songs.find(song => song.id === id) + promises.push(new Promise((resolve, reject) => { - var songObj - assets.songs.forEach(song => { - if(song.id == id){ - songObj = song - } - }) if(songObj.sound){ songObj.sound.gain = snd.musicGain resolve() + }else if(songObj.music){ + snd.musicGain.load(songObj.music, true).then(sound => { + songObj.sound = sound + resolve() + }, reject) }else{ snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => { songObj.sound = sound @@ -83,9 +84,13 @@ class loadSong{ }, reject) } })) - promises.push(loader.ajax(this.getSongPath(song)).then(data => { - this.songData = data.replace(/\0/g, "").split("\n") - })) + if(songObj.chart){ + this.songData = songObj.chart + }else{ + promises.push(loader.ajax(this.getSongPath(song)).then(data => { + this.songData = data.replace(/\0/g, "").split("\n") + })) + } Promise.all(promises).then(() => { this.setupMultiplayer() }, error => { diff --git a/public/src/js/parseosu.js b/public/src/js/parseosu.js index c0c54a4..d56791c 100644 --- a/public/src/js/parseosu.js +++ b/public/src/js/parseosu.js @@ -1,5 +1,5 @@ class ParseOsu{ - constructor(fileContent, offset){ + constructor(fileContent, offset, metaOnly){ this.osu = { OFFSET: 0, MSPERBEAT: 1, @@ -52,9 +52,11 @@ class ParseOsu{ this.metadata = this.parseMetadata() this.editor = this.parseEditor() this.difficulty = this.parseDifficulty() - this.timingPoints = this.parseTiming() - this.circles = this.parseCircles() - this.measures = this.parseMeasures() + if(!metaOnly){ + this.timingPoints = this.parseTiming() + this.circles = this.parseCircles() + this.measures = this.parseMeasures() + } } getStartEndIndexes(type){ var indexes = { @@ -186,40 +188,20 @@ class ParseOsu{ return measures } parseGeneralInfo(){ - var generalInfo = { - audioFilename: "", - audioWait: 0 - } + var generalInfo = {} var indexes = this.getStartEndIndexes("General") for(var i = indexes.start; i<= indexes.end; i++){ var [item, key] = this.data[i].split(":") - switch(item){ - case "SliderMultiple": - generalInfo.audioFilename = key - break - case "AudioWait": - generalInfo.audioWait = parseInt(key) - break - } + generalInfo[item] = key.trim() } return generalInfo } parseMetadata(){ - var metadata = { - title: "", - artist: "" - } + var metadata = {} var indexes = this.getStartEndIndexes("Metadata") for(var i = indexes.start; i <= indexes.end; i++){ var [item, key] = this.data[i].split(":") - switch(item){ - case "TitleUnicode": - metadata.title = key - break - case "ArtistUnicode": - metadata.artist = key - break - } + metadata[item] = key.trim() } return metadata } diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js index f739578..5cd1a8f 100644 --- a/public/src/js/parsetja.js +++ b/public/src/js/parsetja.js @@ -1,5 +1,5 @@ class ParseTja{ - constructor(file, difficulty, offset){ + constructor(file, difficulty, offset, metaOnly){ this.data = [] for(let line of file){ line = line.replace(/\/\/.*/, "").trim() @@ -34,10 +34,12 @@ this.metadata = this.parseMetadata() this.measures = [] this.beatInfo = {} - this.circles = this.parseCircles() + if(!metaOnly){ + this.circles = this.parseCircles() + } } parseMetadata(){ - var metaNumbers = ["bpm", "offset"] + var metaNumbers = ["bpm", "offset", "demostart", "level"] var inSong = false var courses = {} var currentCourse = {} diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 7242b7a..056e55a 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -35,6 +35,12 @@ class SongSelect{ border: ["#dff0ff", "#6890b2"], outline: "#217abb" }, + "browse": { + sort: 7, + background: "#9791ff", + border: ["#e2dfff", "#6d68b2"], + outline: "#5350ba" + }, "J-POP": { sort: 0, background: "#219fbb", @@ -98,7 +104,8 @@ class SongSelect{ preview: song.preview || 0, type: song.type, offset: song.offset, - songSkin: song.song_skin || {} + songSkin: song.song_skin || {}, + music: song.music }) } this.songs.sort((a, b) => { @@ -139,6 +146,17 @@ class SongSelect{ action: "about", category: "ランダム" }) + if("webkitdirectory" in HTMLInputElement.prototype && !(/Android|iPhone|iPad/.test(navigator.userAgent))){ + this.browse = document.getElementById("browse") + pageEvents.add(this.browse, "change", this.browseChange.bind(this)) + + this.songs.push({ + title: assets.customSongs ? "デフォルト曲リスト" : "参照する…", + skin: this.songSkin.browse, + action: "browse", + category: "ランダム" + }) + } this.songs.push({ title: "もどる", skin: this.songSkin.back, @@ -204,8 +222,10 @@ class SongSelect{ this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial) this.playBgm(true) }else{ - if((!p2.session || fadeIn) && "selectedSong" in localStorage){ - this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length) + if(assets.customSongs){ + this.selectedSong = assets.customSelected + }else if((!p2.session || fadeIn) && "selectedSong" in localStorage){ + this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1) } assets.sounds["song-select"].play() snd.musicGain.fadeOut() @@ -265,6 +285,7 @@ class SongSelect{ this.state.moveHover = null }) pageEvents.add(loader.screen, ["mousedown", "touchstart"], this.mouseDown.bind(this)) + pageEvents.add(this.canvas, "touchend", this.touchEnd.bind(this)) if(touchEnabled && fullScreenSupported){ this.touchFullBtn = document.getElementById("touch-full-btn") this.touchFullBtn.style.display = "block" @@ -404,6 +425,19 @@ class SongSelect{ } } } + touchEnd(event){ + event.preventDefault() + if(this.state.screen === "song"){ + var currentSong = this.songs[this.selectedSong] + if(currentSong.action === "browse"){ + var mouse = this.mouseOffset(event.changedTouches[0].pageX, event.changedTouches[0].pageY) + var moveBy = this.songSelMouse(mouse.x, mouse.y) + if(moveBy === 0){ + this.toBrowse() + } + } + } + } mouseMove(event){ var mouse = this.mouseOffset(event.offsetX, event.offsetY) var moveTo = null @@ -521,6 +555,119 @@ class SongSelect{ assets.sounds["ka"].play() } } + + browseChange(event){ + var files = event.target.files + var promises = [] + var tjaFiles = [] + var osuFiles = [] + var otherFiles = {} + + for(var i = 0; i < files.length; i++){ + var file = files[i] + var name = file.name.toLowerCase() + if(name.endsWith(".tja")){ + tjaFiles.push([file, i]) + }else if(name.endsWith(".osu")){ + osuFiles.push([file, i]) + }else{ + otherFiles[file.webkitRelativePath.toLowerCase()] = file + } + } + var songs = [] + var courseTypes = {"easy": 0, "normal": 1, "hard": 2, "oni": 3, "ura": 4} + for(var i = 0; i < tjaFiles.length; i++){ + let file = tjaFiles[i][0] + let index = tjaFiles[i][1] + var reader = new FileReader() + promises.push(pageEvents.load(reader).then(event => { + var data = event.target.result.replace(/\0/g, "").split("\n") + var tja = new ParseTja(data, "oni", 0, true) + var songObj = { + id: index + 1, + type: "tja", + chart: data, + stars: [] + } + var dir = file.webkitRelativePath.toLowerCase() + dir = dir.slice(0, dir.lastIndexOf("/") + 1) + for(var diff in tja.metadata){ + var meta = tja.metadata[diff] + songObj.title = songObj.title_en = meta.title || file.name.slice(0, file.name.lastIndexOf(".")) + var subtitle = meta.subtitle || "" + if(subtitle.startsWith("--")){ + subtitle = subtitle.slice(2) + } + songObj.subtitle = songObj.subtitle_en = subtitle + songObj.preview = meta.demostart ? Math.floor(meta.demostart * 1000) : 0 + if(meta.level){ + songObj.stars[courseTypes[diff]] = meta.level + } + if(meta.wave){ + songObj.music = otherFiles[dir + meta.wave.toLowerCase()] + } + } + if(songObj.music && songObj.stars.filter(star => star).length !== 0){ + songs[index] = songObj + } + })) + reader.readAsText(file, "sjis") + } + for(var i = 0; i < osuFiles.length; i++){ + let file = osuFiles[i][0] + let index = osuFiles[i][1] + var reader = new FileReader() + promises.push(pageEvents.load(reader).then(event => { + var data = event.target.result.replace(/\0/g, "").split("\n") + var osu = new ParseOsu(data, 0, true) + var dir = file.webkitRelativePath.toLowerCase() + dir = dir.slice(0, dir.lastIndexOf("/") + 1) + var songObj = { + id: index + 1, + type: "osu", + chart: data, + subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist, + subtitle_en: osu.metadata.Artist || osu.metadata.ArtistUnicode, + preview: osu.generalInfo.PreviewTime, + stars: [null, null, null, parseInt(osu.difficulty.overallDifficulty) || 1], + music: otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] + } + var filename = file.name.slice(0, file.name.lastIndexOf(".")) + var title = osu.metadata.TitleUnicode || osu.metadata.Title + if(title){ + var suffix = "" + var matches = filename.match(/\[.+?\]$/) + if(matches){ + suffix = " " + matches[0] + } + songObj.title = title + suffix + songObj.title_en = (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix + }else{ + songObj.title = filename + } + if(songObj.music){ + songs[index] = songObj + } + }).catch(() => {})) + reader.readAsText(file) + } + Promise.all(promises).then(() => { + songs = songs.filter(song => typeof song !== "undefined") + if(songs.length){ + assets.songs = songs + assets.customSongs = true + assets.customSelected = 0 + assets.sounds["don"].play() + this.clean() + setTimeout(() => { + new SongSelect("browse", false, this.touchEnabled) + }, 500) + }else{ + this.browse.parentNode.reset() + } + }) + } + toSelectDifficulty(fromP2){ var currentSong = this.songs[this.selectedSong] if(p2.session && !fromP2 && currentSong.action !== "random"){ @@ -564,6 +711,8 @@ class SongSelect{ this.toTutorial() }else if(currentSong.action === "about"){ this.toAbout() + }else if(currentSong.action === "browse"){ + this.toBrowse() } } this.pointer(false) @@ -593,7 +742,11 @@ class SongSelect{ assets.sounds["don"].play() try{ - localStorage["selectedSong"] = this.selectedSong + if(assets.customSongs){ + assets.customSelected = this.selectedSong + }else{ + localStorage["selectedSong"] = this.selectedSong + } localStorage["selectedDiff"] = difficulty + this.diffOptions.length }catch(e){} @@ -670,6 +823,19 @@ class SongSelect{ }, 500) } } + toBrowse(){ + if(assets.customSongs){ + assets.customSongs = false + assets.songs = assets.songsDefault + assets.sounds["don"].play() + this.clean() + setTimeout(() => { + new SongSelect("browse", false, this.touchEnabled) + }, 500) + }else{ + this.browse.click() + } + } redraw(){ if(!this.redrawRunning){ @@ -1636,10 +1802,16 @@ class SongSelect{ return snd.previewGain.load(gameConfig.songs_baseurl + id + previewFilename) } - songObj.preview_time = 0 - loadPreview(previewFilename).catch(() => { - songObj.preview_time = prvTime - return loadPreview("/main.mp3") + new Promise((resolve, reject) => { + if(currentSong.music){ + snd.previewGain.load(currentSong.music, true).then(resolve, reject) + }else{ + songObj.preview_time = 0 + loadPreview(previewFilename).catch(() => { + songObj.preview_time = prvTime + return loadPreview("/main.mp3") + }).then(resolve, reject) + } }).then(sound => { if(currentId === this.previewId){ songObj.preview_sound = sound @@ -1799,11 +1971,14 @@ class SongSelect{ }) pageEvents.keyRemove(this, "all") pageEvents.remove(loader.screen, ["mousemove", "mouseleave", "mousedown", "touchstart"]) + pageEvents.remove(this.canvas, "touchend") pageEvents.remove(p2, "message") if(this.touchEnabled && fullScreenSupported){ pageEvents.remove(this.touchFullBtn, "click") delete this.touchFullBtn } + pageEvents.remove(this.browse, "change") + delete this.browse delete this.selectable delete this.ctx delete this.canvas diff --git a/public/src/js/soundbuffer.js b/public/src/js/soundbuffer.js index 50b6106..80f0c42 100644 --- a/public/src/js/soundbuffer.js +++ b/public/src/js/soundbuffer.js @@ -4,10 +4,19 @@ this.context = new AudioContext() pageEvents.add(window, ["click", "touchend"], this.pageClicked.bind(this)) } - load(url, gain){ - return loader.ajax(url, request => { - request.responseType = "arraybuffer" - }).then(response => { + load(url, local, gain){ + if(local){ + var reader = new FileReader() + var loadPromise = pageEvents.load(reader).then(event => { + return event.target.result + }) + reader.readAsArrayBuffer(url) + }else{ + var loadPromise = loader.ajax(url, request => { + request.responseType = "arraybuffer" + }) + } + return loadPromise.then(response => { return new Promise((resolve, reject) => { return this.context.decodeAudioData(response, resolve, reject) }).catch(error => { @@ -66,8 +75,8 @@ class SoundGain{ } this.setVolume(1) } - load(url){ - return this.soundBuffer.load(url, this) + load(url, local){ + return this.soundBuffer.load(url, local, this) } convertTime(time, absolute){ return this.soundBuffer.convertTime(time, absolute) diff --git a/public/src/views/songselect.html b/public/src/views/songselect.html index 0c47814..d63c409 100644 --- a/public/src/views/songselect.html +++ b/public/src/views/songselect.html @@ -2,4 +2,5 @@
+