39948-vm/frontend/docs/navigation-smooth-transitions.md
2026-07-03 16:11:24 +02:00

31 KiB

Navigation & Smooth Transitions Analysis

Executive Summary

Deep analysis of how navigation works in the Tour Builder Platform - from Constructor editing to Runtime Presentations, across Online and Offline modes. This document traces each navigation thread step-by-step to verify that smooth transitions are robust.

Architecture Update: The page navigation system was refactored from 6+ fragmented hooks into a unified state machine (usePageNavigationState). This consolidation:

  • Prevents race conditions via atomic useReducer transitions
  • Uses explicit phases instead of boolean flag combinations
  • Computes derived state (isLoading, showSpinner, etc.) from a single phase value
  • Provides a PageNavigationContext for child component access

1. NAVIGATION SYSTEM ARCHITECTURE

1.1 Core Hooks (Shared Between Constructor & RuntimePresentation)

Hook File Purpose
usePageNavigation hooks/usePageNavigation.ts Page navigation state with history tracking, browser-like back behavior
usePageNavigationState hooks/usePageNavigationState.ts Unified state machine - URL resolution, switching, fade effects, cleanup (replaces 6 hooks)
useTransitionPlayback hooks/useTransitionPlayback.ts Transition video playback, last frame preservation, pre-generated reverse support
usePreloadOrchestrator hooks/usePreloadOrchestrator.ts Asset preloading with blob URL cache
useNetworkAware hooks/useNetworkAware.ts Network condition monitoring

Architecture Refactor Note: The following hooks were consolidated into usePageNavigationState:

  • usePageSwitch - Page switching with blob URL resolution
  • useBackgroundState - Background ready tracking
  • useBackgroundTransition - CSS animation-based crossfade
  • useTransitionCleanup - Video cleanup coordination
  • useBackgroundUrls - URL resolution for display
  • pageLoadingUtils - Loading state computation

1.2 Component Integration

RuntimePresentation.tsx / constructor.tsx
    |
+----------------------------------------------------------------+
|  usePageNavigation                                              |
|  - Unified page history management (MAX_HISTORY_LENGTH = 50)    |
|  - Browser-like back: history pops when isBack=true             |
|  - Provides getNavigationContext() for resolveNavigationTarget()|
|  - applyPageSelection(targetPageId, isBack) handles history     |
+----------------------------------------------------------------+
    |
+----------------------------------------------------------------+
|  usePreloadOrchestrator                                         |
|  - Preloads assets for current + neighbor pages                 |
|  - Provides getReadyBlobUrl() for O(1) instant lookup           |
|  - Provides getCachedBlobUrl() for Cache API lookup             |
+----------------------------------------------------------------+
    |
+----------------------------------------------------------------+
|  usePageNavigationState (UNIFIED STATE MACHINE)                 |
|  - navigateToPage() initiates navigation with URL resolution    |
|  - Tracks phase: idle → preparing → transitioning/loading_bg    |
|                → transition_done → fading_in → idle             |
|  - Maintains currentImageUrl, previousImageUrl for overlay      |
|  - Provides derived state: isLoading, showSpinner, showElements |
|  - onBackgroundReady() callback for CanvasBackground            |
|  - onTransitionEnded() callback for video transition completion |
|  - Atomic state via useReducer (prevents race conditions)       |
+----------------------------------------------------------------+
    |
+----------------------------------------------------------------+
|  useTransitionPlayback                                          |
|  - Plays transition videos with presigned URL fallback          |
|  - Preserves last frame until onComplete callback               |
|  - Uses pre-generated reversed videos for back navigation       |
|  - Passes isBack to onComplete for proper history management    |
+----------------------------------------------------------------+

Note: usePageNavigationState consolidated 6 hooks into a single state machine:

  • URL resolution, switching, overlay management (from usePageSwitch)
  • Background ready tracking (from useBackgroundState)
  • Fade-out coordination (from useBackgroundTransition)
  • Video cleanup (from useTransitionCleanup)
  • URL resolution for display (from useBackgroundUrls)
  • Loading state computation (from pageLoadingUtils)

2. NAVIGATION WITHOUT TRANSITION VIDEO

2.1 Flow Overview

User clicks navigation element (no transitionVideoUrl)
    |
handleElementClick / navigateToPage
    |
Direct navigation - no transition video

2.2 Step-by-Step Thread Analysis

Thread 1: Navigation Trigger

handleElementClick(element)                    [RuntimePresentation:309-340]
    |
resolveNavigationTarget(element, pages)        [lib/navigationHelpers.ts]
    +-- Extract targetPageSlug from element
    +-- Find target page in pages array
    +-- Return { pageId, transitionVideoUrl, isBack }
    |
navigateToPage(targetPageId, undefined, false) [No transition video]

Thread 2: Page Switch Initiation

navigateToPage(targetPageId, undefined, isBack) [RuntimePresentation:285-315]
    |
No transitionVideoUrl -> Direct navigation path
    |
setIsBackgroundReady(false)                    [Mark: waiting for new bg]
lastInitializedPageIdRef.current = targetPageId
    |
await pageSwitch.switchToPage(targetPage, () => {
    applyPageSelection(targetPageId, isBack);  [usePageNavigation hook]
    // - If isBack=true AND target matches previous: history pops
    // - Otherwise: history appends (trimmed to MAX_HISTORY_LENGTH=50)
});

Thread 3: URL Resolution & Overlay Setup

switchToPage(targetPage, onSwitched)           [usePageSwitch:372-419]
    |
Save current as previous for overlay:
    setPreviousBgImageUrl(currentBgImageUrlRef.current)
    |
setIsSwitching(true)
setIsNewBgReady(false)
    |
Resolve URLs in parallel (prefer preloaded blob URLs):
    [imageUrl, videoUrl, audioUrl] = await Promise.all([
        resolveToDisplayUrl(background_image_url),
        resolveMediaUrl(background_video_url),
        resolveMediaUrl(background_audio_url),
    ])

Thread 4: Blob URL Resolution Priority

resolveToDisplayUrl(storagePath)               [usePageSwitch:237-306]
    |
1. getReadyBlobUrl(storagePath)                [O(1) Map lookup - same session]
       -> If found: return immediately (instant)
    |
2. getCachedBlobUrl(storagePath)               [Cache API lookup ~5ms]
       -> If found: create blob URL, return
    |
3. resolveAssetPlaybackUrl(storagePath)        [Resolve to playback URL]
       |
4. getReadyBlobUrl(resolvedUrl)                [Fallback lookup]
       -> If found: return
    |
5. getCachedBlobUrl(resolvedUrl)               [Fallback Cache API]
       -> If found: return
    |
6. loadImageWithFallback(originalUrl, storageKey)  [Network fetch]
       +-- Try presigned URL
       +-- On CORS failure: markPresignedUrlFailed()
       +-- Retry with proxy URL: /api/file/download

Thread 5: Background Display & Overlay

After URL resolution:
    setCurrentBgImageUrl(imageUrl)
    setCurrentBgVideoUrl(videoUrl)
    setCurrentBgAudioUrl(audioUrl)
    onSwitched()                               [Notify caller]
    |
For blob URLs (local data):
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            setIsNewBgReady(true)              [Mark ready after 2 frames]
        })
    })
    |
For remote images:
    Wait for Image onLoad callback
    -> markBackgroundReady()

Thread 6: Render - Previous Background Overlay

RuntimePresentation render                      [lines 517-528]
    |
{pageSwitch.previousBgImageUrl &&
 pageSwitch.isSwitching &&
 !pageSwitch.isNewBgReady && (
    <div className="absolute inset-0 z-10"
         style={{ backgroundImage: `url("${previousBgImageUrl}")` }}
    />
)}
    |
Previous page background stays visible until new one is ready!

Thread 7: Overlay Clearing

useBackgroundTransition effect                  [lines 144-158]
    |
When: isSwitching && isNewBgReady && previousBgImageUrl
    |
pageSwitch.clearPreviousBackground()
    +-- setPreviousBgImageUrl('')
    +-- setIsSwitching(false)
    +-- revokeBlobUrl(prevUrl)                 [Memory cleanup]

2.3 Summary: Navigation WITHOUT Transition Video

Phase What's Visible State
1. Click Current page isSwitching: false
2. Switch starts Previous bg (z-0) fading out, New content (z-1) fading in isSwitching: true, isFadingIn: true
3. Crossfade Both visible with CSS animation animate-crossfade-out / animate-crossfade-in
4. Animation ends New page fully visible onAnimationEnd fires, isFadingIn: false
5. Cleanup New page only clearPreviousBackground() called

2.4 CSS Animation-Based Crossfade

The crossfade effect uses CSS animations instead of JS-controlled transitions:

CSS Variables (main.css) - Single Source of Truth:

:root {
  --crossfade-duration: 700ms;
  --crossfade-easing: cubic-bezier(0.4, 0, 0.2, 1);
}

CSS Classes (main.css):

.animate-crossfade-in {
  animation: page-crossfade-in var(--crossfade-duration, 700ms) var(--crossfade-easing) forwards;
}

.animate-crossfade-out {
  animation: page-crossfade-out var(--crossfade-duration, 700ms) var(--crossfade-easing) forwards;
}

Easing Characteristics:

  • cubic-bezier(0.4, 0, 0.2, 1) - Material Design standard
  • Slow start prevents abrupt appearance
  • Smooth acceleration and soft landing

Why CSS Animations (not transitions):

  • CSS animations always play when the class is added
  • Immune to React's render batching that can skip transition states
  • onAnimationEnd event provides reliable completion detection
  • Duration controlled via CSS variable (single source of truth)
  • JS can read duration via getCrossfadeDuration() utility

Hook Usage:

const { isFadingIn, onFadeInAnimationEnd } = useBackgroundTransition({
  pageSwitch,
  fadeIn: { hasActiveTransition: false },
});

// In JSX:
<div
  className={`absolute inset-0 z-1 ${isFadingIn ? 'animate-crossfade-in' : ''}`}
  onAnimationEnd={onFadeInAnimationEnd}
>
  {/* New page content */}
</div>

// Previous background:
{previousBgImageUrl && isFadingIn && (
  <div className={`absolute inset-0 z-0 ${isFadingIn ? 'animate-crossfade-out' : ''}`}>
    ...
  </div>
)}

ROBUSTNESS CHECK: Previous page renders while next page background and UI elements not ready


3. NAVIGATION WITH TRANSITION VIDEO

3.1 Flow Overview

User clicks navigation element (has transitionVideoUrl)
    |
handleElementClick / navigateToPage
    |
Start transition video -> Play -> Keep last frame -> Switch page -> Fade out

3.2 Step-by-Step Thread Analysis

Thread 1: Transition Initiation

navigateToPage(targetPageId, transitionVideoUrl, isBack)  [RuntimePresentation:272-307]
    |
Has transitionVideoUrl -> Transition path
    |
resetFadeOut()                                 [Clear previous fade state]
setPendingTransitionComplete(false)
    |
setTransitionPreview({
    targetPageId,
    videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl),
    storageKey: transitionVideoUrl,            [Raw path for cache lookup]
    isReverse: isBack,
})

Thread 2: Transition Video Source Resolution

useTransitionPlayback effect triggers          [lines 355-771]
    |
resolvePlayableSource()                        [lines 431-540]
    |
1. getReadyBlobUrl(storageKey)                 [O(1) lookup by storage path]
       -> If found: use cached blob URL
    |
2. getCachedBlobUrl(storageKey)                [Cache API by storage path]
       -> If found: create blob URL, cache it
    |
3. Check lastLoadedBlobUrlRef (reuse same session)
    |
4. getReadyBlobUrl(sourceUrl)                  [Lookup by resolved URL]
    |
5. getCachedBlobUrl(sourceUrl)                 [Cache API by resolved URL]
    |
6. Network fetch as blob                       [Fallback - full download]
       -> axios.get(requestUrl, { responseType: 'blob' })
       -> URL.createObjectURL(blob)

Thread 3: Video Playback

loadAndPlay()                                  [lines 542-598]
    |
video.src = playableSourceUrl
video.currentTime = 0
video.load()
attemptPlay()
    |
onPlaying event fires                          [lines 633-675]
    |
setPhase('playing')
    |
scheduleFinishByDuration(durationSec)          [lines 405-421]
    +-- finishBeforeEndMs = 50                 [Finish 50ms BEFORE end]
    +-- finishMs = durationSec * 1000 - 50
    +-- setTimeout(() => finishPlayback('duration-timer'), finishMs)

Thread 4: Last Frame Preservation (CRITICAL)

finishPlayback(reason)                         [lines 244-293]
    |
didFinishRef.current = true
clearTimers()
    |
video.pause()
    |
*** LAST FRAME PRESERVATION ***
if (video.duration && Number.isFinite(video.duration) &&
    video.currentTime >= video.duration - 0.1) {
    video.currentTime = Math.max(0, video.duration - 0.05)
}
    |
Explanation:
- Some browsers show BLACK after 'ended' event
- Seeking to duration - 0.05 keeps last visible frame
- This ensures smooth visual continuity
    |
setPhase('finishing')
    |
Optional: waitForImages() for target page pre-decode
    |
setPhase('completed')
onCompleteRef.current(targetPageId)

Thread 5: Page Switch After Transition

onComplete callback (targetPageId, isBack)     [RuntimePresentation:146-166]
    |
if (targetPageId) {
    const targetPage = pages.find(p => p.id === targetPageId)
    lastInitializedPageIdRef.current = targetPageId
    |
    await pageSwitch.switchToPage(targetPage, () => {
        applyPageSelection(targetPageId, isBack ?? false)  [usePageNavigation]
        // Proper history management: pops on back, appends on forward
    })
    |
    setIsBackgroundReady(false)
    setPendingTransitionComplete(true)         [Signal: waiting for bg ready]
}

Thread 6: Background Image Load Detection

Background image element in render             [lines 476-514]
    |
<img onLoad={() => {
    setIsBackgroundReady(true)
    pageSwitch.markBackgroundReady()
}} />
    |
Or for video backgrounds:
useEffect auto-marks ready when no image or has video

Thread 7: Video Transition Overlay Removal (Instant, With rAF Delay)

TransitionPreviewOverlay.tsx
    |
When: isFadingOut=true passed from parent
    |
useEffect triggers:
    if (isFadingOut) {
        requestAnimationFrame(() => {
            setShouldHide(true)   [After one paint frame]
        })
    }
    |
Container opacity:
    - 0 during initial buffering (before first frame)
    - 0 when shouldHide=true (after rAF delay - bg is painted)
    - 1 otherwise (video playing)
    |
transition: 'none' when hiding [NO CSS transition - instant hide]

Thread 8: Render - Transition Overlay Visibility

TransitionPreviewOverlay component             [TransitionPreviewOverlay.tsx]
    |
Container opacity controlled by:
    - isBuffering && !isVideoReady → 0 (initial buffering)
    - shouldHide → 0 (after rAF delay, new bg painted)
    - otherwise → opacity prop or 1
    |
transition: useTransition ? '150ms' : 'none'
    - 150ms ONLY for initial buffering fade-in
    - 'none' when hiding (instant hide since last video frame = new bg)
    |
{transitionPreview && (
    <TransitionPreviewOverlay
        isBuffering={transitionPhase === 'preparing' || isBuffering}
        isFadingOut={pendingTransitionComplete && isBackgroundReady}
    />
)}
    |
Container hidden while: preparing, buffering  (old page visible through)
Container visible when: video ready to play   (instant appearance)
Overlay removed: instantly after rAF (ensures bg is painted first)

Key Design Decision - Instant Hide with rAF Delay:

  • Video itself IS the transition effect
  • First frame = old page background
  • Last frame = new page background
  • Wait one requestAnimationFrame after isFadingOut=true
  • This ensures new background is painted before hiding overlay
  • Overlay hides instantly (no CSS transition) - seamless since last frame = new bg
  • No visual discontinuity or flash

3.3 Summary: Navigation WITH Transition Video

Phase What's Visible State
1. Click Current page transitionPhase: 'idle'
2. Preparing Current page (container hidden) transitionPhase: 'preparing', isBuffering: true
3. Playing Transition video transitionPhase: 'playing'
4. Finishing Last frame of video transitionPhase: 'finishing'
5. Completed Last frame of video pendingTransitionComplete: true
6. Bg loading Last frame of video (z-50) New bg loading underneath
7. Bg ready Instant switch setTransitionPreview(null) (no fade!)
8. Done New page only Overlay removed

Key Changes from Previous Implementation:

  • Container hidden during buffering (no black flash)
  • No fade-out animation for video transitions
  • Instant overlay removal when background ready
  • Video itself is the transition (first frame = old, last frame = new)

ROBUSTNESS CHECK: Last video frame renders while next page background and UI elements not ready

3.4 PreviousBackgroundOverlay (Simplified)

The PreviousBackgroundOverlay component was simplified to remove all fade logic:

// PreviousBackgroundOverlay.tsx - Simplified implementation
const PreviousBackgroundOverlay = ({
  imageUrl,
  isSwitching = false,
  isNewBgReady = false,
  className = '',
}) => {
  // Simple render logic: show while switching AND new bg not ready
  const shouldRender = isSwitching && !isNewBgReady && !!imageUrl;

  if (!shouldRender) return null;

  return (
    <div
      className={`pointer-events-none absolute inset-0 z-2 ${className}`}
      style={{
        backgroundImage: `url("${imageUrl}")`,
        backgroundSize: 'contain',
        backgroundPosition: 'center',
        backgroundRepeat: 'no-repeat',
      }}
    />
  );
};

Changes from previous implementation:

  • Removed all CSS transition/fade logic
  • Removed timeout fallbacks
  • Removed transition event handlers
  • Simplified to pure show/hide based on props
  • Hides instantly when isNewBgReady=true

3.5 scheduleAfterPaint Helper

The CanvasBackground.tsx uses a scheduleAfterPaint helper for video first-frame detection:

/**
 * Schedule a callback to run after the next browser paint.
 * Uses double rAF pattern: first rAF schedules for next frame,
 * second rAF ensures the frame has actually been committed.
 */
const scheduleAfterPaint = (callback: () => void): void => {
  requestAnimationFrame(() => {
    requestAnimationFrame(callback);
  });
};

Usage in video first-frame detection:

// Using requestVideoFrameCallback for accurate first-frame detection
if ('requestVideoFrameCallback' in video) {
  video.requestVideoFrameCallback(() => {
    clearTimeout(timeout);
    scheduleAfterPaint(() => {
      reportVideoReady();  // Called after frame is painted
    });
  });
}

4. ONLINE VS OFFLINE MODE

4.1 Network Awareness

useNetworkAware hook                           [hooks/useNetworkAware.ts]
    |
Monitors:
    - navigator.onLine
    - connection.effectiveType (slow-2g, 2g, 3g, 4g)
    - connection.downlink (Mbps)
    - connection.rtt (ms)
    - connection.saveData (boolean)
    |
Returns:
    - networkInfo.isOnline
    - recommendedConcurrency (1-3 based on connection)
    - shouldPreloadAggressively
    - suggestOfflineMode

4.2 Preload Orchestrator - Online Mode

processQueue()                                 [usePreloadOrchestrator:355-485]
    |
Guard: if (!networkInfo.isOnline) return       [Skip if offline]
    |
While queue has items && activeDownloads < recommendedConcurrency:
    +-- Check preloadedUrls.has(url) -> skip if already loaded
    +-- Check StorageManager.hasAsset(url) -> skip if cached
    |   +-- If cached: createReadyBlobUrl() from cache
    |
    preloadWithProgress(url, jobId, assetId)
        +-- fetch(url) with streaming progress
        +-- Store in Cache API
        +-- createReadyBlobUrl()

4.3 Preload Orchestrator - Offline Mode

processQueue()                                 [usePreloadOrchestrator:355-485]
    |
Guard: if (!networkInfo.isOnline) return       [Don't process queue when offline]
    |
BUT: isUrlCached check still works!
    +-- StorageManager.hasAsset(url)
    |   +-- Check IndexedDB: OfflineDbManager.hasAssetByUrl(url)
    |   +-- Check Cache API: caches.open().match(url)
    |
Assets already downloaded are still accessible!

4.4 Navigation Flow - Same for Both Modes

The navigation flow is identical for online and offline modes:

  1. URL Resolution - resolveToDisplayUrl() / resolvePlayableSource()

    • Tries getReadyBlobUrl() first (in-memory Map - O(1))
    • Tries getCachedBlobUrl() second (Cache API + IndexedDB)
    • Falls back to network only if not cached
  2. Blob URL Priority - Cache is always checked first

    Online:  blob URL (cache) > presigned URL > proxy URL
    Offline: blob URL (cache) > FAIL if not cached
    
  3. Transition Video - Same playback logic

    • If cached: plays from blob URL (instant)
    • If not cached: depends on network availability

4.5 Offline Storage Hierarchy

Asset lookup order (StorageManager)
    |
1. IndexedDB (files >= 5MB)
   +-- OfflineDbManager.getAssetByUrl(url)
    |
2. Cache API (files < 5MB)
   +-- caches.open('tour-builder-assets-v1').match(url)
    |
3. Network (online only)
   +-- fetch(presignedUrl || proxyUrl)

4.6 Summary: Online vs Offline Differences

Aspect Online Mode Offline Mode
Preload queue Active processing Paused (no new downloads)
Asset resolution Cache -> Network Cache only
Transition video Cache -> Network Cache only
Background image Cache -> Network Cache only
Navigation flow Same Same
Overlay behavior Same Same
Last frame handling Same Same

ROBUSTNESS CHECK: Same navigation flow works in both online and offline modes


5. CONSTRUCTOR VS RUNTIME PRESENTATION

5.1 Shared Components

Both use the same hooks:

  • usePageNavigationState - Unified state machine for URL resolution, overlay management, fade effects
  • useTransitionPlayback - Video playback, last frame preservation
  • usePreloadOrchestrator - Asset preloading
  • usePageNavigation - History tracking

5.2 Key Differences

Aspect Constructor RuntimePresentation
State management usePageNavigationState (unified) usePageNavigationState (unified)
Crossfade animation Yes (project transition settings) Yes (project transition settings)
Crossfade easing From transitionSettings.fadeEasing From transitionSettings.fadeEasing
Transition video fade-out No (instant removal) No (instant removal)
Video overlay hiding Container hidden while buffering Container hidden while buffering
Background transition config Via usePageNavigationState Via usePageNavigationState
Overlay component TransitionPreviewOverlay TransitionPreviewOverlay
Post-transition cleanup Via onTransitionEnded + onBackgroundReady Via onTransitionEnded + onBackgroundReady
Animation end detection CSS onAnimationEnd event CSS onAnimationEnd event
Edit mode support Direct background updates (setBackgroundDirectly) N/A

5.3 Constructor Transition Flow

onComplete callback (targetPageId, isBack)     [constructor.tsx]
    |
if (targetPageId) {
    await navState.navigateToPage(targetPage, {
        hasTransition: false,
        isBack: isBack ?? false,
        onSwitched: () => applyPageSelection(targetPage.id, isBack ?? false)
    })
    // navigateToPage handles all state transitions atomically
    // usePageNavigation handles history (pops on back, appends on forward)
    clearSelection()
    setSelectedMenuItem('none')
}
    |
navState.onTransitionEnded() is called when transition video completes
    |
Video cleanup happens via effect watching pendingTransitionComplete + isBackgroundReady

Both constructor and runtime presentation use the same usePageNavigationState hook for unified state management. The usePageNavigation hook handles browser-like history behavior.


6. ROBUSTNESS VERIFICATION

6.1 Scenario: Navigation WITHOUT Transition (Direct)

Step What Happens Verified
1 User clicks navigation element YES
2 Previous bg saved to previousBgImageUrl YES
3 isSwitching: true, isNewBgReady: false YES
4 URL resolution (blob -> cache -> network) YES
5 Previous bg overlay renders (z-10) YES
6 New bg loads underneath (z-1) YES
7 Image onLoad -> markBackgroundReady() YES
8 clearPreviousBackground() removes overlay YES

6.2 Scenario: Navigation WITH Transition Video

Step What Happens Verified
1 User clicks navigation element YES
2 setTransitionPreview() triggers hook YES
3 Video source resolved (blob -> cache -> network) YES
4 Video plays (opacity: 1) YES
5 Timer fires 50ms before video ends YES
6 finishPlayback() seeks to duration - 0.05 YES
7 Last frame stays visible YES
8 onComplete() triggers page switch YES
9 New bg loads underneath overlay YES
10 Image onLoad -> isBackgroundReady: true YES
11 Overlay fades out (opacity: 0 transition) YES
12 Cleanup: remove video src, clear state YES

6.3 Scenario: Reverse Navigation (Back)

Step What Happens Verified
1 User clicks back navigation YES
2 isBack: true set, uses reverseVideoUrl YES
3 Pre-generated reversed video loaded YES
4 Forward playback of reversed video YES
5 Video ends normally YES
6 onComplete() -> finishPlayback() YES
7 Same cleanup flow as forward YES

Note: Reversed videos are pre-generated server-side using FFmpeg when pages are saved. This eliminates client-side frame-stepping and ensures professional audio/video synchronization.

6.4 Scenario: Offline Mode

Step What Happens Verified
1 networkInfo.isOnline: false YES
2 Preload queue paused YES
3 Navigation clicked YES
4 URL resolved from cache (IndexedDB/Cache API) YES
5 If cached: works identically to online YES
6 If not cached: fails gracefully YES

7. CONCLUSION

The navigation and smooth transitions system is robust and comprehensive:

7.1 Key Strengths

  1. Unified State Machine - usePageNavigationState consolidates 6 hooks into atomic state transitions
  2. Race Condition Prevention - useReducer ensures state updates are atomic (no batching issues)
  3. Explicit Phase Tracking - Single phase value (idle, preparing, transitioning, etc.) instead of multiple boolean flags
  4. Derived State via useMemo - isLoading, showSpinner, showElements computed from phase
  5. Unified History Management - usePageNavigation hook handles history in both components
  6. Browser-Like Back Navigation - History pops when navigating back (via applyPageSelection(id, isBack=true))
  7. History Limit - MAX_HISTORY_LENGTH=50 prevents unbounded growth in long sessions
  8. Blob URL Priority - Always checks cache before network
  9. CSS Animation Crossfade - Uses CSS animations (not transitions) for reliable visual effects
  10. Smooth Easing - Project-configurable transition easing
  11. Event-Based Completion - onAnimationEnd event replaces timer-based duration tracking
  12. Previous Background Overlay - Keeps previous page visible during direct navigation
  13. Last Frame Preservation - Seeks to duration - 0.05 to avoid black frame
  14. Timer-Based Finish - Finishes 50ms before video ends to catch last frame
  15. No Video Fade - Video transitions end instantly without fade-out animation
  16. Container Hidden While Buffering - Prevents black flash before video ready
  17. Offline Resilience - Same flow works when offline (if assets cached)
  18. Context Provider - PageNavigationContext allows child components to access navigation state

7.2 No Gaps Identified

  • State consistency: Single useReducer prevents race conditions from async batching
  • Invalid states impossible: State machine phases prevent flag combinations like isLoading && showElements
  • Pages without transition: Previous page renders while next loads (crossfade animation)
  • Pages with transition: Last frame renders while next loads (instant switch when ready)
  • Video overlay: Container hidden while buffering (no black flash)
  • Online mode: Full preloading and network fallback
  • Offline mode: Graceful cache-first behavior
  • Constructor: Same usePageNavigationState hook as RuntimePresentation
  • Edit mode: Direct background updates via setBackgroundDirectly() bypass navigation flow

8. CRITICAL CODE LOCATIONS

Feature File Lines
Page navigation with history usePageNavigation.ts 62-195
History limit (MAX=50) usePageNavigation.ts 8
Browser-like history pop usePageNavigation.ts 120-127
Navigation context usePageNavigation.ts 164-170
Unified state machine usePageNavigationState.ts Full file (~520 LOC)
State machine reducer usePageNavigationState.ts 80-180
Navigation phases usePageNavigationState.ts 30-45
URL resolution usePageNavigationState.ts 200-280
Derived state (isLoading, etc.) usePageNavigationState.ts 350-400
onBackgroundReady callback usePageNavigationState.ts 420-450
Transition video playback useTransitionPlayback.ts 355-771
Last frame preservation useTransitionPlayback.ts 251-262
Timer-based finish useTransitionPlayback.ts 405-421
isBack in onComplete useTransitionPlayback.ts 290-296
CSS variables (duration, easing) css/main.css 15-20
CSS animations css/main.css 42-120
getCrossfadeDuration utility lib/browserUtils.ts 14-32
TransitionPreviewOverlay Constructor/TransitionPreviewOverlay.tsx 1-75
PageNavigationContext provider context/PageNavigationContext.tsx Full file (~120 LOC)
Network awareness useNetworkAware.ts 68-163
Server-side reversal backend/src/services/videoProcessing.ts N/A
Preload queue guard usePreloadOrchestrator.ts 355-358
Navigation helpers lib/navigationHelpers.ts 1-120

Deleted Files (consolidated into usePageNavigationState.ts):

  • usePageSwitch.ts (~475 LOC)
  • useBackgroundState.ts (~156 LOC)
  • useBackgroundTransition.ts (~164 LOC)
  • useTransitionCleanup.ts (~108 LOC)
  • useBackgroundUrls.ts (~104 LOC)
  • lib/pageLoadingUtils.ts (~76 LOC)