From d7900ca083561ecc3b6922de68878a256cb13f2c Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Sat, 27 Oct 2018 21:35:04 +0300 Subject: [PATCH] SongSelect: Selectable text, assets cleanup, and bug fixes --- public/src/css/main.css | 27 +++++- public/src/js/canvasdraw.js | 136 +++++++++++++++++++++++++++---- public/src/js/controller.js | 13 ++- public/src/js/parseosu.js | 64 ++++++++++----- public/src/js/parsetja.js | 9 ++ public/src/js/scoresheet.js | 63 +++++++------- public/src/js/songselect.js | 49 +++++++++-- public/src/js/view.js | 11 ++- public/src/views/songselect.html | 1 + 9 files changed, 296 insertions(+), 77 deletions(-) diff --git a/public/src/css/main.css b/public/src/css/main.css index c188c95..ed8a4cb 100644 --- a/public/src/css/main.css +++ b/public/src/css/main.css @@ -4,7 +4,7 @@ } @font-face{ font-family: Kozuka; - src: url("/assets/fonts/KozGoPro-Bold.otf") format("truetype"); + src: url("/assets/fonts/Kozuka.otf") format("truetype"); } html, body{ @@ -23,6 +23,7 @@ body{ margin: 0; padding: 0; background: #fe7839 url("/assets/img/bg-pattern-1.png") top center; + background-size: 30vh; font-family: TnT, Meiryo, sans-serif; } #assets{ @@ -265,6 +266,30 @@ kbd{ height: 12.5vmin; opacity: 0.5; } +#song-sel-selectable{ + position: absolute; + opacity: 1; + text-align: center; + word-break: break-all; + white-space: pre-wrap; + user-select: all; + cursor: text; + color: transparent; +} +#song-sel-selectable:focus{ + background: #ffdb2c; + color: #000; +} +#song-sel-selectable .stroke-sub{ + position: absolute; + z-index: 1; +} +#song-sel-selectable .stroke-sub::before{ + -webkit-text-stroke: 0; +} +#song-sel-selectable:focus .stroke-sub::before{ + -webkit-text-stroke: 0.25em #fff; +} #version { position: fixed; diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index 7d4ebe7..4ea8c5a 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -53,9 +53,11 @@ brackets: /[\((\))「」『』]/, tilde: /[\--~~]/, tall: /[bbddffh-lh-ltt0-90-9♪]/, - uppercase: /[A-ZA-Z!!]/, + uppercase: /[A-ZA-Z]/, lowercase: /[a-za-z・]/, - latin: /[A-ZA-Z!!a-za-z・]/, + latin: /[A-ZA-Za-za-z・]/, + exclamation: /[!!\?? ]/, + question: /[\??]/, smallHiragana: /[ぁぃぅぇぉっゃゅょァィゥェォッャュョ]/, hiragana: /[\u3040-\u30ff]/, todo: /[トド]/, @@ -63,7 +65,7 @@ em: /[mwmw]/, emCap: /[MWMW]/, rWidth: /[abdfIjo-rtvabdfIjo-rtv]/, - lWidth: /[ilil!!]/, + lWidth: /[ilil]/, uppercaseDigit: /[A-ZA-Z0-90-9]/ } @@ -266,13 +268,16 @@ var drawn = [] var r = this.regex - for(let symbol of string){ + var previousSymbol = "" + + for(var i = 0; i < string.length; i++){ + let symbol = string[i] if(symbol === " "){ // Space drawn.push({text: symbol, x: 0, y: 0, h: 18}) }else if(symbol === "ー"){ // Long-vowel mark - drawn.push({svg: this.longVowelMark, x: -4, y: 5, h: 33, scale: [mul, mul]}) + drawn.push({realText: symbol, svg: this.longVowelMark, x: -4, y: 5, h: 33, scale: [mul, mul]}) }else if(r.comma.test(symbol)){ // Comma, full stop drawn.push({text: symbol, x: 16, y: -7, h: 0, scale: [1.2, 0.7]}) @@ -281,16 +286,13 @@ drawn.push({text: symbol, x: 16, y: -16, h: 18}) }else if(r.apostrophe.test(symbol)){ // Apostrophe - drawn.push({text: ",", x: 20, y: -39, h: 0, scale: [1.2, 0.7]}) + drawn.push({realText: symbol, text: ",", x: 20, y: -39, h: 0, scale: [1.2, 0.7]}) }else if(r.brackets.test(symbol)){ // Rotated brackets drawn.push({text: symbol, x: 0, y: -5, h: 25, rotate: true}) }else if(r.tilde.test(symbol)){ // Rotated hyphen, tilde - if(symbol === "~"){ - symbol = "~" - } - drawn.push({text: symbol, x: 0, y: 2, h: 35, rotate: true}) + drawn.push({realText: symbol, text: symbol === "~" ? "~" : symbol, x: 0, y: 2, h: 35, rotate: true}) }else if(r.tall.test(symbol)){ // Tall latin script lowercase, numbers drawn.push({text: symbol, x: 0, y: 4, h: 34, scale: [1.05, 0.9]}) @@ -300,6 +302,46 @@ }else if(r.lowercase.test(symbol)){ // Latin script lower case drawn.push({text: symbol, x: 0, y: -1, h: 28, scale: [1.05, 0.9]}) + }else if(r.exclamation.test(symbol)){ + // Exclamation mark + var toDraw = [symbol] + for(var repeat = 1; repeat - 1 < i; repeat++){ + if(!r.exclamation.test(string[i - repeat])){ + break + } + toDraw.push(string[i - repeat]) + } + if(repeat > 1){ + drawn.splice(i - repeat + 1, repeat) + var allExclamations = !toDraw.find(a => a !== "!") + + for(var j = 1; j < repeat + 1; j++){ + var text = string[i - repeat + j] + if(allExclamations){ + var y = 18 + var h = 61 + }else{ + var y = 8 + var h = 37 + } + if(i === repeat - 1){ + h -= y - 4 + y = 4 + } + + drawn.push({ + text: text, + x: ((j - 1) - (repeat - 1) / 2) * 15, + y: y - (j === 1 ? 0 : h), + h: j === 1 ? h : 0, + scale: r.question.test(text) ? [0.6, 0.95] : false + }) + } + }else{ + drawn.push({text: symbol, x: 0, y: 8, h: 37, + scale: r.question.test(symbol) ? [0.7, 0.95] : false + }) + } }else if(r.smallHiragana.test(symbol)){ // Small hiragana, small katakana drawn.push({text: symbol, x: 0, y: -8, h: 25, right: true}) @@ -323,8 +365,25 @@ ctx.save() ctx.translate(config.x, config.y) + if(config.selectable){ + config.selectable.innerHTML = "" + var scale = config.selectableScale + var style = config.selectable.style + style.left = (config.x - config.width / 2) * scale + "px" + style.top = config.y * scale + "px" + style.width = config.width * scale + "px" + style.height = (drawnHeight+15) * scale + "px" + style.fontSize = 40 * mul * scale + "px" + style.transform = "" + } + if(config.height && drawnHeight > config.height){ - ctx.scale(1, config.height / drawnHeight) + var scaling = config.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" + } } var actions = [] @@ -334,6 +393,9 @@ if(config.fill){ actions.push("fill") } + if(config.selectable){ + actions.push("selectable") + } for(let action of actions){ ctx.font = config.fontSize + "px " + config.fontFamily ctx.textBaseline = "top" @@ -354,6 +416,34 @@ currentX += 20 * mul } var currentY = offsetY + symbol.y * mul + offsetY += symbol.h * mul + if(action === "selectable"){ + let div = document.createElement("div") + div.classList.add("stroke-sub") + let text = symbol.realText || symbol.text + let textWidth = ctx.measureText(text).width + let transform = [] + if(symbol.scale){ + transform.push("scale(" + symbol.scale[0] + "," + symbol.scale[1] + ")") + } + if(symbol.rotate || symbol.realText === "ー"){ + transform.push("rotate(90deg)") + } + if(transform.length){ + div.style.transform = transform.join(" ") + } + if(symbol.right){ + currentX = currentX + config.width / 2 - textWidth + }else{ + currentX = currentX + config.width / 2 - textWidth / 2 + } + div.style.left = currentX * scale + "px" + div.style.top = currentY * scale + "px" + div.appendChild(document.createTextNode(text)) + div.setAttribute("alt", text) + config.selectable.appendChild(div) + continue + } if(symbol.rotate || symbol.scale || symbol.svg){ saved = true ctx.save() @@ -381,7 +471,6 @@ } ctx[action + "Text"](symbol.text, currentX, currentY) } - offsetY += symbol.h * mul if(saved){ ctx.restore() } @@ -396,13 +485,12 @@ ctx.save() var string = config.text.split("") - if(config.align === "right"){ - string.reverse() - } var drawn = [] var r = this.regex - for(let symbol of string){ + for(var i = 0; i < string.length; i++){ + let symbol = string[i] + if(symbol === "-"){ drawn.push({text: symbol, x: -4, y: 0, w: 28, scale: [0.8, 1]}) }else if(symbol === "™"){ @@ -411,6 +499,8 @@ drawn.push({text: symbol, x: 0, y: 0, w: 10}) }else if(symbol === "'"){ drawn.push({text: ",", x: 0, y: -15, w: 7, scale: [1, 0.7]}) + }else if(symbol === "?"){ + drawn.push({text: symbol, x: 0, y: -1, w: 12, scale: [0.7, 0.95]}) }else if(r.en.test(symbol)){ // n-width drawn.push({text: symbol, x: 0, y: 0, w: 28, scale: [1, 0.95]}) @@ -429,6 +519,16 @@ }else if(r.uppercaseDigit.test(symbol)){ // Latin script uppercase, digits drawn.push({text: symbol, x: 0, y: -2, w: 32}) + }else if(r.exclamation.test(symbol)){ + // Exclamation mark + var nextExclamation = string[i + 1] ? r.exclamation.test(string[i + 1]) : false + drawn.push({ + text: symbol, + x: nextExclamation ? 4 : -1, + y: 0, + w: nextExclamation ? 16 : 28, + scale: symbol === "?" ? [0.7, 0.95] : false + }) }else if(r.smallHiragana.test(symbol)){ // Small hiragana, small katakana drawn.push({text: symbol, x: 0, y: 0, w: 30}) @@ -440,6 +540,10 @@ } } + if(config.align === "right"){ + drawn.reverse() + } + var drawnWidth = 0 for(let symbol of drawn){ if(config.letterSpacing){ diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 51b61a3..08d2bfa 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -26,6 +26,8 @@ class Controller{ this.view = new View(this, backgroundURL, this.selectedSong.title, this.selectedSong.difficulty) this.mekadon = new Mekadon(this, this.game) this.keyboard = new Keyboard(this) + + this.playedSounds = {} } run(syncWith){ this.loadUIEvents() @@ -34,8 +36,8 @@ class Controller{ this.startMainLoop() if(syncWith){ syncWith.run() - syncWith.elapsedTime = this.game.elapsedTime - syncWith.startDate = this.game.startDate + syncWith.game.elapsedTime = this.game.elapsedTime + syncWith.game.startDate = this.game.startDate this.syncWith = syncWith } if(!this.multiplayer){ @@ -76,6 +78,7 @@ class Controller{ requestAnimationFrame(() => { if(this.syncWith){ this.syncWith.game.elapsedTime = this.game.elapsedTime + this.syncWith.game.startDate = this.game.startDate } this.mainLoop() if(this.syncWith){ @@ -161,7 +164,11 @@ class Controller{ } } playSound(id, time){ - assets.sounds[id + this.snd].play(time) + var ms = (+new Date) + (time || 0) * 1000 + if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){ + assets.sounds[id + this.snd].play(time) + this.playedSounds[id] = ms + } } playSoundMeka(soundID, time){ var meka = "" diff --git a/public/src/js/parseosu.js b/public/src/js/parseosu.js index c4b6280..c0c54a4 100644 --- a/public/src/js/parseosu.js +++ b/public/src/js/parseosu.js @@ -109,15 +109,15 @@ class ParseOsu{ var timingPoints = [] var indexes = this.getStartEndIndexes("TimingPoints") var lastBeatInterval = parseInt(this.data[indexes.start].split(",")[1]) - for(var i = indexes.start; i<= indexes.end; i++){ + for(var i = indexes.start; i <= indexes.end; i++){ var values = this.data[i].split(",") var start = parseInt(values[this.osu.OFFSET]) var msOrPercent = parseFloat(values[this.osu.MSPERBEAT]) if(i == indexes.start){ - start = 0 this.beatInfo.beatInterval = msOrPercent this.beatInfo.bpm = Math.floor(1000 / this.beatInfo.beatInterval * 60) } + var beatReset = false if(msOrPercent < 0){ var sliderMultiplier = this.difficulty.lastMultiplier / Math.abs(msOrPercent / 100) }else{ @@ -126,35 +126,61 @@ class ParseOsu{ this.difficulty.originalMultiplier = sliderMultiplier } this.difficulty.lastMultiplier = sliderMultiplier + beatReset = true } timingPoints.push({ start: start + this.offset, sliderMultiplier: sliderMultiplier, measure: parseInt(values[this.osu.METER]), gogoTime: parseInt(values[this.osu.KIAIMODE]), - beatMS: 1000 / this.difficulty.lastMultiplier + beatMS: 1000 / this.difficulty.lastMultiplier, + beatReset: beatReset }) } return timingPoints } parseMeasures(){ var measures = [] - var measureNumber = 0 - for(var i = 0; i= interval){ + start -= interval + } + } + for(var ms = start; ms <= limit; ms += interval){ + + var speed = currentTiming.sliderMultiplier + for(var j = 0; j < this.timingPoints.length; j++){ + var timingPoint = this.timingPoints[j] + if(j !== 0 && timingPoint.start - this.offset > ms){ + break + } + speed = timingPoint.sliderMultiplier + } + + measures.push({ + ms: ms, + originalMS: ms, + speed: speed + }) } } return measures @@ -253,7 +279,7 @@ class ParseOsu{ for(var j = 0; j < this.timingPoints.length; j++){ var timingPoint = this.timingPoints[j] - if(timingPoint.start - this.offset > start){ + if(j !== 0 && timingPoint.start - this.offset > start){ break } speed = timingPoint.sliderMultiplier diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js index 1874c82..8e9220e 100644 --- a/public/src/js/parsetja.js +++ b/public/src/js/parsetja.js @@ -300,6 +300,15 @@ scroll: scroll } if(lastDrumroll){ + if(symbol === "9"){ + currentMeasure.push({ + endDrumroll: lastDrumroll, + bpm: bpm, + scroll: scroll + }) + lastDrumroll = false + break + } circleObj.endDrumroll = lastDrumroll } if(symbol === "7" || symbol === "9"){ diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index 18169c0..af27f1a 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -139,6 +139,9 @@ class Scoresheet{ } }else{ ctx.scale(ratio, ratio) + if(!this.canvasCache.canvas){ + this.canvasCache.resize(winW / ratio, 80 + 1, ratio) + } } this.winW = winW this.winH = winH @@ -406,36 +409,38 @@ class Scoresheet{ if(elapsed >= 800){ ctx.save() - ctx.translate(frameLeft, frameTop) - - ctx.globalAlpha = Math.min(1, (elapsed - 800) / 500) - - for(var p = 0; p < players; p++){ - var results = this.results - if(p === 1){ - results = p2.results - ctx.translate(0, p2Offset) + ctx.setTransform(1, 0, 0, 1, 0, 0) + this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => { + ctx.scale(ratio, ratio) + ctx.translate(frameLeft, frameTop) + + for(var p = 0; p < players; p++){ + var results = this.results + if(p === 1){ + results = p2.results + ctx.translate(0, p2Offset) + } + var gaugePercent = Math.round(results.gauge / 2) / 50 + var w = 712 + this.draw.gauge({ + ctx: ctx, + x: 558 + w, + y: 116, + clear: 25 / 50, + percentage: gaugePercent, + font: this.font, + scale: w / 788, + scoresheet: true + }) + this.draw.soul({ + ctx: ctx, + x: 1215, + y: 144, + scale: 36 / 42, + cleared: gaugePercent - 1 / 50 >= 25 / 50 + }) } - var gaugePercent = Math.round(results.gauge / 2) / 50 - var w = 712 - this.draw.gauge({ - ctx: ctx, - x: 558 + w, - y: 116, - clear: 25 / 50, - percentage: gaugePercent, - font: this.font, - scale: w / 788, - scoresheet: true - }) - this.draw.soul({ - ctx: ctx, - x: 1215, - y: 144, - scale: 36 / 42, - cleared: gaugePercent - 1 / 50 >= 25 / 50 - }) - } + }) ctx.restore() } diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index b3d290d..f0dc2b6 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -242,17 +242,21 @@ class SongSelect{ this.startP2() - this.redrawRunning = true - this.redrawBind = this.redraw.bind(this) - this.redraw() pageEvents.keyAdd(this, "all", "down", this.keyDown.bind(this)) - pageEvents.add(this.canvas, "mousemove", this.mouseMove.bind(this)) - pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this)) + pageEvents.add(window, "mousemove", this.mouseMove.bind(this)) + pageEvents.add(window, ["mousedown", "touchstart"], this.mouseDown.bind(this)) if(touchEnabled && fullScreenSupported){ this.touchFullBtn = document.getElementById("touch-full-btn") this.touchFullBtn.style.display = "block" pageEvents.add(this.touchFullBtn, "touchend", toggleFullscreen) } + + this.selectable = document.getElementById("song-sel-selectable") + this.selectableText = "" + + this.redrawRunning = true + this.redrawBind = this.redraw.bind(this) + this.redraw() } keyDown(event, code){ @@ -321,6 +325,15 @@ class SongSelect{ } mouseDown(event){ + if(event.target === this.selectable || event.target.parentNode === this.selectable){ + this.selectable.focus() + }else{ + getSelection().removeAllRanges() + this.selectable.blur() + } + if(event.target !== this.canvas){ + return + } if(event.type === "mousedown"){ if(event.which !== 1){ return @@ -643,6 +656,8 @@ class SongSelect{ this.categoryCache.resize(280, (this.songAsset.marginTop + 1) * categories , ratio + 0.5) this.difficultyCache.resize((44 + 56 + 2) * 5, 135 + 10, ratio + 0.5) + + this.selectableText = "" }else if(!document.hasFocus()){ this.pointer(false) return @@ -1223,9 +1238,30 @@ class SongSelect{ fontFamily: this.font }) }) + if(!songSel && this.selectableText !== currentSong.title){ + this.draw.verticalText({ + ctx: ctx, + text: currentSong.title, + x: x + textX + textW / 2, + y: y + textY, + width: textW, + height: textH - 35, + fontSize: 40, + fontFamily: this.font, + selectable: this.selectable, + selectableScale: this.ratio / this.pixelRatio + }) + this.selectable.style.display = "" + this.selectableText = currentSong.title + } } }) + if(screen !== "difficulty" && this.selectableText){ + this.selectableText = "" + this.selectable.style.display = "none" + } + if(songSelMoving){ this.draw.highlight({ ctx: ctx, @@ -1423,11 +1459,12 @@ class SongSelect{ this.redrawRunning = false this.endPreview() pageEvents.keyRemove(this, "all") - pageEvents.remove(this.canvas, ["mousemove", "mousedown", "touchstart"]) + pageEvents.remove(window, ["mousemove", "mousedown", "touchstart"]) if(this.touchEnabled && fullScreenSupported){ pageEvents.remove(this.touchFullBtn, "click") delete this.touchFullBtn } + delete this.selectable delete this.ctx delete this.canvas } diff --git a/public/src/js/view.js b/public/src/js/view.js index 37a1906..b34d7bc 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -49,11 +49,16 @@ if(this.controller.touchEnabled){ this.touchDrumDiv = document.getElementById("touch-drum") this.touchDrumImg = document.getElementById("touch-drum-img") + + if(this.controller.autoPlayEnabled){ + this.touchDrumDiv.style.display = "none" + }else{ + pageEvents.add(this.canvas, "touchstart", this.ontouch.bind(this)) + } + this.gameDiv.classList.add("touch-visible") document.getElementById("version").classList.add("version-hide") - pageEvents.add(this.canvas, "touchstart", this.ontouch.bind(this)) - this.touchFullBtn = document.getElementById("touch-full-btn") pageEvents.add(this.touchFullBtn, "touchend", toggleFullscreen) if(!fullScreenSupported){ @@ -1036,7 +1041,7 @@ ctx.strokeText(text, textX, textY) ctx.fillText(text, textX, textY) - if(drumroll){ + if(drumroll === 2){ ctx.strokeText(longText[1], textX + endX, textY) ctx.fillText(longText[1], textX + endX, textY) } diff --git a/public/src/views/songselect.html b/public/src/views/songselect.html index 4eec0d9..8f2395d 100644 --- a/public/src/views/songselect.html +++ b/public/src/views/songselect.html @@ -1,4 +1,5 @@
+