From baf77f6531f1fea6764259aaa8192016e041dcc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 23:01:07 +0000 Subject: [PATCH 1/3] Initial plan From c9c2537ed855a22d17fbd42cde9cc5da21a52176 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 23:07:15 +0000 Subject: [PATCH 2/3] perf: add fixed-step update loop to smooth gameplay rendering Agent-Logs-Url: https://github.com/7effrey89/PianoHero/sessions/703d97bb-feaf-4ffa-a460-60b0d55ce84e Co-authored-by: 7effrey89 <30802073+7effrey89@users.noreply.github.com> --- app.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index eb6eb5b..e5063d9 100644 --- a/app.js +++ b/app.js @@ -58,6 +58,12 @@ class PianoHero { this._laneCanvas = null; // offscreen canvas for lanes + hit zone this._laneCacheDirty = true; this._boundRender = (ts) => this._renderFrame(ts); + this._fixedStepSec = 1 / 60; + this._maxCatchupSteps = 5; + this._logicAccumulatorSec = 0; + this._logicClockSec = 0; + this._logicLastTargetSec = 0; + this._hasLogicClock = false; // PixiJS GPU-accelerated note renderer (DOM-composited, no drawImage needed) this.glCanvas = document.getElementById('glCanvas'); @@ -1107,6 +1113,8 @@ class PianoHero { } else { this.startTime = performance.now() - newCurrentTime * 1000; } + this._hasLogicClock = false; + this._logicAccumulatorSec = 0; } this.speedMultiplier = newSpeed; @@ -1948,6 +1956,8 @@ class PianoHero { const gameClockSec = (refTime - this.startTime) / 1000; this.startTime = performance.now() - gameClockSec * 1000; } + this._hasLogicClock = false; + this._logicAccumulatorSec = 0; this._updateControlButtons(); @@ -2337,6 +2347,7 @@ class PianoHero { if (this.isPaused) { this.pauseTime = performance.now(); + this._logicAccumulatorSec = 0; if (this.isAutoPlay) { this.autoPlayTimeouts.forEach(t => clearTimeout(t)); this.autoPlayTimeouts = []; @@ -2350,6 +2361,8 @@ class PianoHero { } else { const pauseDuration = performance.now() - this.pauseTime; this.startTime += pauseDuration; + this._hasLogicClock = false; + this._logicAccumulatorSec = 0; if (this.isAutoPlay) { this._scheduleAutoPlayNotes(); this.statusMessage.textContent = 'Auto Play in progress...'; @@ -2469,6 +2482,8 @@ class PianoHero { this.autoPlayTimeouts.forEach(t => clearTimeout(t)); this.autoPlayTimeouts = []; this.fallingNotes = []; + this._hasLogicClock = false; + this._logicAccumulatorSec = 0; this.heldFallingNotes.clear(); this.heldKeys.clear(); this.particles = []; @@ -3047,6 +3062,8 @@ class PianoHero { } else { this.startTime = performance.now() - gameClockSec * 1000; } + this._hasLogicClock = false; + this._logicAccumulatorSec = 0; // Cancel auto-play timeouts and clear active sounds this.autoPlayTimeouts.forEach(t => clearTimeout(t)); @@ -3095,10 +3112,10 @@ class PianoHero { const timeUntilHit = scaledNoteTime - gameClockSec; note.y = this.hitZoneY - (timeUntilHit * this.noteSpeed * speed); } - this.draw(); + this._renderFrame(this.startTime + (gameClockSec * 1000), false); } - update() { + update(currentTimeOverride) { if (!this.isPlaying || this.isPaused) return; // ── Practice mode: freeze time until correct note is played ── @@ -3106,7 +3123,9 @@ class PianoHero { return this.updatePracticeMode(); } - const currentTime = (this._frameTime - this.startTime) / 1000; + const currentTime = typeof currentTimeOverride === 'number' + ? currentTimeOverride + : (this._frameTime - this.startTime) / 1000; const speed = this.speedMultiplier; // Update song progress timeline @@ -3267,9 +3286,48 @@ class PianoHero { return true; } - _renderFrame(ts) { + _renderFrame(ts, scheduleNext = true) { this._frameTime = ts || performance.now(); - this.update(); + if (this.isPlaying && !this.isPaused) { + const targetTimeSec = Math.max(0, (this._frameTime - this.startTime) / 1000); + if (!this._hasLogicClock) { + this._logicClockSec = targetTimeSec; + this._logicLastTargetSec = targetTimeSec; + this._logicAccumulatorSec = 0; + this._hasLogicClock = true; + } + let deltaToTarget = targetTimeSec - this._logicLastTargetSec; + this._logicLastTargetSec = targetTimeSec; + if (deltaToTarget < 0) { + this._logicClockSec = targetTimeSec; + this._logicLastTargetSec = targetTimeSec; + this._logicAccumulatorSec = 0; + deltaToTarget = 0; + } + const maxBacklog = this._fixedStepSec * this._maxCatchupSteps; + this._logicAccumulatorSec = Math.min(this._logicAccumulatorSec + deltaToTarget, maxBacklog); + + let steps = 0; + while (this._logicAccumulatorSec >= this._fixedStepSec && steps < this._maxCatchupSteps) { + this._logicClockSec += this._fixedStepSec; + this.update(this._logicClockSec); + this._logicAccumulatorSec -= this._fixedStepSec; + steps++; + } + + if (steps === this._maxCatchupSteps) { + this._logicClockSec = targetTimeSec; + this._logicLastTargetSec = targetTimeSec; + this._logicAccumulatorSec = 0; + this.update(this._logicClockSec); + } else if (steps === 0) { + this.update(this._logicClockSec); + } + } else { + this._logicAccumulatorSec = 0; + this._hasLogicClock = false; + this.update(); + } const w = this.canvas.width, h = this.canvas.height; const ctx = this.ctx; @@ -3368,7 +3426,7 @@ class PianoHero { this._emitHeldNoteParticles(); this._updateAndDrawParticles(ctx); - requestAnimationFrame(this._boundRender); + if (scheduleNext) requestAnimationFrame(this._boundRender); } _rebuildLaneCache() { From f897855cf1b2ebcf87fc9e0739d15bc82596b725 Mon Sep 17 00:00:00 2001 From: Jeffrey Lai Date: Sun, 17 May 2026 10:21:40 +0200 Subject: [PATCH 3/3] start rolling fixed --- app.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/app.js b/app.js index e5063d9..5ebabc7 100644 --- a/app.js +++ b/app.js @@ -64,6 +64,8 @@ class PianoHero { this._logicClockSec = 0; this._logicLastTargetSec = 0; this._hasLogicClock = false; + this.noteLeadInSec = 3; + this._preRollSec = 0; // PixiJS GPU-accelerated note renderer (DOM-composited, no drawImage needed) this.glCanvas = document.getElementById('glCanvas'); @@ -1941,8 +1943,8 @@ class PianoHero { // Otherwise start from the beginning with a lead-in. const preSeeked = this.fallingNotes.length > 0 && this.startTime; if (!preSeeked) { - const leadInSec = this.hitZoneY / (this.noteSpeed * this.speedMultiplier); - this.startTime = performance.now() + leadInSec * 1000; + this._preRollSec = this.noteLeadInSec; + this.startTime = performance.now(); this.fallingNotes = this.notes.map(note => ({ ...note, @@ -1955,6 +1957,7 @@ class PianoHero { const refTime = this.pauseTime || performance.now(); const gameClockSec = (refTime - this.startTime) / 1000; this.startTime = performance.now() - gameClockSec * 1000; + this._preRollSec = 0; } this._hasLogicClock = false; this._logicAccumulatorSec = 0; @@ -2243,7 +2246,7 @@ class PianoHero { if (closestNote) { closestNote.hit = true; - closestNote.holdStart = (performance.now() - this.startTime) / 1000; + closestNote.holdStart = this._getGameClockSec(); this.combo++; this.hitNotes++; const accuracy = 1 - (closestDistance / this.hitTolerance); @@ -2418,9 +2421,13 @@ class PianoHero { }); } + _getGameClockSec(referenceTime = performance.now()) { + return ((referenceTime - this.startTime) / 1000) - this._preRollSec; + } + _scheduleAutoPlayNotes() { - // Use real game clock (negative during lead-in) so sounds sync with visual notes - const currentTime = (performance.now() - this.startTime) / 1000; + // Use the same negative lead-in clock as rendering so sounds sync with note travel. + const currentTime = this._getGameClockSec(); // Schedule automatic key presses for remaining notes const speed = this.speedMultiplier; @@ -2441,7 +2448,7 @@ class PianoHero { // Directly mark the note as hit (bypasses timing-sensitive position check) if (!note.hit && !note.missed) { note.hit = true; - note.holdStart = (performance.now() - this.startTime) / 1000; + note.holdStart = this._getGameClockSec(); this.combo++; this.hitNotes++; this.score += Math.floor(100 * (1 + this.combo * 0.1)); @@ -2927,7 +2934,7 @@ class PianoHero { if (closestNote) { closestNote.hit = true; - closestNote.holdStart = (performance.now() - this.startTime) / 1000; + closestNote.holdStart = this._getGameClockSec(); this.combo++; this.hitNotes++; @@ -3125,7 +3132,7 @@ class PianoHero { const currentTime = typeof currentTimeOverride === 'number' ? currentTimeOverride - : (this._frameTime - this.startTime) / 1000; + : this._getGameClockSec(this._frameTime); const speed = this.speedMultiplier; // Update song progress timeline @@ -3289,7 +3296,7 @@ class PianoHero { _renderFrame(ts, scheduleNext = true) { this._frameTime = ts || performance.now(); if (this.isPlaying && !this.isPaused) { - const targetTimeSec = Math.max(0, (this._frameTime - this.startTime) / 1000); + const targetTimeSec = this._getGameClockSec(this._frameTime); if (!this._hasLogicClock) { this._logicClockSec = targetTimeSec; this._logicLastTargetSec = targetTimeSec; @@ -4206,7 +4213,7 @@ class PianoHero { if (this.laneStyle === 'synthesia') return; // clean look, no timeline grid const ctx = this.ctx; const speed = this.speedMultiplier; - const currentTime = (this._frameTime - this.startTime) / 1000; + const currentTime = this._getGameClockSec(this._frameTime); const pxPerSec = this.noteSpeed * speed; // Determine tick interval: 1s, 5s, or 10s depending on zoom