SongSelect: Selectable text, assets cleanup, and bug fixes

This commit is contained in:
LoveEevee 2018-10-27 21:35:04 +03:00
parent 0d1444de0c
commit d7900ca083
9 changed files with 296 additions and 77 deletions

View File

@ -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;

View File

@ -53,9 +53,11 @@
brackets: /[\(\))「」『』]/,
tilde: /[\-~]/,
tall: /[bdfh-l-t0-9-9♪]/,
uppercase: /[A-Z-!]/,
uppercase: /[A-Z-]/,
lowercase: /[a-z-z・]/,
latin: /[A-Z-!a-z-z・]/,
latin: /[A-Z-a-z-z・]/,
exclamation: /[!\? ]/,
question: /[\?]/,
smallHiragana: /[ぁぃぅぇぉっゃゅょァィゥェォッャュョ]/,
hiragana: /[\u3040-\u30ff]/,
todo: /[トド]/,
@ -63,7 +65,7 @@
em: /[mw]/,
emCap: /[MW]/,
rWidth: /[abdfIjo-rtv-]/,
lWidth: /[il!]/,
lWidth: /[il]/,
uppercaseDigit: /[A-Z-0-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){

View File

@ -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){
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 = ""

View File

@ -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,36 +126,62 @@ 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<this.timingPoints.length; i++){
if(this.timingPoints[i + 1]){
var limit = this.timingPoints[i + 1].start - this.offset
}else{
var limit = this.circles[this.circles.length - 1].getMS() - this.offset
for(var i = 0; i < this.timingPoints.length; i++){
var currentTiming = this.timingPoints[i]
var firstTiming = i === 0
var limit = this.circles[this.circles.length - 1].endTime + currentTiming.beatMS
for(var j = i + 1; j < this.timingPoints.length; j++){
var nextTiming = this.timingPoints[j]
var newLimit = nextTiming.start
if(nextTiming.measure !== currentTiming.measure || nextTiming.beatReset){
limit = newLimit - currentTiming.beatMS
break
}
for(var start = this.timingPoints[i].start; start <= limit; start += this.beatInfo.beatInterval){
if(measureNumber === 0){
i = j
}
var start = currentTiming.start
var interval = currentTiming.beatMS * currentTiming.measure
if(firstTiming){
while(start >= 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: start + this.offset,
originalMS: start + this.offset,
speed: this.timingPoints[i].sliderMultiplier
ms: ms,
originalMS: ms,
speed: speed
})
}
measureNumber = (measureNumber + 1) % (this.timingPoints[i].measure + 1)
}
}
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

View File

@ -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"){

View File

@ -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,10 +409,11 @@ class Scoresheet{
if(elapsed >= 800){
ctx.save()
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)
ctx.globalAlpha = Math.min(1, (elapsed - 800) / 500)
for(var p = 0; p < players; p++){
var results = this.results
if(p === 1){
@ -436,6 +440,7 @@ class Scoresheet{
cleared: gaugePercent - 1 / 50 >= 25 / 50
})
}
})
ctx.restore()
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -1,4 +1,5 @@
<div id="song-select">
<canvas id="song-sel-canvas"></canvas>
<div id="song-sel-selectable" tabindex="1"></div>
<img id="touch-full-btn" src="/assets/img/touch_fullscreen.png">
</div>