From ff5b0028f1aa8d8e27ae6d0dfc03e5c53531cc65 Mon Sep 17 00:00:00 2001 From: isUnknown Date: Fri, 20 Mar 2026 14:29:41 +0100 Subject: [PATCH] =?UTF-8?q?fix(expertise):=20refonte=20logique=20vid=C3=A9?= =?UTF-8?q?o=20forward/reverse=20sans=20flash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problèmes corrigés : - Flash au changement de direction : l'ancien code attendait un événement seeked avant de switcher la visibilité, montrant un mauvais frame. Maintenant on positionne la vidéo cible AVANT de la rendre visible. - Reprise forward depuis mauvaise position : stopActiveVideo() + playForward() synchronisent correctement currentFwdTime avant le switch. - segmentEnds[0] = 0 → currentFwdTime snappé exactement à la cible dans les handlers timeupdate (plus de dérive à 0.1). Changements : - switchToForward/switchToReverse : positionnent la cible puis changent isReverse - playForward/playReverse simplifiés : plus de logique seeked conditionnelle - navigate() simplifié : forward=down, reverse=up, extension de cible si déjà actif - CSS : opacity 0/1 + transition au lieu de display none/block pour éviter les flashes - timeupdate : snap exact à la cible au lieu d'approximation Co-Authored-By: Claude Opus 4.6 --- src/views/Expertise.svelte | 111 ++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/src/views/Expertise.svelte b/src/views/Expertise.svelte index 99a6362..fbd6bbf 100644 --- a/src/views/Expertise.svelte +++ b/src/views/Expertise.svelte @@ -8,7 +8,6 @@ // --- Constants --- const PLAY_DELAY_MS = 300 // ms delay before starting playback on slide activation - const REV_TARGET_MIN = 0.1 // min revTarget to avoid pausing exactly at videoDuration // --- State --- let currentItem = $state(0) @@ -37,10 +36,12 @@ if (isActive) navigation.setScrolled(currentItem > 0) }) + // segmentEnds[i] = position forward correspondant à l'item i. + // Item 0 = début de vidéo (0), item final = fin de vidéo (duration). const segmentEnds = $derived( - itemCount > 0 && videoDuration > 0 - ? Array.from({ length: itemCount }, (_, i) => - itemCount === 1 ? videoDuration : videoDuration * i / (itemCount - 1)) + itemCount > 1 && videoDuration > 0 + ? Array.from({ length: itemCount }, (_, i) => videoDuration * i / (itemCount - 1)) + : itemCount === 1 ? [0] : [] ) @@ -68,6 +69,8 @@ } // --- Video control helpers --- + + /** Stoppe la vidéo active et sauvegarde la position forward-équivalente. */ function stopActiveVideo() { if (videoFwd && !videoFwd.paused) { currentFwdTime = videoFwd.currentTime @@ -81,53 +84,65 @@ } } + /** + * Switch visuel entre les deux vidéos. + * Positionne la vidéo cible au bon frame AVANT de l'afficher + * pour éliminer les flashes. + */ + function switchToForward() { + if (!isReverse) return + // Positionner forward au bon frame avant de l'afficher + if (videoFwd) videoFwd.currentTime = currentFwdTime + isReverse = false + } + + function switchToReverse() { + if (isReverse) return + // Positionner reverse au bon frame avant de l'afficher + const revPos = videoDuration - currentFwdTime + if (videoRev) videoRev.currentTime = revPos + isReverse = true + } + function playForward(targetTime) { fwdTarget = targetTime if (videoFwd) videoFwd.currentTime = currentFwdTime - if (isReverse) { - videoFwd?.addEventListener('seeked', () => { - isReverse = false - videoFwd?.play().catch(() => {}) - }, { once: true }) - } else { - videoFwd?.play().catch(() => {}) - } + // Switcher visuellement, puis lancer la lecture + switchToForward() + videoFwd?.play().catch(() => {}) } - function playReverse(targetTime) { - const revStart = Math.max(0, videoDuration - currentFwdTime) - revTarget = targetTime + function playReverse(targetFwdPos) { + // targetFwdPos = la position forward-équivalente où on veut arriver + // On la convertit en position dans la vidéo reverse + revTarget = videoDuration - Math.max(targetFwdPos, 0) + const revStart = videoDuration - currentFwdTime if (videoRev) videoRev.currentTime = revStart - if (!isReverse) { - videoRev?.addEventListener('seeked', () => { - isReverse = true - videoRev?.play().catch(() => {}) - }, { once: true }) - } else { - videoRev?.play().catch(() => {}) - } + // Switcher visuellement, puis lancer la lecture + switchToReverse() + videoRev?.play().catch(() => {}) } // --- Navigate: move one item up or down (called by composable) --- function navigate(direction, newItem) { - const prevItem = currentItem - currentItem = newItem + currentItem = newItem + const targetPos = segmentEnds[currentItem] - if (direction === 'down' && videoFwd && !videoFwd.paused) { - fwdTarget = segmentEnds[currentItem] - } else if (direction === 'up' && videoRev && !videoRev.paused) { - const liveFwdPos = videoDuration - videoRev.currentTime - const segBoundary = segmentEnds[currentItem] - const targetFwd = liveFwdPos > segBoundary ? segBoundary : 0 - revTarget = videoDuration - Math.max(targetFwd, REV_TARGET_MIN) - } else { - stopActiveVideo() - if (direction === 'down') { - playForward(segmentEnds[currentItem]) + if (direction === 'down') { + if (videoFwd && !videoFwd.paused) { + // Forward déjà en cours : juste mettre à jour la cible + fwdTarget = targetPos } else { - const segBoundary = segmentEnds[currentItem] - const targetFwd = currentFwdTime > segBoundary ? segBoundary : 0 - playReverse(videoDuration - Math.max(targetFwd, REV_TARGET_MIN)) + stopActiveVideo() + playForward(targetPos) + } + } else { + if (videoRev && !videoRev.paused) { + // Reverse déjà en cours : juste mettre à jour la cible + revTarget = videoDuration - Math.max(targetPos, 0) + } else { + stopActiveVideo() + playReverse(targetPos) } } @@ -137,7 +152,7 @@ // --- Playback lifecycle --- function initPlayback() { setTimeout(() => { - const tryPlay = () => { + const init = () => { const dur = videoFwd?.duration if (!dur || itemCount === 0) return videoDuration = dur @@ -148,11 +163,11 @@ requestAnimationFrame(() => computeOffset()) } if (videoFwd?.duration) { - tryPlay() + init() } else { videoFwd?.addEventListener('loadedmetadata', () => { videoDuration = videoFwd.duration - tryPlay() + init() }, { once: true }) } }, PLAY_DELAY_MS) @@ -177,7 +192,8 @@ const onFwdUpdate = () => { if (!videoFwd || videoFwd.paused || fwdTarget === null) return if (videoFwd.currentTime >= fwdTarget) { - currentFwdTime = videoFwd.currentTime + currentFwdTime = fwdTarget // snap exactement à la cible + videoFwd.currentTime = fwdTarget videoFwd.pause() fwdTarget = null } @@ -187,7 +203,9 @@ const onRevUpdate = () => { if (!videoRev || videoRev.paused || revTarget === null) return if (videoRev.currentTime >= revTarget) { - currentFwdTime = videoDuration - videoRev.currentTime + const fwdPos = videoDuration - revTarget + currentFwdTime = Math.max(fwdPos, 0) // snap exactement + videoRev.currentTime = revTarget videoRev.pause() revTarget = null } @@ -303,11 +321,12 @@ width: 100%; height: 100%; object-fit: cover; - display: none; + opacity: 0; + transition: opacity 0.15s ease; } .expertise-bg video.active { - display: block; + opacity: 1; } /* Custom vertical lines */