japanese-drum-game/public/src/js/canvasdraw.js
LoveEevee 0221c977c8 SongSelect: Add crowns
- Improve the soul gauge to fill properly. The algorithm is different for each difficulty.
- Saves score to localStorage, the whole score is correct now.
- Adds crowns to song selection screen.

The scores would take a lot of space if stored as readable objects so they are stored compressed. If you need to edit your scores, you can do so by opening dev console and entering `scoreStorage.get()`. Expand to the song and double click on values that you need to edit. When you are done editing, do not forget to save your scores with `scoreStorage.save()`. Adding new scores can be done with `scoreStorage.add`, first get a template with `obj=scoreStorage.template(),obj` and after editing, add it with `scoreStorage.add("song name", "oni", obj)`. To remove a score use `scoreStorage.remove("song name"[, "oni"])`.
2020-03-06 20:57:01 +03:00

1587 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class CanvasDraw{
constructor(noSmoothing){
this.diffStarPath = new Path2D(vectors.diffStar)
this.longVowelMark = new Path2D(vectors.longVowelMark)
this.diffIconPath = [[{w: 40, h: 33}, {
fill: "#ff2803",
d: new Path2D(vectors.diffEasy1)
}, {
fill: "#ffb910",
noStroke: true,
d: new Path2D(vectors.diffEasy2)
}], [{w: 48, h: 31}, {
fill: "#8daf51",
d: new Path2D(vectors.diffNormal)
}], [{w: 56, h: 37}, {
fill: "#784439",
d: new Path2D(vectors.diffHard1)
}, {
fill: "#000",
noStroke: true,
d: new Path2D(vectors.diffHard2)
}, {
fill: "#414b2b",
d: new Path2D(vectors.diffHard3)
}], [{w: 29, h: 27}, {
fill: "#db1885",
d: new Path2D(vectors.diffOni1)
}, {
fill: "#fff",
d: new Path2D(vectors.diffOni2)
}]]
this.diffPath = {
good: new Path2D(vectors.good),
ok: new Path2D(vectors.ok),
bad: new Path2D(vectors.bad)
}
this.crownPath = new Path2D(vectors.crown)
this.soulPath = new Path2D(vectors.soul)
this.optionsPath = {
main: new Path2D(vectors.options),
shadow: new Path2D(vectors.optionsShadow)
}
this.categoryPath = {
main: new Path2D(vectors.category),
shadow: new Path2D(vectors.categoryShadow),
highlight: new Path2D(vectors.categoryHighlight)
}
this.regex = {
comma: /[,.]/,
ideographicComma: /[、。]/,
apostrophe: /[']/,
degree: /[゚°]/,
brackets: /[\(\)\[\]「」『』【】:;]/,
tilde: /[\-~〜_]/,
tall: /[bdfghj-l-t♪]/,
i: /[i]/,
uppercase: /[A-Z-]/,
lowercase: /[a-z-z・]/,
latin: /[A-Z-a-z-z・]/,
numbers: /[0-9-]/,
exclamation: /[!\? ]/,
question: /[\?]/,
smallHiragana: /[ぁぃぅぇぉっゃゅょァィゥェォッャュョ]/,
hiragana: /[\u3040-\u30ff]/,
todo: /[トド]/,
en: /[ceghknsuxyz]/,
em: /[mw]/,
emCap: /[MW]/,
rWidth: /[abdfIjo-rtv-]/,
lWidth: /[il]/,
ura: /\s*[\(]裏[\)]$/,
cjk: /[\u3040-ゞ゠-ヾ一-\u9ffe]/
}
var numbersFull = ""
var numbersHalf = "0123456789"
this.numbersFullToHalf = {}
for(var i = 0; i < 10; i++){
this.numbersFullToHalf[numbersFull[i]] = numbersHalf[i]
this.numbersFullToHalf[numbersHalf[i]] = numbersHalf[i]
}
this.wrapOn = [" ", "\n", "%s"]
this.stickySymbols = "!,.:;?~‐–‼、。々〜ぁぃぅぇぉっゃゅょァィゥェォッャュョ・ーヽヾ!:;?"
this.songFrameCache = new CanvasCache(noSmoothing)
this.diffStarCache = new CanvasCache(noSmoothing)
this.crownCache = new CanvasCache(noSmoothing)
this.tmpCanvas = document.createElement("canvas")
this.tmpCtx = this.tmpCanvas.getContext("2d")
}
roundedRect(config){
var ctx = config.ctx
var x = config.x
var y = config.y
var w = config.w
var h = config.h
var r = config.radius
ctx.beginPath()
this.roundedCorner(ctx, x, y, r, 0)
this.roundedCorner(ctx, x + w, y, r, 1)
this.roundedCorner(ctx, x + w, y + h, r, 2)
this.roundedCorner(ctx, x, y + h, r, 3)
ctx.closePath()
}
roundedCorner(ctx, x, y, r, rotation){
var pi = Math.PI
switch(rotation){
case 0:
return ctx.arc(x + r, y + r, r, pi, pi / -2)
case 1:
return ctx.arc(x - r, y + r, r, pi / -2, 0)
case 2:
return ctx.arc(x - r, y - r, r, 0, pi / 2)
case 3:
return ctx.arc(x + r, y - r, r, pi / 2, pi)
}
}
songFrame(config){
var ctx = config.ctx
var x = config.x
var y = config.y
var w = config.width
var h = config.height
var border = config.border
var innerBorder = config.innerBorder
var allBorders = border + innerBorder
var innerX = x + allBorders
var innerY = y + allBorders
var innerW = w - allBorders * 2
var innerH = h - allBorders * 2
ctx.save()
var shadowBg = (ctx, noForce) => {
this.shadow({
ctx: ctx,
fill: "rgba(0, 0, 0, 0.5)",
blur: 10,
x: 5,
y: 5,
force: !noForce
})
ctx.fillStyle = "#000"
ctx.fillRect(0, 0, w, h)
}
if(config.cached){
if(this.songFrameCache.w !== config.frameCache.w){
this.songFrameCache.resize(config.frameCache.w, config.frameCache.h, config.frameCache.ratio)
}
this.songFrameCache.get({
ctx: ctx,
x: x,
y: y,
w: w + 15,
h: h + 15,
id: "shadow" + config.cached
}, shadowBg)
}else{
ctx.translate(x, y)
shadowBg(ctx, true)
}
ctx.restore()
ctx.save()
{
let _x = x + border
let _y = y + border
let _w = w - border * 2
let _h = h - border * 2
ctx.fillStyle = config.borderStyle[1]
ctx.fillRect(_x, _y, _w, _h)
ctx.fillStyle = config.borderStyle[0]
ctx.beginPath()
ctx.moveTo(_x, _y)
ctx.lineTo(_x + _w, _y)
ctx.lineTo(_x + _w - innerBorder, _y + innerBorder)
ctx.lineTo(_x + innerBorder, _y + _h - innerBorder)
ctx.lineTo(_x, _y + _h)
ctx.fill()
}
ctx.fillStyle = config.background
ctx.fillRect(innerX, innerY, innerW, innerH)
ctx.save()
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)"
ctx.lineWidth = 3
ctx.strokeRect(innerX, innerY, innerW, innerH)
if(!config.noCrop){
ctx.beginPath()
ctx.rect(innerX, innerY, innerW, innerH)
ctx.clip()
}
config.innerContent(innerX, innerY, innerW, innerH)
ctx.restore()
if(config.disabled){
ctx.fillStyle = "rgba(0, 0, 0, 0.5)"
ctx.fillRect(x, y, w, h)
}
if(config.highlight){
this.highlight({
ctx: ctx,
x: x,
y: y,
w: w,
h: h,
animate: config.highlight === 2,
animateMS: config.animateMS,
opacity: config.highlight === 1 ? 0.8 : 1
})
}
ctx.restore()
}
highlight(config){
var ctx = config.ctx
ctx.save()
if(config.shape){
ctx.translate(config.x, config.y)
}else{
var _x = config.x + 3.5
var _y = config.y + 3.5
var _w = config.w - 7
var _h = config.h - 7
}
if(config.animate){
ctx.globalAlpha = this.fade((this.getMS() - config.animateMS) % 2000 / 2000)
}else if(config.opacity){
ctx.globalAlpha = config.opacity
}
if(config.radius){
this.roundedRect({
ctx: ctx,
x: _x,
y: _y,
w: _w,
h: _h,
radius: config.radius
})
}else if(!config.shape){
ctx.beginPath()
ctx.rect(_x, _y, _w, _h)
}
if(config.shape){
var stroke = () => ctx.stroke(config.shape)
}else{
var stroke = () => ctx.stroke()
}
var size = config.size || 14
ctx.strokeStyle = "rgba(255, 249, 1, 0.45)"
ctx.lineWidth = size
stroke()
ctx.strokeStyle = "rgba(255, 249, 1, .8)"
ctx.lineWidth = 8 / 14 * size
stroke()
ctx.strokeStyle = "#fff"
ctx.lineWidth = 6 / 14 * size
stroke()
ctx.restore()
}
fade(pos){
if(pos < 0.5){
pos = 1 - pos
}
return (1 - Math.cos(Math.PI * pos * 2)) / 2
}
easeIn(pos){
return 1 - Math.cos(Math.PI / 2 * pos)
}
easeOut(pos){
return Math.sin(Math.PI / 2 * pos)
}
easeOutBack(pos){
return Math.sin(Math.PI / 1.74 * pos) * 1.03
}
easeInOut(pos){
return (Math.cos(Math.PI * pos) - 1) / -2
}
verticalText(config){
var ctx = config.ctx
var inputText = config.text.toString()
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 bold = this.bold(config.fontFamily)
var string = inputText.split("")
var drawn = []
var quoteOpened = false
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
if(bold){
drawn.push({text: symbol, x: -1, y: -1, h: 33, rotate: true})
}else{
drawn.push({realText: symbol, svg: this.longVowelMark, x: -4, y: 5, h: 33, scale: [mul, mul]})
}
}else if(symbol === "∀"){
drawn.push({text: symbol, x: 0, y: 0, h: 39, rotate: true})
}else if(symbol === "↓"){
drawn.push({text: symbol, x: 0, y: 12, h: 45})
}else if(symbol === ""){
if(bold){
drawn.push({realText: symbol, text: ".", x: 13, y: -15, h: 15})
}else{
drawn.push({realText: symbol, text: ".", x: 13, y: -7, h: 15, scale: [1.2, 0.7]})
}
}else if(symbol === "…"){
drawn.push({text: symbol, x: bold ? 9 : 0, y: 5, h: 25, rotate: true})
}else if(symbol === '"'){
if(quoteOpened){
drawn.push({realText: symbol, text: "“", x: -25, y: 10, h: 20})
}else{
drawn.push({realText: symbol, text: "”", x: 12, y: 15, h: 20})
}
quoteOpened = !quoteOpened
}else if(r.comma.test(symbol)){
// Comma, full stop
if(bold){
drawn.push({text: symbol, x: 13, y: -15, h: 15})
}else{
drawn.push({text: symbol, x: 13, y: -7, h: 15, scale: [1.2, 0.7]})
}
}else if(r.ideographicComma.test(symbol)){
// Ideographic comma, full stop
drawn.push({text: symbol, x: 16, y: -16, h: 18})
}else if(r.apostrophe.test(symbol)){
// Apostrophe
if(bold){
drawn.push({text: symbol, x: 20, y: -25, h: 0})
}else{
drawn.push({realText: symbol, text: ",", x: 20, y: -39, h: 0, scale: [1.2, 0.7]})
}
}else if(r.degree.test(symbol)){
// Degree
if(bold){
drawn.push({text: symbol, x: 16, y: 9, h: 25})
}else{
drawn.push({text: symbol, x: 16, y: 3, h: 18})
}
}else if(r.brackets.test(symbol)){
// Rotated brackets
if(bold){
drawn.push({text: symbol, x: 0, y: 0, h: 35, rotate: true})
}else{
drawn.push({text: symbol, x: 0, y: -5, h: 25, rotate: true})
}
}else if(r.tilde.test(symbol)){
// Rotated hyphen, tilde
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
drawn.push({text: symbol, x: 0, y: 4, h: 34})
}else if(r.i.test(symbol)){
// Lowercase i
drawn.push({text: symbol, x: 0, y: 7, h: 34})
}else if(r.uppercase.test(symbol)){
// Latin script upper case
drawn.push({text: symbol, x: 0, y: 8, h: 37})
}else if(r.lowercase.test(symbol)){
// Latin script lower case
drawn.push({text: symbol, x: 0, y: -1, h: 28})
}else if(r.numbers.test(symbol)){
// Numbers
var number = this.numbersFullToHalf[symbol]
drawn.push({realText: symbol, text: number, x: 0, y: 4, h: 34})
}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
}
var addX = bold && (text === "" || text === "") ? 10 : 0
drawn.push({
text: text,
x: ((j - 1) - (repeat - 1) / 2) * 15 + addX,
y: y - (j === 1 ? 0 : h),
h: j === 1 ? h : 0
})
}
}else{
var addX = bold && (symbol === "" || symbol === "") ? 10 : 0
drawn.push({text: symbol, x: addX, y: 8, h: 37})
}
}else if(r.smallHiragana.test(symbol)){
// Small hiragana, small katakana
drawn.push({text: symbol, x: 0, y: -8, h: 25, right: true})
}else if(r.hiragana.test(symbol)){
// Hiragana, katakana
drawn.push({text: symbol, x: 0, y: 5, h: 38, right: r.todo.test(symbol)})
}else{
// Kanji, other
drawn.push({text: symbol, x: 0, y: 3, h: 39})
}
}
var drawnHeight = 0
for(let symbol of drawn){
if(config.letterSpacing){
symbol.h += config.letterSpacing
}
drawnHeight += symbol.h * mul
}
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 = ""
}
var scaling = 1
var strokeScaling = 1
var height = config.height - (ura ? 52 * mul : 0)
if(height && drawnHeight > height){
scaling = height / drawnHeight
if(config.align === "bottom"){
strokeScaling = Math.max(0.6, height / drawnHeight)
ctx.translate(40 * mul, 0)
ctx.scale(strokeScaling, scaling)
ctx.translate(-40 * mul, 0)
}else{
strokeScaling = scaling
ctx.scale(1, scaling)
}
if(config.selectable){
style.transform = "scale(1, " + scaling + ")"
style.top = (config.y + (height - drawnHeight) / 2 - 15 / 2 * scaling) * scale + "px"
}
}
if(ura){
// Circled ura
drawn.push({realText: ura, text: "裏", x: 0, y: 25, h: 52, ura: true, scale: [1, 1 / scaling]})
}
if(config.align === "bottom"){
drawn.reverse()
}
var actions = []
if(config.outline){
actions.push("stroke")
}
if(config.fill){
actions.push("fill")
}
if(config.selectable){
actions.push("selectable")
}
for(let action of actions){
ctx.font = bold + config.fontSize + "px " + config.fontFamily
ctx.textBaseline = "top"
if(action === "stroke"){
ctx.strokeStyle = config.outline
ctx.lineWidth = config.outlineSize * mul
if(config.align === "bottom"){
ctx.lineWidth /= strokeScaling
}
ctx.lineJoin = "round"
ctx.miterLimit = 1
}else if(action === "fill"){
ctx.fillStyle = config.fill
}
if(config.align === "bottom"){
var offsetY = drawnHeight > config.height ? drawnHeight : config.height
}else{
var offsetY = 0
}
for(let symbol of drawn){
var saved = false
var currentX = symbol.x
if(symbol.right){
currentX += 20 * mul
}
var currentY = offsetY + symbol.y * mul
if(config.align === "bottom"){
currentY -= symbol.h * mul
}
offsetY = offsetY + symbol.h * mul * (config.align === "bottom" ? -1 : 1)
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
}
if(symbol.ura){
div.style.font = (30 / (40 * mul)) + "em Meiryo, sans-serif"
}
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 || symbol.ura){
saved = true
ctx.save()
if(symbol.rotate){
ctx.translate(currentX + 20 * mul, currentY + 20 * mul)
ctx.rotate(Math.PI / 2)
}else{
ctx.translate(currentX, currentY)
}
if(symbol.scale){
ctx.scale(symbol.scale[0], symbol.scale[1])
ctx.lineWidth = ctx.lineWidth / symbol.scale[0]
}
currentX = 0
currentY = 0
}
if(symbol.svg){
ctx[action](symbol.svg)
}else{
if(symbol.right){
ctx.textAlign = "right"
}else{
ctx.textAlign = "center"
}
if(symbol.ura){
ctx.font = (30 * mul) + "px Meiryo, sans-serif"
ctx.textBaseline = "middle"
ctx.beginPath()
ctx.arc(currentX, currentY + (17 * 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 + (17 * mul))
}
ctx.stroke()
}else{
ctx[action + "Text"](symbol.text, currentX, currentY)
}
}
if(saved){
ctx.restore()
}
}
}
ctx.restore()
}
layeredText(config, layers){
var ctx = config.ctx
var inputText = config.text.toString()
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 bold = this.bold(config.fontFamily)
var string = inputText.split("")
var drawn = []
for(var i = 0; i < string.length; i++){
let symbol = string[i]
if(symbol === "-"){
drawn.push({text: symbol, x: -2, y: 0, w: 28})
}else if(symbol === "™"){
drawn.push({text: symbol, x: -2, y: 0, w: 20, scale: [0.6, 0.5]})
}else if(symbol === " "){
drawn.push({text: symbol, x: 0, y: 0, w: 10})
}else if(symbol === '"'){
drawn.push({text: symbol, x: 2, y: 0, w: 10})
}else if(symbol === "∀"){
if(bold){
drawn.push({text: symbol, x: 0, y: 0, w: 40})
}else{
drawn.push({text: symbol, x: -3, y: 0, w: 55})
}
}else if(symbol === ""){
drawn.push({text: symbol, x: -9, y: 0, w: 37})
}else if(r.apostrophe.test(symbol)){
drawn.push({text: ",", x: 0, y: -15, w: 7, scale: [1, 0.7]})
}else if(r.comma.test(symbol)){
// Comma, full stop
if(bold){
drawn.push({text: symbol, x: -3, y: 0, w: 13})
}else{
drawn.push({text: symbol, x: -3, y: 13, w: 13, scale: [1.2, 0.7]})
}
}else if(r.tilde.test(symbol)){
// Hyphen, tilde
drawn.push({text: symbol === "~" ? "" : symbol, x: 0, y: 0, w: 39})
}else if(r.en.test(symbol)){
// n-width
drawn.push({text: symbol, x: 0, y: 0, w: 28})
}else if(r.em.test(symbol)){
// m-width
drawn.push({text: symbol, x: 0, y: 0, w: 38})
}else if(r.rWidth.test(symbol)){
// r-width
drawn.push({text: symbol, x: 0, y: 0, w: 24})
}else if(r.lWidth.test(symbol)){
// l-width
drawn.push({text: symbol, x: 0, y: 0, w: 12})
}else if(r.emCap.test(symbol)){
// m-width uppercase
drawn.push({text: symbol, x: 0, y: 0, w: 38})
}else if(r.numbers.test(symbol)){
// Numbers
var number = this.numbersFullToHalf[symbol]
drawn.push({text: number, x: 0, y: 0, w: 32})
}else if(r.degree.test(symbol)){
// Degree
if(bold){
drawn.push({text: symbol, x: 0, y: 0, w: 20})
}else{
drawn.push({text: symbol, x: 5, y: 0, w: 0})
}
}else if(r.uppercase.test(symbol)){
// Latin script uppercase
drawn.push({text: symbol, x: 0, y: 0, 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
})
}else if(r.smallHiragana.test(symbol)){
// Small hiragana, small katakana
drawn.push({text: symbol, x: 0, y: 0, w: 30})
}else if(r.hiragana.test(symbol)){
// Hiragana, katakana
drawn.push({text: symbol, x: 0, y: 0, w: 35})
}else{
drawn.push({text: symbol, x: 0, y: 0, w: 39})
}
}
var drawnWidth = 0
for(let symbol of drawn){
if(config.letterSpacing){
symbol.w += config.letterSpacing
}
drawnWidth += symbol.w * mul
}
ctx.save()
ctx.translate(config.x, config.y)
if(config.scale){
ctx.scale(config.scale[0], config.scale[1])
}
var scaling = 1
var width = config.width - (ura ? 55 * mul : 0)
if(width && drawnWidth > width){
scaling = width / drawnWidth
ctx.scale(scaling, 1)
}
if(ura){
// Circled ura
drawn.push({text: "裏", x: 0, y: 3, w: 55, ura: true, scale: [1 / scaling, 1]})
}
if(config.align === "right"){
drawn.reverse()
}
ctx.font = bold + config.fontSize + "px " + config.fontFamily
ctx.textBaseline = config.baseline || "top"
ctx.textAlign = "center"
for(let layer of layers){
var savedLayer = false
var action = "strokeText"
if(layer.scale){
savedLayer = true
ctx.save()
ctx.scale(layer.scale[0], layer.scale[1])
}
if(layer.outline){
ctx.strokeStyle = layer.outline
ctx.lineJoin = "round"
ctx.miterLimit = 1
}
if(layer.letterBorder){
ctx.lineWidth = layer.letterBorder
}
if(layer.fill){
ctx.fillStyle = layer.fill
action = "fillText"
}
if(layer.shadow){
if(!savedLayer){
savedLayer = true
ctx.save()
}
this.shadow({
ctx: ctx,
fill: "rgba(0, 0, 0, " + (1 / (layer.shadow[3] || 2)) + ")",
blur: layer.shadow[2],
x: layer.shadow[0],
y: layer.shadow[1],
force: config.forceShadow
})
}
var offsetX = 0
for(let symbol of drawn){
var saved = false
var currentX = offsetX + symbol.x * mul + (layer.x || 0) + symbol.w * mul / 2
var currentY = symbol.y + (layer.y || 0)
if(config.align === "center"){
currentX -= drawnWidth / 2
}else if(config.align === "right"){
currentX = -offsetX + symbol.x + (layer.x || 0) - symbol.w / 2
}
if(symbol.scale || symbol.ura){
saved = true
ctx.save()
ctx.translate(currentX, currentY)
if(symbol.scale){
if(config.baseline === "middle"){
ctx.translate(0, -ctx.lineWidth * (2 / symbol.scale[1]))
}
ctx.scale(symbol.scale[0], symbol.scale[1])
ctx.lineWidth /= symbol.scale[0]
}
currentX = 0
currentY = 0
}
if(symbol.ura){
ctx.font = (30 * mul) + "px Meiryo, sans-serif"
ctx.textBaseline = "middle"
ctx.beginPath()
ctx.arc(currentX, currentY + (17 * mul), (18 * mul), 0, Math.PI * 2)
if(action === "strokeText"){
ctx.fillStyle = layer.outline
ctx.fill()
}else if(action === "fillText"){
ctx.strokeStyle = layer.fill
ctx.lineWidth = 2.5 * mul
ctx.fillText(symbol.text, currentX, currentY + (17 * mul))
}
ctx.stroke()
}else{
ctx[action](symbol.text, currentX, currentY)
}
if(saved){
ctx.restore()
}
offsetX += symbol.w * mul
}
if(savedLayer){
ctx.restore()
}
}
ctx.restore()
}
wrappingText(config){
var ctx = config.ctx
var inputText = config.text.toString()
var words = []
var start = 0
var substituteIndex = 0
while(start < inputText.length){
var character = inputText.slice(start, start + 1)
if(words.length !== 0){
var previous = words[words.length - 1]
if(!previous.substitute && previous !== "\n" && this.stickySymbols.indexOf(character) !== -1){
words[words.length - 1] += character
start++
continue
}
}
var index = Infinity
var currentIndex = inputText.slice(start).search(this.regex.cjk)
if(currentIndex !== -1){
index = start + currentIndex
var on = inputText.charAt(index)
}
for(var i = 0; i < this.wrapOn.length; i++){
var currentIndex = inputText.indexOf(this.wrapOn[i], start)
if(currentIndex !== -1 && currentIndex < index){
var on = this.wrapOn[i]
index = currentIndex
}
}
if(index === Infinity){
if(start !== inputText.length){
words.push(inputText.slice(start, inputText.length))
}
break
}
var end = index + (on === " " ? 1 : 0)
if(start !== end){
words.push(inputText.slice(start, end))
}
if(on === "%s" && config.substitute){
words.push({
substitute: true,
index: substituteIndex,
width: config.substitute(config, substituteIndex, true) || 0
})
substituteIndex++
}else if(on !== " "){
words.push(on)
}
start = index + on.length
}
ctx.save()
var bold = this.bold(config.fontFamily)
ctx.font = bold + config.fontSize + "px " + config.fontFamily
ctx.textBaseline = config.baseline || "top"
ctx.textAlign = "left"
ctx.fillStyle = config.fill
var lineHeight = config.lineHeight || config.fontSize
var x = 0
var y = 0
var totalW = 0
var totalH = 0
var line = ""
var toDraw = []
var lastWidth = 0
var addToDraw = obj => {
toDraw.push(obj)
if(x + lastWidth > totalW){
totalW = x + lastWidth
}
if(y + lineHeight > totalH){
totalH = y + lineHeight
}
}
var recenter = () => {
if(config.textAlign === "center"){
for(var j in toDraw){
if(toDraw[j].y === y){
toDraw[j].x += (config.width - x - lastWidth) / 2
}
}
}
}
for(var i in words){
var skip = words[i].substitute || words[i] === "\n"
if(!skip){
var currentWidth = ctx.measureText(line + words[i]).width
}
if(skip || (x !== 0 || line) && x + currentWidth > config.width){
if(line){
addToDraw({
text: line,
x: x, y: y
})
}
if(words[i].substitute){
line = ""
var currentWidth = words[i].width
if(x + lastWidth + currentWidth > config.width){
recenter()
x = 0
y += lineHeight
lastWidth = 0
}
addToDraw({
substitute: true,
index: words[i].index,
x: x + lastWidth, y: y
})
x += lastWidth + currentWidth
lastWidth = currentWidth
}else{
recenter()
x = 0
y += lineHeight
line = words[i] === "\n" ? "" : words[i]
lastWidth = ctx.measureText(line).width
}
}else{
line += words[i]
lastWidth = currentWidth
}
}
if(line){
addToDraw({
text: line,
x: x, y: y
})
recenter()
}
var addX = 0
var addY = 0
if(config.verticalAlign === "middle"){
addY = ((config.height || 0) - totalH) / 2
}
for(var i in toDraw){
var x = config.x + toDraw[i].x + addX
var y = config.y + toDraw[i].y + addY
if(toDraw[i].text){
ctx.fillText(toDraw[i].text, x, y)
}else if(toDraw[i].substitute){
ctx.save()
ctx.translate(x, y)
config.substitute(config, toDraw[i].index)
ctx.restore()
}
}
ctx.restore()
}
diffIcon(config){
var ctx = config.ctx
var scale = config.scale
ctx.save()
ctx.lineWidth = config.border
ctx.strokeStyle = "#000"
var icon = this.diffIconPath[config.diff === 4 ? 3 : config.diff]
ctx.translate(config.x - icon[0].w * scale / 2, config.y - icon[0].h * scale / 2)
ctx.scale(scale, scale)
for(var i = 1; i < icon.length; i++){
if(!icon[i].noStroke){
ctx.stroke(icon[i].d)
}
}
if(!config.noFill){
for(var i = 1; i < icon.length; i++){
if(config.diff === 4 && icon[i].fill === "#db1885"){
ctx.fillStyle = "#7135db"
}else{
ctx.fillStyle = icon[i].fill
}
ctx.fill(icon[i].d)
}
}
ctx.restore()
}
diffOptionsIcon(config){
var ctx = config.ctx
ctx.save()
if(config.iconName === "back"){
ctx.translate(config.x - 21, config.y - 21)
var drawLine = y => {
ctx.beginPath()
ctx.moveTo(12, y)
ctx.arc(20.5, 24, 8.5, Math.PI, Math.PI * 2, true)
ctx.lineTo(29, 18)
ctx.stroke()
}
var drawTriangle = noFill => {
ctx.beginPath()
ctx.moveTo(29, 5)
ctx.lineTo(21, 19)
ctx.lineTo(37, 19)
ctx.closePath()
if(!noFill){
ctx.fill()
}
}
ctx.strokeStyle = "#000"
ctx.lineWidth = 13
drawLine(8)
ctx.lineWidth = 6
drawTriangle(true)
ctx.stroke()
ctx.lineWidth = 7
ctx.fillStyle = "#fff"
ctx.strokeStyle = "#fff"
drawLine(11)
drawTriangle()
ctx.translate(-1.5, -0.5)
ctx.fillStyle = "#23a6e1"
ctx.strokeStyle = "#23a6e1"
ctx.globalCompositeOperation = "darken"
drawLine(11)
drawTriangle()
}else if(config.iconName === "options"){
ctx.translate(config.x, config.y)
ctx.rotate(-55 * Math.PI / 180)
ctx.translate(-6, -20)
ctx.strokeStyle = "#000"
ctx.lineWidth = 6
ctx.stroke(this.optionsPath.main)
ctx.translate(-2, 2)
ctx.stroke(this.optionsPath.main)
ctx.fillStyle = "#7e7c76"
ctx.fill(this.optionsPath.shadow)
ctx.translate(2, -2)
ctx.fillStyle = "#d9d6ce"
ctx.fill(this.optionsPath.main)
}
ctx.restore()
}
diffCursor(config){
var ctx = config.ctx
ctx.save()
if(config.scale){
ctx.translate(config.x, config.y)
ctx.scale(config.scale, config.scale)
ctx.translate(-48, -64)
}else{
ctx.translate(config.x - 48, config.y - 64)
}
ctx.fillStyle = config.two ? "#65cdcd" : "#ff411c"
ctx.strokeStyle = "#000"
ctx.lineWidth = 6
ctx.beginPath()
if(!config.side){
var textX = config.two ? 22 : 20
ctx.moveTo(48, 120)
ctx.arc(48, 48.5, 45, Math.PI * 0.58, Math.PI * 0.42)
}else if(config.two){
var textX = 72
ctx.moveTo(56, 115)
ctx.arc(98, 48.5, 45, Math.PI * 0.75, Math.PI * 0.59)
}else{
var textX = -30
ctx.moveTo(39, 115)
ctx.arc(-2, 48.5, 45, Math.PI * 0.41, Math.PI * 0.25)
}
ctx.closePath()
ctx.fill()
ctx.stroke()
this.layeredText({
ctx: ctx,
text: config.two ? "2P" : "1P",
fontSize: 43,
fontFamily: config.font,
x: textX,
y: 26,
width: 54,
letterSpacing: -4
}, [
{outline: "#fff", letterBorder: 11},
{fill: "#000"}
])
ctx.restore()
}
diffStar(config){
var ctx = config.ctx
ctx.save()
if(config.songSel || config.ura){
if(this.diffStarCache.scale !== config.ratio){
this.diffStarCache.resize(62, 31, config.ratio)
}
var offset = 30 / 2 - 18 / 2
var big = config.ura && !config.songSel
this.diffStarCache.get({
ctx: ctx,
x: config.x - 9 - offset,
y: config.y - 9 - offset,
w: 30,
h: 30,
id: big ? "big" : "small"
}, ctx => {
ctx.fillStyle = "#fff"
this.shadow({
ctx: ctx,
fill: "#fff",
blur: 10,
force: true
})
if(big){
ctx.translate(30 / 2 - 21 / 2, 30 / 2 - 19 / 2)
ctx.scale(1.1, 1.1)
}else{
ctx.translate(offset, offset)
}
ctx.fill(this.diffStarPath)
})
}else{
ctx.fillStyle = "#f72568"
ctx.translate(config.x - 10.5, config.y - 9.5)
ctx.scale(1.1, 1.1)
ctx.fill(this.diffStarPath)
}
ctx.restore()
}
pattern(config){
var ctx = config.ctx
ctx.save()
var mul = config.scale || 1
if(mul !== 1){
ctx.scale(1 / mul, 1 / mul)
}
ctx.fillStyle = ctx.createPattern(config.img, "repeat")
if(config.shape){
config.shape(ctx, mul)
}else{
ctx.beginPath()
ctx.rect(config.x * mul, config.y * mul, config.w * mul, config.h * mul)
}
ctx.translate(config.dx * mul, config.dy * mul)
ctx.fill()
ctx.restore()
}
score(config){
var ctx = config.ctx
ctx.save()
ctx.translate(config.x, config.y)
if(config.scale){
ctx.scale(config.scale, config.scale)
}
ctx.strokeStyle = "#000"
ctx.lineWidth = 7
if(strings.good === "良"){
if(config.align === "center"){
ctx.translate(config.score === "bad" ? -49 / 2 : -23 / 2, 0)
}
if(config.score === "good"){
var grd = ctx.createLinearGradient(0, 0, 0, 29)
grd.addColorStop(0.3, "#f7fb00")
grd.addColorStop(0.9, "#ff4900")
ctx.fillStyle = grd
ctx.stroke(this.diffPath.good)
ctx.fill(this.diffPath.good)
}else if(config.score === "ok"){
ctx.fillStyle = "#fff"
ctx.stroke(this.diffPath.ok)
ctx.fill(this.diffPath.ok)
}else if(config.score === "bad"){
var grd = ctx.createLinearGradient(0, 0, 0, 27)
grd.addColorStop(0.1, "#6B5DFF")
grd.addColorStop(0.7, "#00AEDE")
ctx.fillStyle = grd
ctx.stroke(this.diffPath.bad)
ctx.fill(this.diffPath.bad)
ctx.translate(26, 0)
ctx.stroke(this.diffPath.ok)
ctx.fill(this.diffPath.ok)
}
}else{
ctx.font = this.bold(strings.font) + "26px " + strings.font
if(config.results){
ctx.textAlign = "left"
}else{
ctx.textAlign = "center"
}
ctx.textBaseline = "top"
ctx.miterLimit = 1
if(config.score === "good"){
if(config.results && strings.id === "en"){
ctx.scale(0.75, 1)
}
var grd = ctx.createLinearGradient(0, 0, 0, 29)
grd.addColorStop(0.3, "#f7fb00")
grd.addColorStop(0.9, "#ff4900")
ctx.fillStyle = grd
ctx.strokeText(strings.good, 0, 4)
ctx.fillText(strings.good, 0, 4)
}else if(config.score === "ok"){
ctx.fillStyle = "#fff"
ctx.strokeText(strings.ok, 0, 4)
ctx.fillText(strings.ok, 0, 4)
}else if(config.score === "bad"){
var grd = ctx.createLinearGradient(0, 0, 0, 27)
grd.addColorStop(0.1, "#6B5DFF")
grd.addColorStop(0.7, "#00AEDE")
ctx.fillStyle = grd
ctx.strokeText(strings.bad, 0, 4)
ctx.fillText(strings.bad, 0, 4)
}
}
ctx.restore()
}
crown(config){
var ctx = config.ctx
ctx.save()
ctx.translate(config.x, config.y)
if(config.scale){
ctx.scale(config.scale, config.scale)
}
ctx.translate(-47, -39)
ctx.miterLimit = 1.7
if(config.whiteOutline){
if(!this.crownCache.w){
this.crownCache.resize(140, 140, config.ratio)
}
var offset = 140 / 2 - 94 / 2
this.crownCache.get({
ctx: ctx,
x: -offset,
y: -offset,
w: 140,
h: 140,
id: "crown"
}, ctx => {
ctx.save()
ctx.translate(offset, offset)
ctx.strokeStyle = "#fff"
ctx.lineWidth = 35
ctx.miterLimit = 1.7
ctx.filter = "blur(1.5px)"
ctx.stroke(this.crownPath)
ctx.restore()
})
}
if(config.shine){
ctx.strokeStyle = "#fff"
ctx.lineWidth = 18
ctx.stroke(this.crownPath)
ctx.globalAlpha = 1 - config.shine
}
ctx.strokeStyle = config.type ? "#000" : "rgba(255, 193, 0, 0.5)"
ctx.lineWidth = 18
ctx.stroke(this.crownPath)
if(config.shine){
ctx.globalAlpha = 1
ctx.fillStyle = "#fff"
ctx.fill(this.crownPath)
ctx.globalAlpha = 1 - config.shine
}
if(config.type){
var grd = ctx.createLinearGradient(0, 0, 94, 0)
if(config.type === "gold"){
grd.addColorStop(0, "#ffffc5")
grd.addColorStop(0.23, "#ffff44")
grd.addColorStop(0.53, "#efbd12")
grd.addColorStop(0.83, "#ffff44")
grd.addColorStop(1, "#efbd12")
}else if(config.type === "silver"){
grd.addColorStop(0, "#d6efef")
grd.addColorStop(0.23, "#bddfde")
grd.addColorStop(0.53, "#97c1c0")
grd.addColorStop(0.83, "#bddfde")
grd.addColorStop(1, "#97c1c0")
}
ctx.fillStyle = grd
}else{
ctx.fillStyle = "#ffdb2c"
}
ctx.fill(this.crownPath)
ctx.restore()
}
gauge(config){
var ctx = config.ctx
ctx.save()
ctx.translate(config.x, config.y)
if(config.scale){
ctx.scale(config.scale, config.scale)
}
ctx.translate(-788, 0)
var firstTop = config.multiplayer ? 0 : 30
var secondTop = config.multiplayer ? 0 : 8
config.percentage = Math.max(0, Math.min(1, config.percentage))
var cleared = config.percentage >= config.clear
var gaugeW = 14 * 50
var gaugeClear = gaugeW * (config.clear - 1 / 50)
var gaugeFilled = gaugeW * config.percentage
ctx.fillStyle = "#000"
ctx.beginPath()
if(config.scoresheet){
ctx.moveTo(-4, 26)
ctx.lineTo(gaugeClear - 4, 26)
this.roundedCorner(ctx, gaugeClear - 4, 4, 13, 0)
this.roundedCorner(ctx, 760, 4, 13, 1)
ctx.lineTo(760, 56)
ctx.lineTo(-4, 56)
}else if(config.multiplayer){
ctx.moveTo(gaugeClear - 7, 27)
ctx.lineTo(788, 27)
ctx.lineTo(788, 52)
this.roundedCorner(ctx, gaugeClear - 7, 52, 18, 3)
}else{
ctx.moveTo(gaugeClear - 7, 24)
this.roundedCorner(ctx, gaugeClear - 7, 0, 18, 0)
ctx.lineTo(788, 0)
ctx.lineTo(788, 24)
}
ctx.fill()
if(gaugeFilled <= gaugeClear){
ctx.fillStyle = config.blue ? "#184d55" : "#680000"
var x = Math.max(0, gaugeFilled - 5)
ctx.fillRect(x, firstTop, gaugeClear - x + 2, 22)
}
if(gaugeFilled > 0){
var w = Math.min(gaugeClear + 1, gaugeFilled - 4)
ctx.fillStyle = config.blue ? "#00edff" : "#ff3408"
ctx.fillRect(0, firstTop + 2, w, 20)
ctx.fillStyle = config.blue ? "#9cffff" : "#ffa191"
ctx.fillRect(0, firstTop, w, 3)
}
if(gaugeFilled < gaugeW - 4){
ctx.fillStyle = "#684900"
var x = Math.max(gaugeClear + 9, gaugeFilled - gaugeClear + 9)
ctx.fillRect(x, secondTop, gaugeW - 4 - x, 44)
}
if(gaugeFilled > gaugeClear + 14){
var w = Math.min(gaugeW - 4, gaugeFilled - gaugeClear - 14)
ctx.fillStyle = "#ff0"
ctx.fillRect(gaugeClear + 9, secondTop + 2, w, 42)
ctx.fillStyle = "#fff"
ctx.fillRect(gaugeClear + 9, secondTop, w, 3)
}
ctx.fillStyle = cleared ? "#ff0" : "#684900"
ctx.beginPath()
if(config.multiplayer){
this.roundedCorner(ctx, gaugeClear, secondTop + 44, 10, 3)
ctx.lineTo(gaugeClear, secondTop)
ctx.lineTo(gaugeClear + 10, secondTop)
}else{
ctx.moveTo(gaugeClear, secondTop + 44)
this.roundedCorner(ctx, gaugeClear, secondTop, 10, 0)
ctx.lineTo(gaugeClear + 10, secondTop + 44)
}
ctx.fill()
if(cleared){
ctx.save()
ctx.clip()
ctx.fillStyle = "#fff"
ctx.fillRect(gaugeClear, secondTop, 10, 3)
ctx.restore()
}
ctx.strokeStyle = "rgba(0, 0, 0, 0.16)"
ctx.beginPath()
ctx.lineWidth = 5
for(var i = 0; i < 49; i++){
var x = 14 + i * 14 - ctx.lineWidth / 2
if(i === 26){
ctx.stroke()
ctx.beginPath()
ctx.lineWidth = 4
}
ctx.moveTo(x, x < gaugeClear ? firstTop : secondTop)
ctx.lineTo(x, x < gaugeClear ? firstTop + 22 : secondTop + 44)
}
ctx.stroke()
this.layeredText({
ctx: ctx,
text: strings.clear,
fontSize: 18,
fontFamily: config.font,
x: gaugeClear + 3,
y: config.multiplayer ? 22 : 11,
letterSpacing: -2
}, [
{scale: [1.1, 1.01], outline: "#000", letterBorder: 6},
{scale: [1.11, 1], fill: cleared ? "#fff" : "#737373"}
])
ctx.restore()
}
soul(config){
var ctx = config.ctx
ctx.save()
ctx.translate(config.x, config.y)
if(config.scale){
ctx.scale(config.scale, config.scale)
}
ctx.translate(-23, -21)
ctx.fillStyle = config.cleared ? "#fff" : "#737373"
ctx.fill(this.soulPath)
ctx.restore()
}
slot(ctx, x, y, size){
var mul = size / 106
ctx.save()
ctx.globalAlpha = 0.7
ctx.globalCompositeOperation = "screen"
ctx.fillStyle = "#444544"
ctx.beginPath()
ctx.arc(x, y, 26 * mul, 0, Math.PI * 2)
ctx.fill()
ctx.lineWidth = 3
ctx.strokeStyle = "#9c9e9c"
ctx.beginPath()
ctx.arc(x, y, 33.5 * mul, 0, Math.PI * 2)
ctx.stroke()
ctx.lineWidth = 3.5
ctx.strokeStyle = "#5d5e5d"
ctx.beginPath()
ctx.arc(x, y, 51.5 * mul, 0, Math.PI * 2)
ctx.stroke()
ctx.restore()
}
category(config){
var ctx = config.ctx
ctx.save()
ctx.translate(config.x, config.y)
if(config.scale || config.right){
ctx.scale((config.right ? -1 : 1) * (config.scale || 1), config.scale || 1)
}
ctx.translate(-15.5 + 14, -11)
for(var i = 0; i < 4; i++){
if(i < 2){
ctx.lineWidth = 6
ctx.strokeStyle = "#000"
ctx.stroke(this.categoryPath.main)
}else{
ctx.fillStyle = config.fill
ctx.fill(this.categoryPath.main)
ctx.fillStyle = "rgba(255, 255, 255, 0.25)"
ctx.fill(this.categoryPath.main)
ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
ctx.fill(this.categoryPath.shadow)
}
if(i % 2 === 0){
ctx.translate(-14, 0)
}else if(i === 1){
ctx.translate(14, 0)
}
}
if(config.highlight){
this.highlight({
ctx: ctx,
x: 0,
y: 0,
opacity: 0.8,
shape: this.categoryPath.highlight,
size: 8
})
}
ctx.restore()
}
alpha(amount, ctx, callback, winW, winH){
if(amount >= 1){
return callback(ctx)
}else if(amount >= 0){
this.tmpCanvas.width = winW || ctx.canvas.width
this.tmpCanvas.height = winH || ctx.canvas.height
callback(this.tmpCtx)
ctx.save()
ctx.globalAlpha = amount
ctx.drawImage(this.tmpCanvas, 0, 0)
ctx.restore()
}
}
shadow(config){
if(!disableBlur || config.force){
var ctx = config.ctx
if(config.fill){
ctx.shadowColor = config.fill
}
if(config.blur){
ctx.shadowBlur = config.blur
}
if(config.x){
ctx.shadowOffsetX = config.x
}
if(config.y){
ctx.shadowOffsetY = config.y
}
}
}
bold(font){
return font === "Microsoft YaHei, sans-serif" ? "bold " : ""
}
getMS(){
return Date.now()
}
clean(){
this.songFrameCache.clean()
this.diffStarCache.clean()
this.crownCache.clean()
delete this.tmpCtx
delete this.tmpCanvas
}
}