Compare commits
2 Commits
851e2a40ba
...
d68b90c4ea
| Author | SHA1 | Date | |
|---|---|---|---|
| d68b90c4ea | |||
| 307085711b |
150
app.js
150
app.js
@@ -9,6 +9,7 @@ const DEFAULT_MOVE_LIMIT_MS = 20000;
|
|||||||
const TIME_MODE_INITIAL_CLOCK_MS = 600000;
|
const TIME_MODE_INITIAL_CLOCK_MS = 600000;
|
||||||
const CUBE_TIME_CAP_MS = 120000;
|
const CUBE_TIME_CAP_MS = 120000;
|
||||||
const CUBE_START_HOLD_MS = 2000;
|
const CUBE_START_HOLD_MS = 2000;
|
||||||
|
const CUBE_PHASE_ALERT_NAVIGATION_DELAY_MS = 700;
|
||||||
|
|
||||||
const PRESETS = {
|
const PRESETS = {
|
||||||
fast: {
|
fast: {
|
||||||
@@ -56,6 +57,8 @@ window.requestAnimationFrame(() => {
|
|||||||
|
|
||||||
let match = readStoredMatch();
|
let match = readStoredMatch();
|
||||||
let dirty = false;
|
let dirty = false;
|
||||||
|
let audioContext = null;
|
||||||
|
let cubePhaseAlertRetryCleanup = null;
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
if (normalizeRecoveredMatch(match)) {
|
if (normalizeRecoveredMatch(match)) {
|
||||||
@@ -67,6 +70,8 @@ if (match) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("beforeunload", flushState);
|
window.addEventListener("beforeunload", flushState);
|
||||||
|
document.addEventListener("pointerdown", tryUnlockAudioContext, { passive: true });
|
||||||
|
document.addEventListener("keydown", tryUnlockAudioContext);
|
||||||
document.addEventListener("visibilitychange", () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -110,6 +115,120 @@ function syncViewportHeight() {
|
|||||||
document.documentElement.style.setProperty("--app-viewport-height", `${Math.round(viewportHeight)}px`);
|
document.documentElement.style.setProperty("--app-viewport-height", `${Math.round(viewportHeight)}px`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAudioContext() {
|
||||||
|
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (!AudioContextClass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!audioContext) {
|
||||||
|
try {
|
||||||
|
audioContext = new AudioContextClass();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryUnlockAudioContext() {
|
||||||
|
const context = getAudioContext();
|
||||||
|
if (!context || context.state === "running") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.resume().catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playCubePhaseAlert() {
|
||||||
|
const context = getAudioContext();
|
||||||
|
if (!context || context.state !== "running") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = [
|
||||||
|
{ frequency: 740, offset: 0, duration: 0.12, gain: 0.035 },
|
||||||
|
{ frequency: 988, offset: 0.18, duration: 0.12, gain: 0.04 },
|
||||||
|
{ frequency: 1318, offset: 0.36, duration: 0.2, gain: 0.05 },
|
||||||
|
];
|
||||||
|
const startAt = context.currentTime + 0.02;
|
||||||
|
|
||||||
|
pattern.forEach(({ frequency, offset, duration, gain }) => {
|
||||||
|
const oscillator = context.createOscillator();
|
||||||
|
const envelope = context.createGain();
|
||||||
|
const toneStartAt = startAt + offset;
|
||||||
|
|
||||||
|
oscillator.type = "triangle";
|
||||||
|
oscillator.frequency.setValueAtTime(frequency, toneStartAt);
|
||||||
|
envelope.gain.setValueAtTime(0.0001, toneStartAt);
|
||||||
|
envelope.gain.exponentialRampToValueAtTime(gain, toneStartAt + 0.02);
|
||||||
|
envelope.gain.exponentialRampToValueAtTime(0.0001, toneStartAt + duration);
|
||||||
|
|
||||||
|
oscillator.connect(envelope);
|
||||||
|
envelope.connect(context.destination);
|
||||||
|
oscillator.start(toneStartAt);
|
||||||
|
oscillator.stop(toneStartAt + duration + 0.03);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCubePhaseAlertRetry() {
|
||||||
|
if (typeof cubePhaseAlertRetryCleanup === "function") {
|
||||||
|
cubePhaseAlertRetryCleanup();
|
||||||
|
cubePhaseAlertRetryCleanup = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleCubePhaseAlertRetry(storedMatch) {
|
||||||
|
if (!storedMatch?.cube?.phaseAlertPending || cubePhaseAlertRetryCleanup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryPlayOnGesture = () => {
|
||||||
|
tryUnlockAudioContext();
|
||||||
|
if (!playCubePhaseAlert()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
storedMatch.cube.phaseAlertPending = false;
|
||||||
|
dirty = true;
|
||||||
|
persistMatch();
|
||||||
|
clearCubePhaseAlertRetry();
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = ["pointerdown", "keydown"];
|
||||||
|
cubePhaseAlertRetryCleanup = () => {
|
||||||
|
events.forEach((eventName) => {
|
||||||
|
document.removeEventListener(eventName, tryPlayOnGesture);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
events.forEach((eventName) => {
|
||||||
|
document.addEventListener(eventName, tryPlayOnGesture, { passive: eventName === "pointerdown" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybePlayPendingCubePhaseAlert(storedMatch) {
|
||||||
|
if (!storedMatch?.cube?.phaseAlertPending) {
|
||||||
|
clearCubePhaseAlertRetry();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryUnlockAudioContext();
|
||||||
|
if (!playCubePhaseAlert()) {
|
||||||
|
scheduleCubePhaseAlertRetry(storedMatch);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
storedMatch.cube.phaseAlertPending = false;
|
||||||
|
dirty = true;
|
||||||
|
persistMatch();
|
||||||
|
clearCubePhaseAlertRetry();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function initSetupPage() {
|
function initSetupPage() {
|
||||||
const form = document.querySelector("#setupForm");
|
const form = document.querySelector("#setupForm");
|
||||||
const summary = document.querySelector("#setupSummary");
|
const summary = document.querySelector("#setupSummary");
|
||||||
@@ -325,7 +444,25 @@ function initChronoPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cubeNavigationTimeoutId = null;
|
||||||
const goToCubePage = () => {
|
const goToCubePage = () => {
|
||||||
|
if (cubeNavigationTimeoutId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match?.cube?.phaseAlertPending) {
|
||||||
|
tryUnlockAudioContext();
|
||||||
|
if (playCubePhaseAlert()) {
|
||||||
|
match.cube.phaseAlertPending = false;
|
||||||
|
dirty = true;
|
||||||
|
persistMatch();
|
||||||
|
cubeNavigationTimeoutId = window.setTimeout(() => {
|
||||||
|
replaceTo("cube.html");
|
||||||
|
}, CUBE_PHASE_ALERT_NAVIGATION_DELAY_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dirty = true;
|
dirty = true;
|
||||||
persistMatch();
|
persistMatch();
|
||||||
replaceTo("cube.html");
|
replaceTo("cube.html");
|
||||||
@@ -681,6 +818,8 @@ function initCubePage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maybePlayPendingCubePhaseAlert(match);
|
||||||
|
|
||||||
const refs = {
|
const refs = {
|
||||||
title: document.querySelector("#cubeTitle"),
|
title: document.querySelector("#cubeTitle"),
|
||||||
subtitle: document.querySelector("#cubeSubtitle"),
|
subtitle: document.querySelector("#cubeSubtitle"),
|
||||||
@@ -790,6 +929,7 @@ function initCubePage() {
|
|||||||
resultModalKey = null;
|
resultModalKey = null;
|
||||||
replayCubePhase(match);
|
replayCubePhase(match);
|
||||||
dirty = true;
|
dirty = true;
|
||||||
|
maybePlayPendingCubePhaseAlert(match);
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -806,6 +946,7 @@ function initCubePage() {
|
|||||||
resultModalKey = null;
|
resultModalKey = null;
|
||||||
replayCubePhase(match);
|
replayCubePhase(match);
|
||||||
dirty = true;
|
dirty = true;
|
||||||
|
maybePlayPendingCubePhaseAlert(match);
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1303,6 +1444,7 @@ function createMatch(config) {
|
|||||||
running: false,
|
running: false,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
elapsedMs: 0,
|
elapsedMs: 0,
|
||||||
|
phaseAlertPending: false,
|
||||||
times: {
|
times: {
|
||||||
white: null,
|
white: null,
|
||||||
black: null,
|
black: null,
|
||||||
@@ -1405,6 +1547,7 @@ function normalizeRecoveredMatch(storedMatch) {
|
|||||||
running: false,
|
running: false,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
elapsedMs: 0,
|
elapsedMs: 0,
|
||||||
|
phaseAlertPending: false,
|
||||||
times: { white: null, black: null },
|
times: { white: null, black: null },
|
||||||
playerState: {
|
playerState: {
|
||||||
white: createCubePlayerState(),
|
white: createCubePlayerState(),
|
||||||
@@ -1424,6 +1567,11 @@ function normalizeRecoveredMatch(storedMatch) {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof storedMatch.cube.phaseAlertPending !== "boolean") {
|
||||||
|
storedMatch.cube.phaseAlertPending = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
storedMatch.cube.playerState.white = normalizeCubePlayerState(storedMatch.cube.playerState.white);
|
storedMatch.cube.playerState.white = normalizeCubePlayerState(storedMatch.cube.playerState.white);
|
||||||
storedMatch.cube.playerState.black = normalizeCubePlayerState(storedMatch.cube.playerState.black);
|
storedMatch.cube.playerState.black = normalizeCubePlayerState(storedMatch.cube.playerState.black);
|
||||||
storedMatch.cube.running = isAnyCubeTimerRunning(storedMatch);
|
storedMatch.cube.running = isAnyCubeTimerRunning(storedMatch);
|
||||||
@@ -1537,6 +1685,7 @@ function openCubePhase(storedMatch, reason = "") {
|
|||||||
storedMatch.cube.running = false;
|
storedMatch.cube.running = false;
|
||||||
storedMatch.cube.startedAt = null;
|
storedMatch.cube.startedAt = null;
|
||||||
storedMatch.cube.elapsedMs = 0;
|
storedMatch.cube.elapsedMs = 0;
|
||||||
|
storedMatch.cube.phaseAlertPending = true;
|
||||||
storedMatch.cube.times = { white: null, black: null };
|
storedMatch.cube.times = { white: null, black: null };
|
||||||
storedMatch.cube.playerState = {
|
storedMatch.cube.playerState = {
|
||||||
white: createCubePlayerState(),
|
white: createCubePlayerState(),
|
||||||
@@ -1730,6 +1879,7 @@ function replayCubePhase(storedMatch) {
|
|||||||
storedMatch.cube.running = false;
|
storedMatch.cube.running = false;
|
||||||
storedMatch.cube.startedAt = null;
|
storedMatch.cube.startedAt = null;
|
||||||
storedMatch.cube.elapsedMs = 0;
|
storedMatch.cube.elapsedMs = 0;
|
||||||
|
storedMatch.cube.phaseAlertPending = true;
|
||||||
storedMatch.cube.times = { white: null, black: null };
|
storedMatch.cube.times = { white: null, black: null };
|
||||||
storedMatch.cube.playerState = {
|
storedMatch.cube.playerState = {
|
||||||
white: createCubePlayerState(),
|
white: createCubePlayerState(),
|
||||||
|
|||||||
Reference in New Issue
Block a user