diff --git a/app.js b/app.js index 5493621..2daf59f 100644 --- a/app.js +++ b/app.js @@ -7,7 +7,7 @@ const DEFAULT_BLOCK_DURATION_MS = 180000; const DEFAULT_MOVE_LIMIT_MS = 20000; const TIME_MODE_INITIAL_CLOCK_MS = 600000; const CUBE_TIME_CAP_MS = 120000; -const CUBE_START_HOLD_MS = 300; +const CUBE_START_HOLD_MS = 2000; const PRESETS = { fast: { @@ -584,6 +584,7 @@ function initCubePage() { white: createCubeHoldIntent(), black: createCubeHoldIntent(), }; + let cubeHoldAnimationFrameId = null; const openModal = () => toggleModal(refs.helpModal, true); const closeModal = () => toggleModal(refs.helpModal, false); @@ -693,6 +694,7 @@ function initCubePage() { clearCubeHoldTimeout(holdIntent); holdIntent.armed = true; holdIntent.ready = false; + holdIntent.startedAt = Date.now(); holdIntent.pointerId = event.pointerId; holdIntent.timeoutId = window.setTimeout(() => { holdIntent.timeoutId = null; @@ -711,6 +713,7 @@ function initCubePage() { } render(); + ensureCubeHoldAnimation(); } function handleCubePressEnd(color, button, event) { @@ -771,7 +774,44 @@ function initCubePage() { clearCubeHoldTimeout(holdIntent); holdIntent.armed = false; holdIntent.ready = false; + holdIntent.startedAt = 0; holdIntent.pointerId = null; + stopCubeHoldAnimationIfIdle(); + } + + function ensureCubeHoldAnimation() { + if (cubeHoldAnimationFrameId !== null) { + return; + } + + const tick = () => { + cubeHoldAnimationFrameId = null; + if (!isCubeHoldAnimating()) { + render(); + return; + } + + render(); + cubeHoldAnimationFrameId = window.requestAnimationFrame(tick); + }; + + cubeHoldAnimationFrameId = window.requestAnimationFrame(tick); + } + + function stopCubeHoldAnimationIfIdle() { + if (cubeHoldAnimationFrameId === null || isCubeHoldAnimating()) { + return; + } + + window.cancelAnimationFrame(cubeHoldAnimationFrameId); + cubeHoldAnimationFrameId = null; + } + + function isCubeHoldAnimating() { + return Object.entries(cubeHoldState).some(([color, holdIntent]) => { + const playerState = match.cube.playerState[color]; + return holdIntent.armed && !holdIntent.ready && !playerState.running && match.cube.times[color] === null; + }); } function clearCubeHoldTimeout(holdIntent) { @@ -793,12 +833,16 @@ function initCubePage() { const time = match.cube.times[color]; const holdArmed = holdIntent.armed && !playerState.running && time === null && match.phase === "cube" && !match.result; const holdReady = holdIntent.ready && holdArmed; + const holdProgress = holdArmed + ? Math.min((Date.now() - holdIntent.startedAt) / CUBE_START_HOLD_MS, 1) + : 0; name.textContent = playerName(match, color); result.textContent = formatCubePlayerTime(match, color); cap.textContent = renderCubeCap(match, time); button.classList.toggle("cube-hold-arming", holdArmed && !holdReady); button.classList.toggle("cube-hold-ready", holdReady); + button.style.setProperty("--cube-hold-progress", `${holdProgress}`); if (match.result) { button.textContent = resultText(match); @@ -836,15 +880,15 @@ function initCubePage() { } if (holdArmed) { - button.textContent = "Maintenez..."; + button.textContent = "Maintenez 2 s..."; button.disabled = false; - hint.textContent = "Gardez le doigt pose un court instant pour armer le depart."; + hint.textContent = "Gardez le doigt pose 2 secondes, jusqu'a la fin de la barre."; return; } - button.textContent = "Maintenir pour demarrer"; + button.textContent = "Maintenir 2 s pour demarrer"; button.disabled = false; - hint.textContent = "Maintenez la grande zone, puis relachez pour lancer votre chrono."; + hint.textContent = "Maintenez la grande zone 2 secondes, puis relachez pour lancer votre chrono."; } function render() { @@ -1787,6 +1831,7 @@ function createCubeHoldIntent() { return { armed: false, ready: false, + startedAt: 0, pointerId: null, timeoutId: null, }; diff --git a/styles.css b/styles.css index 738be76..6b06f50 100644 --- a/styles.css +++ b/styles.css @@ -711,9 +711,38 @@ textarea:focus { } body[data-page="cube"] .zone-button { + position: relative; + overflow: hidden; touch-action: none; -webkit-user-select: none; user-select: none; + --cube-hold-progress: 0; +} + +body[data-page="cube"] .zone-button::before, +body[data-page="cube"] .zone-button::after { + content: ""; + position: absolute; + left: 1rem; + right: 1rem; + bottom: 0.9rem; + height: 0.38rem; + border-radius: 999px; + pointer-events: none; +} + +body[data-page="cube"] .zone-button::before { + background: rgba(255, 255, 255, 0.12); + opacity: 0; + transition: opacity 120ms ease; +} + +body[data-page="cube"] .zone-button::after { + background: linear-gradient(90deg, rgba(255, 255, 255, 0.4), currentColor); + transform: scaleX(var(--cube-hold-progress)); + transform-origin: left center; + opacity: 0; + transition: opacity 120ms ease; } body[data-page="cube"] .zone-button.cube-hold-arming { @@ -722,6 +751,13 @@ body[data-page="cube"] .zone-button.cube-hold-arming { box-shadow: inset 0 0 0 1px currentColor; } +body[data-page="cube"] .zone-button.cube-hold-arming::before, +body[data-page="cube"] .zone-button.cube-hold-arming::after, +body[data-page="cube"] .zone-button.cube-hold-ready::before, +body[data-page="cube"] .zone-button.cube-hold-ready::after { + opacity: 1; +} + body[data-page="cube"] .zone-button.cube-hold-ready { transform: none; filter: brightness(1.08); @@ -730,6 +766,10 @@ body[data-page="cube"] .zone-button.cube-hold-ready { 0 0 24px rgba(255, 255, 255, 0.08); } +body[data-page="cube"] .zone-button.cube-hold-ready::after { + transform: scaleX(1); +} + .zone-button:hover { transform: translateY(-2px); filter: brightness(1.04);