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:
parent
dd69e54746
commit
ff5b0028f1
1 changed files with 65 additions and 46 deletions
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
const PLAY_DELAY_MS = 300 // ms delay before starting playback on slide activation
|
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 ---
|
// --- State ---
|
||||||
let currentItem = $state(0)
|
let currentItem = $state(0)
|
||||||
|
|
@ -37,10 +36,12 @@
|
||||||
if (isActive) navigation.setScrolled(currentItem > 0)
|
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(
|
const segmentEnds = $derived(
|
||||||
itemCount > 0 && videoDuration > 0
|
itemCount > 1 && videoDuration > 0
|
||||||
? Array.from({ length: itemCount }, (_, i) =>
|
? Array.from({ length: itemCount }, (_, i) => videoDuration * i / (itemCount - 1))
|
||||||
itemCount === 1 ? videoDuration : videoDuration * i / (itemCount - 1))
|
: itemCount === 1 ? [0]
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -68,6 +69,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Video control helpers ---
|
// --- Video control helpers ---
|
||||||
|
|
||||||
|
/** Stoppe la vidéo active et sauvegarde la position forward-équivalente. */
|
||||||
function stopActiveVideo() {
|
function stopActiveVideo() {
|
||||||
if (videoFwd && !videoFwd.paused) {
|
if (videoFwd && !videoFwd.paused) {
|
||||||
currentFwdTime = videoFwd.currentTime
|
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) {
|
function playForward(targetTime) {
|
||||||
fwdTarget = targetTime
|
fwdTarget = targetTime
|
||||||
if (videoFwd) videoFwd.currentTime = currentFwdTime
|
if (videoFwd) videoFwd.currentTime = currentFwdTime
|
||||||
if (isReverse) {
|
// Switcher visuellement, puis lancer la lecture
|
||||||
videoFwd?.addEventListener('seeked', () => {
|
switchToForward()
|
||||||
isReverse = false
|
|
||||||
videoFwd?.play().catch(() => {})
|
videoFwd?.play().catch(() => {})
|
||||||
}, { once: true })
|
|
||||||
} else {
|
|
||||||
videoFwd?.play().catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function playReverse(targetTime) {
|
function playReverse(targetFwdPos) {
|
||||||
const revStart = Math.max(0, videoDuration - currentFwdTime)
|
// targetFwdPos = la position forward-équivalente où on veut arriver
|
||||||
revTarget = targetTime
|
// 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 (videoRev) videoRev.currentTime = revStart
|
||||||
if (!isReverse) {
|
// Switcher visuellement, puis lancer la lecture
|
||||||
videoRev?.addEventListener('seeked', () => {
|
switchToReverse()
|
||||||
isReverse = true
|
|
||||||
videoRev?.play().catch(() => {})
|
videoRev?.play().catch(() => {})
|
||||||
}, { once: true })
|
|
||||||
} else {
|
|
||||||
videoRev?.play().catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Navigate: move one item up or down (called by composable) ---
|
// --- Navigate: move one item up or down (called by composable) ---
|
||||||
function navigate(direction, newItem) {
|
function navigate(direction, newItem) {
|
||||||
const prevItem = currentItem
|
|
||||||
currentItem = newItem
|
currentItem = newItem
|
||||||
|
const targetPos = segmentEnds[currentItem]
|
||||||
|
|
||||||
if (direction === 'down' && videoFwd && !videoFwd.paused) {
|
if (direction === 'down') {
|
||||||
fwdTarget = segmentEnds[currentItem]
|
if (videoFwd && !videoFwd.paused) {
|
||||||
} else if (direction === 'up' && videoRev && !videoRev.paused) {
|
// Forward déjà en cours : juste mettre à jour la cible
|
||||||
const liveFwdPos = videoDuration - videoRev.currentTime
|
fwdTarget = targetPos
|
||||||
const segBoundary = segmentEnds[currentItem]
|
|
||||||
const targetFwd = liveFwdPos > segBoundary ? segBoundary : 0
|
|
||||||
revTarget = videoDuration - Math.max(targetFwd, REV_TARGET_MIN)
|
|
||||||
} else {
|
} else {
|
||||||
stopActiveVideo()
|
stopActiveVideo()
|
||||||
if (direction === 'down') {
|
playForward(targetPos)
|
||||||
playForward(segmentEnds[currentItem])
|
}
|
||||||
} else {
|
} else {
|
||||||
const segBoundary = segmentEnds[currentItem]
|
if (videoRev && !videoRev.paused) {
|
||||||
const targetFwd = currentFwdTime > segBoundary ? segBoundary : 0
|
// Reverse déjà en cours : juste mettre à jour la cible
|
||||||
playReverse(videoDuration - Math.max(targetFwd, REV_TARGET_MIN))
|
revTarget = videoDuration - Math.max(targetPos, 0)
|
||||||
|
} else {
|
||||||
|
stopActiveVideo()
|
||||||
|
playReverse(targetPos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,7 +152,7 @@
|
||||||
// --- Playback lifecycle ---
|
// --- Playback lifecycle ---
|
||||||
function initPlayback() {
|
function initPlayback() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const tryPlay = () => {
|
const init = () => {
|
||||||
const dur = videoFwd?.duration
|
const dur = videoFwd?.duration
|
||||||
if (!dur || itemCount === 0) return
|
if (!dur || itemCount === 0) return
|
||||||
videoDuration = dur
|
videoDuration = dur
|
||||||
|
|
@ -148,11 +163,11 @@
|
||||||
requestAnimationFrame(() => computeOffset())
|
requestAnimationFrame(() => computeOffset())
|
||||||
}
|
}
|
||||||
if (videoFwd?.duration) {
|
if (videoFwd?.duration) {
|
||||||
tryPlay()
|
init()
|
||||||
} else {
|
} else {
|
||||||
videoFwd?.addEventListener('loadedmetadata', () => {
|
videoFwd?.addEventListener('loadedmetadata', () => {
|
||||||
videoDuration = videoFwd.duration
|
videoDuration = videoFwd.duration
|
||||||
tryPlay()
|
init()
|
||||||
}, { once: true })
|
}, { once: true })
|
||||||
}
|
}
|
||||||
}, PLAY_DELAY_MS)
|
}, PLAY_DELAY_MS)
|
||||||
|
|
@ -177,7 +192,8 @@
|
||||||
const onFwdUpdate = () => {
|
const onFwdUpdate = () => {
|
||||||
if (!videoFwd || videoFwd.paused || fwdTarget === null) return
|
if (!videoFwd || videoFwd.paused || fwdTarget === null) return
|
||||||
if (videoFwd.currentTime >= fwdTarget) {
|
if (videoFwd.currentTime >= fwdTarget) {
|
||||||
currentFwdTime = videoFwd.currentTime
|
currentFwdTime = fwdTarget // snap exactement à la cible
|
||||||
|
videoFwd.currentTime = fwdTarget
|
||||||
videoFwd.pause()
|
videoFwd.pause()
|
||||||
fwdTarget = null
|
fwdTarget = null
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +203,9 @@
|
||||||
const onRevUpdate = () => {
|
const onRevUpdate = () => {
|
||||||
if (!videoRev || videoRev.paused || revTarget === null) return
|
if (!videoRev || videoRev.paused || revTarget === null) return
|
||||||
if (videoRev.currentTime >= revTarget) {
|
if (videoRev.currentTime >= revTarget) {
|
||||||
currentFwdTime = videoDuration - videoRev.currentTime
|
const fwdPos = videoDuration - revTarget
|
||||||
|
currentFwdTime = Math.max(fwdPos, 0) // snap exactement
|
||||||
|
videoRev.currentTime = revTarget
|
||||||
videoRev.pause()
|
videoRev.pause()
|
||||||
revTarget = null
|
revTarget = null
|
||||||
}
|
}
|
||||||
|
|
@ -303,11 +321,12 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: none;
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expertise-bg video.active {
|
.expertise-bg video.active {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom vertical lines */
|
/* Custom vertical lines */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue