Fix playing charts without audio, rendering improvements, bugfixes??

This commit is contained in:
Zsolt Zitting
2025-08-11 16:27:45 -07:00
parent ceab443b15
commit 222c587ea8
3 changed files with 218 additions and 125 deletions
+31 -17
View File
@@ -1,16 +1,26 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8" name="viewport" content="width=device-width">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes" />
<title>MercuryPlayer</title>
<link rel="stylesheet" href="main.css">
</head>
<body ontouchstart>
<a href="https://github.com/yellowberryHN/MercuryPlayer" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#fff; color:#151513" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
<video src preload id="bga"></video>
<canvas id="canvas"></canvas>
<div style="position: fixed; left: 5px; bottom: 5px; color: white;" class="no-hide">
<div style="white-space: pre-wrap;" id="stats"></div>
<a href="https://github.com/yellowberryHN/MercuryPlayer" class="github-corner" aria-label="View source on GitHub">
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#fff; color:#151513" aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
<div style="white-space: pre-wrap;" id="stats"></div>
<div class="responsive-square">
<video src preload id="bga"></video>
<canvas id="canvas"></canvas>
</div>
<div id="ui">
<div>SE Volume: <select id="se_volume"></select></div>
<div class="hide-remote">SE: <input id="se_file" name="mer_se_file" type="file" accept="audio/*" placeholder="sound effect file path..."/></div>
<div id="music_select_div"><select id="music_select"></select><br><select id="diffi_select"><option value="0">Normal</option><option value="1">Hard</option><option value="2" selected>Expert</option><option value="3">Inferno</option></select></div>
@@ -18,18 +28,22 @@
<div>BGM: <input id="bgm_file" name="mer_bgm_file" type="file" accept="audio/*" placeholder="sound file path..."/></div>
<div>Speed: <input type="range" min="5" max="60" step="1" value="38" autocomplete="off" id="speed_input"><span id="speed_val">3.8</span></div>
<div class="no-hide">
<span><label><input type="checkbox" id="toggle_ui" autocomplete="off">Hide UI</label></span>
<span><label><input type="checkbox" id="toggle_long_audio" autocomplete="off">Long audio</label></span>
<span><label class="show-remote"><input type="checkbox" id="toggle_officials" autocomplete="off">Official charts</label></span>
<div class="no-hide" style="display: inline-block">
<span><label><input type="checkbox" id="show_ui" autocomplete="off" checked>Show UI</label></span>
</div>
<div style="display: inline-block">
<span class="no-mobile"><label><input type="checkbox" id="toggle_long_audio" autocomplete="off">Long audio</label></span>
<span class="show-remote"><label><input type="checkbox" id="toggle_officials" autocomplete="off">Official charts</label></span>
</div>
<input id="loadbtn" type="button" value="LOAD" onclick="loadFiles()">
&nbsp;
<input type="button" value="play" onclick="play()">
<input type="button" value="pause" onclick="pause()">
<input type="button" value="stop" onclick="stop()">
<br>
<div>
<input id="loadbtn" type="button" value="LOAD" onclick="loadFiles()">
&nbsp;
<input type="button" value="play" onclick="play()">
<input type="button" value="pause" onclick="pause()">
<input type="button" value="stop" onclick="stop()">
<br>
</div>
<!--<audio preload id="bgm" controls></audio>-->
<div class="bgm no-hide" id="bgm_custom"></div>
</div>
+38 -7
View File
@@ -1,28 +1,42 @@
html {
background: black;
color: white;
font-family: Meiryo, Helvetica, Arial, sans-serif;
user-select: none;
}
select, option {
-moz-font: -moz-pull-down-menu;
font-family: Meiryo, Helvetica, Arial, sans-serif;
}
.responsive-square {
width: 100vmin;
height: 100vmin;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
canvas {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 100vw;
height: 100%;
width: 100%;
}
video {
position: fixed;
mask-size: 100%;
mask-image: url('data:image/svg+xml,%3Csvg height%3D"2" width%3D"2" version%3D"1.1" xmlns%3D"http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg"%3E%3Cellipse style%3D"fill%3A%23000" cx%3D"1" cy%3D"1" rx%3D"1" ry%3D"1"%2F%3E%3C%2Fsvg%3E');
}
#ui {
position: fixed;
left: 5px;
bottom: 5px;
}
input {
padding-top: 0.5px;
padding-bottom: 0.5px;
}
.hide-control div:not(.no-hide) {
.hide-control #ui div:not(.no-hide) {
display: none !important; /* lol */
}
.long-audio audio {
@@ -62,6 +76,7 @@ input {
line-height: 40px;
background: rgba(30, 30, 30, 0.7);
width: 500px;
max-width: calc(100vw - 10px);
text-align: center;
}
.bgm svg {
@@ -113,7 +128,6 @@ input {
}
.long-audio .bgm {
width: 1000px;
max-width: calc(100vw - 16px);
}
.github-corner:hover .octo-arm {
@@ -145,3 +159,20 @@ input {
right: 0;
z-index: 10;
}
#stats {
position: fixed;
top: 5px;
left: 5px;
color: #444;
}
@media screen and (max-width: 500px) {
.no-mobile {
display: none;
}
.bgm {
width: 100vw;
}
}
+149 -101
View File
@@ -1,4 +1,4 @@
const mpVersion = '1.3.0';
const mpVersion = '1.4.0';
let remoteRun = window.location.protocol !== "file:"
@@ -34,18 +34,18 @@ let noteList = [];
/**
* 2: bpm
* 3: met
* 5: sfl
* 5: hi-speed
* 6: reverse start
* 7: reverse point
* 8: reverse end
* 9: sfl 0
* 10: sfl 1
* 9: stop start
* 10: stop end
*/
let controlList = [];
let noteListForPlayback = []
let bpmList = []
let metList = []
let sflTsList = []
let hiSpeedTsList = []
let laneToggleList = []
let idOffsetMap = []
let prevIdMap = {}
@@ -251,10 +251,12 @@ document.addEventListener('drop', (e) => {
dT.items.add(e.dataTransfer.files[i]);
switch (ext) {
/*
case 'wav':
document.getElementById('se_file').files = dT.files;
se_file_load(); // immediately load
break;
*/
case 'mer':
document.getElementById('music_file').files = dT.files;
break;
@@ -286,17 +288,19 @@ function loadUsingSelect() {
parseNotesFromFile(`MusicData/${strId}/${strId}_0${diffi}.mer`)
history.replaceState(null, null, document.location.pathname + `#${id}_${diffi}`);
if (bgm_file.files.length) setBgm(URL.createObjectURL(bgm_file.files[0]))
else setBgm(null)
}
function loadUsingFile() {
if (music_file.files.length && bgm_file.files.length) {
if (music_file.files.length) {
stop()
const reader = new FileReader()
reader.readAsText(music_file.files[0], 'UTF-8')
reader.onload = e => parseNotesFromText(reader.result)
bgmFileName = 'file'
setBgm(URL.createObjectURL(bgm_file.files[0]))
if (bgm_file.files.length) setBgm(URL.createObjectURL(bgm_file.files[0]))
else setBgm(null)
} else {
alert('Please choose remaining files.')
alert('Please select a chart file.')
}
}
music_select.addEventListener('change', e => {
@@ -321,9 +325,12 @@ let bgmFileName = 'file'
function setBgm(path) {
bgmCtr.duration = 0
bgmBuffer = null
fetch(path).then(r => r.arrayBuffer()).then(b => {
setBgmFromBuffer(b)
}).catch(e => console.error(path, e))
if(path != null) {
fetch(path).then(r => r.arrayBuffer()).then(b => {
setBgmFromBuffer(b)
}).catch(e => console.error(path, e))
}
stop()
}
function setBgmFromBuffer(r) {
seContext.decodeAudioData(r, buf => {
@@ -341,13 +348,27 @@ function parseNotesFromFile(file) {
function tickFromSectionAndTick(section, tick) {
return section * 1920 + tick;
}
function parseNotesFromText(text) {noteList = [];
function parseNotesFromText(text) {
const lines = text.trim().replace(/ +/g, '\t').split('\n');
let validChart = false
lines.forEach(line => {
if(line.startsWith('#BODY')) {
validChart = true;
}
})
if(!validChart) {
alert("This is not a valid chart file!")
return
}
noteList = []
controlList = []
noteListForPlayback = []
bpmList = []
metList = []
sflTsList = []
hiSpeedTsList = []
idOffsetMap = []
prevIdMap = {}
chartHeader = {}
@@ -360,8 +381,7 @@ function parseNotesFromText(text) {noteList = [];
seClockTrigger = []
comboInfo = [0,0]
const controlDupFix = {}
const lines = text.trim().replace(/ +/g, '\t').split('\n');
let lastEventTick = 0;
let nextHoldMap = {}
const holdFillTickGap = 3
@@ -429,7 +449,7 @@ function parseNotesFromText(text) {noteList = [];
}
})
lastEventTick++;
// add section seperator
for (let i=0; i <= lastEventTick; i+= 1920) {
noteList.push({
@@ -479,27 +499,27 @@ function parseNotesFromText(text) {noteList = [];
timeStampOffset += Math.round((toTick - fromTick) / TICK_PER_BEAT * 60000 / bpm);
}
const sflList = controlList.filter(i => i.cmdType === '5')
const hiSpeedList = controlList.filter(i => i.cmdType === '5')
noteNo = 0;
let distanceOffset = 0;
sflList.unshift({tickTotal:-999999, value1:1, timestamp:-999999})
hiSpeedList.unshift({tickTotal:-999999, value1:1, timestamp:-999999})
// convert milli second to distance(?) for chart speed control
for (let i = 0; i < sflList.length; i++) {
const currentSfl = sflList[i]
const fromTs = currentSfl.timestamp
const toTs = i == sflList.length - 1 ? Infinity : sflList[i + 1].timestamp
const sfl = currentSfl.value1
for (let i = 0; i < hiSpeedList.length; i++) {
const currentHiSpeed = hiSpeedList[i]
const fromTs = currentHiSpeed.timestamp
const toTs = i == hiSpeedList.length - 1 ? Infinity : hiSpeedList[i + 1].timestamp
const hiSpeed = currentHiSpeed.value1
for (; noteNo < noteListForPlayback.length; noteNo++) {
const currentNote = noteListForPlayback[noteNo]
if (currentNote.timestamp > toTs) break;
currentNote.distance = distanceOffset + Math.round((currentNote.timestamp - fromTs) * sfl);
currentNote.distance = distanceOffset + Math.round((currentNote.timestamp - fromTs) * hiSpeed);
}
sflTsList.push({
hiSpeedTsList.push({
timestamp: fromTs,
distance: distanceOffset,
sfl
hiSpeed
})
distanceOffset += Math.round((toTs - fromTs) * sfl);
distanceOffset += Math.round((toTs - fromTs) * hiSpeed);
}
// sort by distance
@@ -507,7 +527,7 @@ function parseNotesFromText(text) {noteList = [];
for (let i=0; i<noteListForPlayback.length; i++) {
idOffsetMap[noteListForPlayback[i].id] = i
}
// fix hold chain
noteListForPlayback.forEach(i => {
if (i.noteType === '9' || i.noteType === '25') {
@@ -515,10 +535,10 @@ function parseNotesFromText(text) {noteList = [];
const changePoint = [0]
let prevOffset = i.laneOffset
let laneOffsetAdjust = 0
let acrossMinusSfl = false
let acrossMinusHiSpeed = false
while (i.noteType !== '11') {
i = noteListForPlayback[idOffsetMap[i.extParam2]]
if (i.extParam1 === 1) changePoint.push(holdChain.length)
/*if (i.extParam1 === 1)*/ changePoint.push(holdChain.length)
holdChain.push(i)
if (prevOffset == 59 && i.laneOffset == 0) laneOffsetAdjust += 60
else if (prevOffset == 0 && i.laneOffset == 59) laneOffsetAdjust -= 60
@@ -527,7 +547,7 @@ function parseNotesFromText(text) {noteList = [];
}
for (let j = 1; j < holdChain.length - 1; j++) {
if (holdChain[j].distance > holdChain[j-1].distance && holdChain[j].distance > holdChain[j+1].distance) {
acrossMinusSfl = true
acrossMinusHiSpeed = true
break
}
}
@@ -538,7 +558,7 @@ function parseNotesFromText(text) {noteList = [];
for (let k = startIndex + 1; k < endIndex; k++) {
holdChain[k].laneOffset = (endOffset - startOffset) * (k - startIndex) / segments + startOffset
holdChain[k].noteWidth = (endWidth - startWidth) * (k - startIndex) / segments + startWidth
if (acrossMinusSfl) holdChain[k].distance = (endDistance - startDistance) * (k - startIndex) / segments + startDistance
if (acrossMinusHiSpeed) holdChain[k].distance = (endDistance - startDistance) * (k - startIndex) / segments + startDistance
}
}
@@ -611,8 +631,6 @@ function parseNotesFromText(text) {noteList = [];
})
laneToggleList.sort((a,b) => a.timestamp-b.timestamp)
console.log('parsed notes')
window.noteTypes = {}
noteListForPlayback.forEach(i => {
if (!window.noteTypes[i.noteType]) window.noteTypes[i.noteType] = []
@@ -620,7 +638,7 @@ function parseNotesFromText(text) {noteList = [];
})
seRTrigger = Object.keys(noteListForPlayback.filter(i=>['20','21','22','23','24','25','26'].indexOf(i.noteType) !== -1).map(i => i.timestamp).reduce((v,i) => (v[Math.round(i)]=1,v), {})).map(i => parseInt(i)).sort((a,b)=>(a-b))
seTrigger = Object.keys(noteListForPlayback.filter(i=>['1','2','9','16'].indexOf(i.noteType) !== -1).map(i => i.timestamp).reduce((v,i) => (v[Math.round(i)]=1,v), {})).map(i => parseInt(i)).sort((a,b)=>(a-b))
seTrigger = Object.keys(noteListForPlayback.filter(i=>['1','2','9','11','16'].indexOf(i.noteType) !== -1).map(i => i.timestamp).reduce((v,i) => (v[Math.round(i)]=1,v), {})).map(i => parseInt(i)).sort((a,b)=>(a-b))
seSwipeTrigger = Object.keys(noteListForPlayback.filter(i=>['3','4','5','6','7','8'].indexOf(i.noteType) !== -1).map(i => i.timestamp).reduce((v,i) => (v[Math.round(i)]=1,v), {})).map(i => parseInt(i)).sort((a,b)=>(a-b))
seBonusTrigger = Object.keys(noteListForPlayback.filter(i=>['2','6','8'].indexOf(i.noteType) !== -1).map(i => i.timestamp).reduce((v,i) => (v[Math.round(i)]=1,v), {})).map(i => parseInt(i)).sort((a,b)=>(a-b))
@@ -650,8 +668,8 @@ const drawCount = {
frame: 0,
actualFrame: 0,
}
toggle_ui.addEventListener('input', () => {
document.body.classList[toggle_ui.checked ? 'add' : 'remove']('hide-control')
show_ui.addEventListener('input', () => {
document.body.classList[show_ui.checked ? 'remove' : 'add']('hide-control')
})
toggle_long_audio.addEventListener('input', () => {
document.body.classList[toggle_long_audio.checked ? 'add' : 'remove']('long-audio')
@@ -674,8 +692,8 @@ let startNextFrame = false
let currentTs = 0
let currentDistance = 0;
let playing = false;
let sfl = 1
let sflOffset = 0
let hiSpeed = 1
let hiSpeedOffset = 0
let drawForNextFrame = false
let NOTE_APPEAR_DISTANCE = 1
let NOTE_SPEED_POWER = 1.95
@@ -694,7 +712,7 @@ requestAnimationFrame(mainClock)
function render(now) {
drawCount.frame++
if (!sflTsList.length) return
if (!hiSpeedTsList.length) return
if (!playing) {
if (!drawForNextFrame) {
@@ -705,8 +723,7 @@ function render(now) {
drawCount.actualFrame++
ctx.clearRect(0, 0, canvas.width, canvas.height)
const cabView = (canvas.width == 1080 && canvas.height == 1920)
const centerX = canvas.width / 2, centerY = cabView ? (canvas.height / 2) - 58 : canvas.height / 2
const centerX = canvas.width / 2, centerY = canvas.height / 2
// outer ring
ctx.lineWidth = 3
@@ -755,13 +772,13 @@ function render(now) {
}
updateLaneOnState(-1, currentTs)
}
if (!(currentTs >= sflTsList[sflOffset].timestamp && (sflOffset === sflTsList.length - 1 || currentTs <= sflTsList[sflOffset + 1].timestamp))) {
for (sflOffset = 0; sflOffset < sflTsList.length - 1; sflOffset++) {
if (currentTs >= sflTsList[sflOffset].timestamp && currentTs <= sflTsList[sflOffset + 1].timestamp) {
if (!(currentTs >= hiSpeedTsList[hiSpeedOffset].timestamp && (hiSpeedOffset === hiSpeedTsList.length - 1 || currentTs <= hiSpeedTsList[hiSpeedOffset + 1].timestamp))) {
for (hiSpeedOffset = 0; hiSpeedOffset < hiSpeedTsList.length - 1; hiSpeedOffset++) {
if (currentTs >= hiSpeedTsList[hiSpeedOffset].timestamp && currentTs <= hiSpeedTsList[hiSpeedOffset + 1].timestamp) {
break;
}
}
sfl = sflTsList[sflOffset].sfl
hiSpeed = hiSpeedTsList[hiSpeedOffset].hiSpeed
}
currentTs = now - startTs
let calcBaseTs = Math.max(currentTs, 0)
@@ -773,7 +790,7 @@ function render(now) {
break
}
}
currentDistance = (calcBaseTs - sflTsList[sflOffset].timestamp) * sfl + sflTsList[sflOffset].distance
currentDistance = (calcBaseTs - hiSpeedTsList[hiSpeedOffset].timestamp) * hiSpeed + hiSpeedTsList[hiSpeedOffset].distance
}
updateLaneOnState(previousTs, currentTs)
@@ -801,6 +818,7 @@ function render(now) {
sectionSep: [],
touch: [],
hold: [],
holdEnd: [],
holdBody: [],
chain: [],
flickL: [],
@@ -827,9 +845,8 @@ function render(now) {
case '7': // flick R
case '8': // flick R with effect
case '24': {notesToRender.arrow.push(i); notesToRender.flickR.push(i); break;} // flick Right R
case '9': // hold start
case '25': // hold start R
case '11-': {notesToRender.hold.push(i);} // hold end
case '9': case '25': {notesToRender.hold.push(i); break;} // hold start, hold start R
case '11': {notesToRender.holdEnd.push(i); break;} // hold end
case '10': {notesToRender.holdBody.push(i); break;} // hold body
case '12':
case '13': {notesToRender.laneEffect.push(i); break;}
@@ -865,7 +882,7 @@ function render(now) {
{
const drawHoldList = getHoldsForDraw(currentDistance, currentTs, drawDistance);
//ctx.fillStyle = 'rgba(207,162,93, 0.7)'
ctx.fillStyle = 'rgba(220,200,90, 0.7)'
ctx.fillStyle = 'rgba(255,198,73, 0.6)'
drawHoldList.forEach(nodes => {
ctx.beginPath()
const nodeCount = nodes.length
@@ -891,31 +908,33 @@ function render(now) {
}
for (let i=0; i<nodeCount-1; i++) {
const r = distanceToRenderRadius(maxR, Math.max(nodes[i].distance - currentDistance, 0) / RENDER_DISTANCE)
const start = 60 - nodes[i].laneOffset - nodes[i].noteWidth, end = 60 - nodes[i].laneOffset
const gapWidth = nodes[i].noteWidth == 60 ? 0 : 0.01
const shrinkSize = nodes[i].noteWidth < 60 ? 1 : 0
const start = 60 - nodes[i].laneOffset - nodes[i].noteWidth + shrinkSize, end = 60 - nodes[i].laneOffset - shrinkSize
const growSize = (1 - shrinkSize) * 0.25
if (i === 0) {
ctx.arc(
centerX, centerY,
r,
Math.PI * (start / 30) + gapWidth, Math.PI * (end / 30) - gapWidth
Math.PI * ((start - growSize) / 30), Math.PI * ((end + growSize) / 30)
)
} else {
pathToArcPoint(ctx, centerX, centerY, r, Math.PI * (end / 30) - gapWidth)
pathToArcPoint(ctx, centerX, centerY, r, Math.PI * ((end + growSize) / 30))
}
}
for (let i=nodeCount-1; i>=0; i--) {
const r = distanceToRenderRadius(maxR, Math.min(nodes[i].distance - currentDistance, RENDER_DISTANCE) / RENDER_DISTANCE)
const start = 60 - nodes[i].laneOffset - nodes[i].noteWidth, end = 60 - nodes[i].laneOffset
const gapWidth = nodes[i].noteWidth == 60 ? 0 : 0.01
const shrinkSize = nodes[i].noteWidth < 60 ? 1 : 0
const start = 60 - nodes[i].laneOffset - nodes[i].noteWidth + shrinkSize, end = 60 - nodes[i].laneOffset - shrinkSize
const growSize = (1 - shrinkSize) * 0.25
if (i === nodeCount-1) {
ctx.arc(
centerX, centerY,
r,
Math.PI * (end / 30) - gapWidth, Math.PI * (start / 30) + gapWidth,
Math.PI * ((end + growSize) / 30), Math.PI * ((start - growSize) / 30),
true
)
} else {
pathToArcPoint(ctx, centerX, centerY, r, Math.PI * (start / 30) - gapWidth)
pathToArcPoint(ctx, centerX, centerY, r, Math.PI * ((start - growSize) / 30))
}
}
ctx.closePath()
@@ -925,14 +944,14 @@ function render(now) {
// blue extend for same time notes
{
const key = 'sameTime', color = 'rgb(80,255,250)'
const key = 'sameTime', color = 'rgb(0,255,255)'
const thicc = 4 * displayRatio
if (notesToRender[key].length) {
ctx.strokeStyle = color
notesToRender[key].forEach(i => {
const r = distanceToRenderRadius(maxR, (i.distance - currentDistance) / RENDER_DISTANCE)
ctx.lineWidth = 10 * thicc
const start = 60 - i.laneOffset - i.noteWidth, end = 60 - i.laneOffset
const start = 60 - i.laneOffset - i.noteWidth + 0.5, end = 60 - i.laneOffset - 0.5
const scale = r / maxR
ctx.translate(centerX, centerY)
ctx.scale(scale, scale)
@@ -957,8 +976,8 @@ function render(now) {
notesToRender[key].forEach(i => {
const r = distanceToRenderRadius(maxR, (i.distance - currentDistance) / RENDER_DISTANCE)
ctx.lineWidth = 10 * thicc
const start = 60 - i.laneOffset - i.noteWidth, end = 60 - i.laneOffset
const cutOut = i.noteWidth < 60 ? 0.01 : 0
const cutOut = i.noteWidth < 60 ? 0.5 : 0
const start = 60 - i.laneOffset - i.noteWidth + cutOut, end = 60 - i.laneOffset - cutOut
const scale = r / maxR
ctx.translate(centerX, centerY)
ctx.scale(scale, scale)
@@ -966,7 +985,7 @@ function render(now) {
ctx.arc(
0, 0,
maxR,
Math.PI * (start / 30) + cutOut, Math.PI * (end / 30) - cutOut
Math.PI * (start / 30), Math.PI * (end / 30)
)
ctx.stroke()
ctx.setTransform(1, 0, 0, 1, 0, 0)
@@ -975,35 +994,68 @@ function render(now) {
}
const colorMap = [
['hold', 'rgb(88,75,47)'],
['touch', 'rgb(216,45,184)'],
['chain', 'rgb(234,234,170)'],
['flickL', 'rgb(246,159,55)'],
['flickR', 'rgb(98,251,43)'],
['snapIn', 'rgb(203,29,25)'],
['snapOut', 'rgb(33,180,251)'],
['hold', 'rgb(100,66,0)'],
['holdEnd', 'rgba(255,198,73, 0.7)'],
['touch', 'rgb(255,9,220)'],
['chain', 'rgb(255,225,30)'],
['flickL', 'rgb(255,154,0)'],
['flickR', 'rgb(57,210,52)'],
['snapIn', 'rgb(221,44,44)'],
['snapOut', 'rgb(2,107,255)'],
]
colorMap.forEach(noteType => {
const key = noteType[0], color = noteType[1]
const thicc = (['flickL','flickR','snapIn','snapOut'].indexOf(key) === -1 ? 2.25 : 2.5) * displayRatio
if (notesToRender[key].length) {
ctx.strokeStyle = color
notesToRender[key].forEach(i => {
const r = distanceToRenderRadius(maxR, (i.distance - currentDistance) / RENDER_DISTANCE)
const r = distanceToRenderRadius(maxR, (i.distance - currentDistance) / RENDER_DISTANCE)
ctx.lineWidth = 10 * thicc
const start = 60 - i.laneOffset - i.noteWidth, end = 60 - i.laneOffset
const cutOut = i.noteWidth < 60 ? 0.03 : 0
const cutOut = (i.noteWidth < 60) ? (key == 'holdEnd' ? 0.5 : 1) : 0
const start = 60 - i.laneOffset - i.noteWidth + cutOut, end = 60 - i.laneOffset - cutOut
const scale = r / maxR
// set note color
ctx.strokeStyle = color
ctx.translate(centerX, centerY)
ctx.scale(scale, scale)
ctx.beginPath()
ctx.arc(
0, 0,
maxR,
Math.PI * (start / 30) + cutOut, Math.PI * (end / 30) - cutOut
Math.PI * (start / 30), Math.PI * (end / 30)
)
ctx.stroke()
ctx.setTransform(1, 0, 0, 1, 0, 0)
if(key != 'holdEnd' && i.noteWidth != 60) {
// set endcap color
ctx.strokeStyle = 'rgb(78,172,247)'
// left endcap
ctx.translate(centerX, centerY)
ctx.scale(scale, scale)
ctx.beginPath()
ctx.arc(
0, 0,
maxR,
Math.PI * (end / 30), Math.PI * ((end + 0.25) / 30)
)
ctx.stroke()
ctx.setTransform(1, 0, 0, 1, 0, 0)
// right endcap
ctx.translate(centerX, centerY)
ctx.scale(scale, scale)
ctx.beginPath()
ctx.arc(
0, 0,
maxR,
Math.PI * ((start - 0.25) / 30), Math.PI * (start / 30)
)
ctx.stroke()
ctx.setTransform(1, 0, 0, 1, 0, 0)
}
})
}
})
@@ -1023,7 +1075,7 @@ function render(now) {
continue
}
const r = distanceToRenderRadius(maxR, (a.distance - currentDistance) / RENDER_DISTANCE)
const start = 60 - a.laneOffset - a.noteWidth, end = 60 - a.laneOffset
const start = 60 - a.laneOffset - a.noteWidth + 1, end = 60 - a.laneOffset - 1
ctx.save()
ctx.beginPath()
ctx.moveTo(centerX, centerY)
@@ -1068,10 +1120,10 @@ function render(now) {
const nodes = []
for (let i=0; i<2; i++) {
const arrowAngleDirection = [-0.4, 0.4][i]
const color = ['rgb(246,159,55)', 'rgb(98,251,43)'][i]
const color = ['rgb(255,154,0)', 'rgb(57,210,52)'][i]
flicks[i].forEach(a => {
const r = distanceToRenderRadius(maxR*0.95, (a.distance - currentDistance) / RENDER_DISTANCE)
const start = 60 - a.laneOffset - a.noteWidth, end = 60 - a.laneOffset
const start = 60 - a.laneOffset - a.noteWidth + 1, end = 60 - a.laneOffset - 1
const rPos = 1 - (a.distance - currentDistance) / RENDER_DISTANCE
const rotateOffset = ((rPos * rotateSpeed) * [1, -1][i] + 2) % 2
const flickNodes = {n:[], r:r, c: color}
@@ -1136,9 +1188,9 @@ function render(now) {
}
{
let chartRemaining = 1
let chartProgress = 0
if (chartLength) {
chartRemaining = Math.max(0, 1 - currentTs / chartLength)
chartProgress = Math.max(0, currentTs / chartLength)
}
ctx.strokeStyle = 'rgba(0,0,0,0.3)'
const r = maxR * 0.98
@@ -1147,7 +1199,7 @@ function render(now) {
ctx.arc(
centerX, centerY,
r,
-Math.PI * (0.5 - chartRemaining * 2), Math.PI * -0.5
-Math.PI * (0.5 + chartProgress * 2), Math.PI * -0.5
)
ctx.stroke()
}
@@ -1185,19 +1237,16 @@ function render(now) {
section: ('000'+Math.floor((chkTs - refBase.timestamp) / beatDuration / met.value1 + refBase.section)).slice(-3),
beat: Math.floor((chkTs - refBase.timestamp) / beatDuration + refBase.tick / (TICK_PER_GAME_SECTION / met.value1)) % met.value1,
bpm,
sfl
hiSpeed
}
const playInfoText = [
`${playedNotesCount[0]} (${playedNotesCount[1]}) / ${comboInfo[0]} (${comboInfo[1]})`,
`${playInfoStat.min}:${playInfoStat.sec}.${playInfoStat.milisec} (${playInfoStat.section}/${playInfoStat.beat})`,
`BPM: ${playInfoStat.bpm}`,
`SFL: ${playInfoStat.sfl}`,
`BPM: ${playInfoStat.bpm.toFixed(1)}`,
`HS: ${playInfoStat.hiSpeed}` + (reversing ? ' (Reversing)' : ''),
``,
`v${mpVersion}`
]
if (reversing) {
playInfoText.push('Reversing')
}
const fontSize = 16 * devicePixelRatio
ctx.font = `${fontSize}px Arial`
ctx.fillStyle = 'white'
@@ -1277,8 +1326,8 @@ window.play = function () {
bgmBufSrc.buffer = bgmBuffer
bgmBufSrc.connect(bgmGain)
bgmBufSrc.start(0, bgmCtr.currentTime)
currentTs = Math.round(bgmCtr.currentTime * 1000) + globalPlaybackOffset
}
currentTs = Math.round(bgmCtr.currentTime * 1000) + globalPlaybackOffset
bgmCtr.play()
if (enableBga) bga.play()
bga.muted = true
@@ -1308,8 +1357,8 @@ window.stop = function () {
bgmCtr.currentTime = 0
if (enableBga) bga.currentTime = 0
drawForNextFrame = true
sflOffset = 0
sfl = 1
hiSpeedOffset = 0
hiSpeed = 1
}
window.setPlaybackTime = function (time = 0) {
currentTs = Math.round(time * 1000)
@@ -1374,7 +1423,7 @@ function getHoldsForDraw(currentDistance, currentTs, renderDistance = RENDER_DIS
} else if (offset === nodeCount - 1) { // last one in chain
tail = offset
if (tail-head > 0) drawHoldList.push(hold.nodes.slice(head, tail + 1))
} else if (offset > 0 && offset < nodeCount - 1) { // not first or last one in chain, if change render direction (due to sfl), break render chain
} else if (offset > 0 && offset < nodeCount - 1) { // not first or last one in chain, if change render direction (due to note speed chages), break render chain
if (hold.nodes[offset].distance > hold.nodes[offset - 1].distance && hold.nodes[offset].distance > hold.nodes[offset + 1].distance) {
tail = offset
if (tail-head > 0) drawHoldList.push(hold.nodes.slice(head, tail + 1))
@@ -1425,16 +1474,15 @@ speed_input.value = window.settings.scrollSpeed * 10
speed_input.dispatchEvent(new Event('input'))
function resize() {
const w = Math.round(window.innerWidth * devicePixelRatio), h = Math.round(window.innerHeight * devicePixelRatio)
canvas.width = w
canvas.height = h
const cabView = (canvas.width == 1080 && canvas.height == 1920)
maxR = cabView ? 530 : Math.round(Math.min(w, h) * 0.45)
const size = Math.min(window.innerWidth, window.innerHeight)
canvas.width = canvas.height = size * devicePixelRatio
//const cabView = (canvas.width == 1080 && canvas.height == 1920)
//maxR = cabView ? 530 : Math.round(Math.min(w, h) * 0.45)
maxR = Math.round(size * devicePixelRatio * 0.45)
drawForNextFrame = true
displayRatio = Math.max(w, h) / 1920
const wView = window.innerWidth, hView = window.innerHeight
const centerX = wView / 2, centerY = cabView ? (hView / 2) - 58 : hView / 2
const rView = Math.round(Math.min(wView, hView) * 0.45)
displayRatio = 1
const centerX = size / 2, centerY = size / 2
const rView = Math.round(size * 0.45)
if (enableBga) {
bga.style.left = (centerX - rView) + 'px'