fix(expertise): refonte logique vidéo forward/reverse sans flash

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 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-03-20 14:29:41 +01:00
parent dd69e54746
commit ff5b0028f1

View file

@ -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 */