Skip to content

Implement visual sync settings UI and offset logic#297

Merged
HyeokjinKang merged 7 commits into
mainfrom
feat-visual-sync
Apr 1, 2026
Merged

Implement visual sync settings UI and offset logic#297
HyeokjinKang merged 7 commits into
mainfrom
feat-visual-sync

Conversation

@HyeokjinKang

Copy link
Copy Markdown
Owner

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:

  • Added a new "Visual Sync" adjustment UI in the game options, including a canvas-based visualizer, value display, and reset functionality (public/js/game.js, public/css/game.css) [1] [2].
  • Implemented logic for adjusting, displaying, and saving the visual sync offset, with keyboard and button controls for fine-tuning (public/js/game.js) [1] [2].
  • Integrated the visual sync offset into gameplay and editor timing calculations, ensuring visuals and audio remain in sync during play and editing (public/js/play.js, public/js/editor.js) [1] [2] [3] [4] [5] [6] [7].

Localization and UI Text Updates:

  • Updated English and Korean localization files to include new strings for the visual sync feature and improved terminology for "In-Game"/"인게임" (locales/en.json, locales/ko.json) [1] [2] [3] [4].

Dependency Update:

  • Upgraded libphonenumber-js from version 1.12.40 to 1.12.41 in pnpm-lock.yaml [1] [2] [3].

These changes collectively provide a more customizable and user-friendly experience for synchronizing audio and visuals in-game.

HyeokjinKang and others added 5 commits March 29, 2026 23:49
- 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-js in pnpm-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.

Comment thread public/js/play.js
});

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

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));

Copilot uses AI. Check for mistakes.
Comment thread public/js/test.js
});

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

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));

Copilot uses AI. Check for mistakes.
Comment thread public/js/tutorial.js
});

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

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));

Copilot uses AI. Check for mistakes.
Comment thread public/js/game.js Outdated
Comment on lines +2156 to +2157
if (key == "arrowup") visualSyncOffset++;
else if (key == "arrowdown") visualSyncOffset--;

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
if (key == "arrowup") visualSyncOffset++;
else if (key == "arrowdown") visualSyncOffset--;
if (key == "arrowup") {
e.preventDefault();
visualSyncOffset++;
} else if (key == "arrowdown") {
e.preventDefault();
visualSyncOffset--;
}

Copilot uses AI. Check for mistakes.
@HyeokjinKang HyeokjinKang merged commit 45b903c into main Apr 1, 2026
4 checks passed
@HyeokjinKang HyeokjinKang deleted the feat-visual-sync branch April 1, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants