Implement visual sync settings UI and offset logic#297
Conversation
- visualSyncContainer에 캔버스 프리뷰, 설명, +/- 버튼, 리셋 버튼 추가 - 옵션 화면 Display 탭에 syncButton 추가 - visual sync 관련 CSS 스타일 추가 (#visualSyncCanvas, #visualSyncButtonContainer 등) - en.json, ko.json에 visual_sync_* 번역 키 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- visualSyncOffset 변수 추가, settings.display.offset으로 저장/로드 - visualSyncSetting(): offsetSong 재생 및 캔버스 노트 프리뷰 애니메이션 - visualSyncUp/Down/Reset() 함수 추가 - 방향키(↑↓)로 1ms 단위 세밀 조정 지원 (display==13) - displayClose() display==13 처리: 애니메이션 정리 및 offsetSong fade Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- visualSync 변수 추가, settings.display.offset으로 로드 (없으면 0) - calcBeats()에 seek 파라미터 추가 (기본값: song.seek() * 1000) - seekMs = song.seek() * 1000 + visualSync로 노트/총알 비주얼 기준 적용 - calculateScore 내 판정 beats는 visualSync 미반영 유지 (판정은 오디오 기준) - record.push의 ms 기록은 실제 오디오 기준(song.seek() * 1000)으로 유지 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- visualSync 변수 추가, settings.display.offset으로 로드 (없으면 0) - beats 계산에 visualSync 반영: seekMs - (offset + sync - visualSync) - 타임라인 오디오 기준선(offsetLineX) 계산에도 visualSync 반영 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a new Visual Sync setting that lets players adjust a display-only timing offset to better align on-screen visuals with audio during gameplay and editing.
Changes:
- Added a new Visual Sync calibration overlay (canvas visualizer, value controls, reset) and exposed it from the options UI.
- Integrated a
display.offset(visual sync) setting into timing calculations in play/test/tutorial and the editor. - Updated EN/KR localization strings and bumped
libphonenumber-jsinpnpm-lock.yaml.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| views/game.ejs | Adds Visual Sync UI container and option entry point (syncButton). |
| public/js/game.js | Implements Visual Sync overlay logic, animation loop, keyboard/button adjustment, and settings persistence. |
| public/js/play.js | Applies visual sync offset to render-time seeking/beat calculations. |
| public/js/test.js | Applies visual sync offset to render-time seeking/beat calculations (test mode). |
| public/js/tutorial.js | Applies visual sync offset to render-time seeking/beat calculations (tutorial). |
| public/js/editor.js | Applies visual sync offset to editor timeline/playback timing calculations. |
| public/css/game.css | Styles Visual Sync overlay elements and reuses offset-setting styling patterns. |
| locales/en.json | Adds Visual Sync strings; updates “Game” → “In-Game”. |
| locales/ko.json | Adds Visual Sync strings; updates “게임” → “인게임”. |
| pnpm-lock.yaml | Bumps libphonenumber-js 1.12.40 → 1.12.41. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }); | ||
|
|
||
| const calcBeats = () => Number((bpmsync.beat + (song.seek() * 1000 - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); | ||
| const calcBeats = (seek = song.seek() * 1000) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); |
There was a problem hiding this comment.
calcBeats now accepts an optional seek parameter, but its default still uses the raw song.seek() time. Since rendering uses seekMs = song.seek()*1000 + visualSync, any call sites that still use calcBeats() (e.g., input judging / score updates) will ignore visualSync, causing the visual timing and judgement timing to drift. Consider including visualSync in the default (e.g., default seek adds visualSync), or refactor to a single authoritative seekMs used for both rendering and judgement so they stay consistent.
| const calcBeats = (seek = song.seek() * 1000) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); | |
| const calcBeats = (seek = song.seek() * 1000 + visualSync) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); |
| }); | ||
|
|
||
| const calcBeats = () => Number((bpmsync.beat + (song.seek() * 1000 - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); | ||
| const calcBeats = (seek = song.seek() * 1000) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); |
There was a problem hiding this comment.
Same issue as in play.js: calcBeats defaults to raw song.seek() even though rendering uses song.seek()*1000 + visualSync. Any remaining calcBeats() calls (judging / scoring) will ignore visualSync, making hits register early/late compared to what the player sees. Update the default seek calculation or ensure all beat calculations consistently apply visualSync.
| const calcBeats = (seek = song.seek() * 1000) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); | |
| const calcBeats = (seek = song.seek() * 1000 + visualSync) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); |
| }); | ||
|
|
||
| const calcBeats = () => Number((bpmsync.beat + (song.seek() * 1000 - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); | ||
| const calcBeats = (seek = song.seek() * 1000) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); |
There was a problem hiding this comment.
Same issue as in play.js: calcBeats defaults to raw song.seek() even though rendering uses song.seek()*1000 + visualSync. Any remaining calcBeats() calls (judging / scoring) will ignore visualSync, making hits register early/late compared to what the player sees. Update the default seek calculation or ensure all beat calculations consistently apply visualSync.
| const calcBeats = (seek = song.seek() * 1000) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); | |
| const calcBeats = (seek = song.seek() * 1000 + visualSync) => Number((bpmsync.beat + (seek - (offset + sync + audioLatency * 1000) - bpmsync.ms) / (60000 / bpm)).toPrecision(10)); |
| if (key == "arrowup") visualSyncOffset++; | ||
| else if (key == "arrowdown") visualSyncOffset--; |
There was a problem hiding this comment.
When the Visual Sync overlay is open (display == 13), arrow keys adjust the value but the handler doesn’t call preventDefault(). Arrow keys can still trigger browser scrolling / focus navigation, which can interfere with calibration. Consider preventing default for ArrowUp/ArrowDown while display == 13 (similar to how other overlays handle key events).
| if (key == "arrowup") visualSyncOffset++; | |
| else if (key == "arrowdown") visualSyncOffset--; | |
| if (key == "arrowup") { | |
| e.preventDefault(); | |
| visualSyncOffset++; | |
| } else if (key == "arrowdown") { | |
| e.preventDefault(); | |
| visualSyncOffset--; | |
| } |
This pull request introduces a new "Visual Sync" feature, allowing users to adjust the synchronization between audio and visuals for improved gameplay experience. The update includes UI, logic, and localization changes to support this feature, as well as a minor dependency update.
Visual Sync Feature Implementation:
public/js/game.js,public/css/game.css) [1] [2].public/js/game.js) [1] [2].public/js/play.js,public/js/editor.js) [1] [2] [3] [4] [5] [6] [7].Localization and UI Text Updates:
locales/en.json,locales/ko.json) [1] [2] [3] [4].Dependency Update:
libphonenumber-jsfrom version 1.12.40 to 1.12.41 inpnpm-lock.yaml[1] [2] [3].These changes collectively provide a more customizable and user-friendly experience for synchronizing audio and visuals in-game.