@@ -67,6 +67,8 @@ import {
6767 resolveTimelineSpanToTrim ,
6868 ventilateTimelineSpanToTrims ,
6969} from "@/lib/ai-edition/timeline/trim-mapping" ;
70+ import { effectiveZoomScale } from "@/lib/ai-edition/timeline/zoom-scale" ;
71+ import { transitionCutsMs , zoomTransitions } from "@/lib/ai-edition/timeline/zoom-transitions" ;
7072import { formatBinding } from "@/lib/shortcuts" ;
7173import { nativeBridgeClient } from "@/native/client" ;
7274import { TransportBar } from "../TransportBar" ;
@@ -742,7 +744,8 @@ export function V4Timeline({
742744 label : t ( "toolbar.newAnnotation" ) ,
743745 sourceIds : p . ids ,
744746 } ) ) ;
745- const speedPills : LanePill [ ] = coalesceRegionsForRuler ( tl . speedRegions ) . map ( ( p ) => ( {
747+ const speedRuns = coalesceRegionsForRuler ( tl . speedRegions ) ;
748+ const speedPills : LanePill [ ] = speedRuns . map ( ( p ) => ( {
746749 id : p . ids [ 0 ] ,
747750 kind : "speed" ,
748751 start : p . start ,
@@ -760,7 +763,8 @@ export function V4Timeline({
760763 sourceIds : p . ids ,
761764 } ) ,
762765 ) ;
763- const zoomPills : LanePill [ ] = coalesceRegionsForRuler ( tl . zoomRegions ) . map ( ( p ) => ( {
766+ const zoomRuns = coalesceRegionsForRuler ( tl . zoomRegions ) ;
767+ const zoomPills : LanePill [ ] = zoomRuns . map ( ( p ) => ( {
764768 id : p . ids [ 0 ] ,
765769 kind : "zoom" ,
766770 start : p . start ,
@@ -1748,13 +1752,14 @@ export function V4Timeline({
17481752 ) ;
17491753 } ;
17501754
1755+ // A pill's span as drawn: the live drag geometry while it is being dragged.
1756+ const liveSpan = < T extends { start : number ; end : number } > ( id : string , p : T ) : T =>
1757+ activePillDrag && activePillDrag . id === id
1758+ ? { ...p , start : activePillDrag . start , end : activePillDrag . end }
1759+ : p ;
1760+
17511761 const renderPills = ( pills : LanePill [ ] , emptyLabel : string ) => {
1752- const effectivePills = pills . map ( ( p ) => {
1753- if ( activePillDrag && activePillDrag . id === p . id ) {
1754- return { ...p , start : activePillDrag . start , end : activePillDrag . end } ;
1755- }
1756- return p ;
1757- } ) ;
1762+ const effectivePills = pills . map ( ( p ) => liveSpan ( p . id , p ) ) ;
17581763 return (
17591764 < >
17601765 { effectivePills . length === 0 ? (
@@ -1831,6 +1836,85 @@ export function V4Timeline({
18311836 ) ;
18321837 } ;
18331838
1839+ // Each zoom's camera moves (#1028): the pill shows the hold, the zoom-in runs in the trail
1840+ // before it and the zoom-out in the trail after it, on the speed-aware clock the compositor
1841+ // uses. The part a trim cuts is hatched: the export jumps there instead of easing. Display
1842+ // only, not a control. Hidden while a clip is dragged, when the pills split and slide.
1843+ const renderZoomTrails = ( ) => {
1844+ if ( clipDrag ) return null ;
1845+ const ms = ( span : { start : number ; end : number } ) => ( {
1846+ startMs : span . start * 1000 ,
1847+ endMs : span . end * 1000 ,
1848+ } ) ;
1849+ const speeds = speedRuns . map ( ( r ) => ( {
1850+ id : r . ids [ 0 ] ,
1851+ speed : r . member . speed ,
1852+ ...ms ( liveSpan ( r . ids [ 0 ] , r ) ) ,
1853+ } ) ) ;
1854+ const trims = trimPills . map ( ( p ) => ms ( liveSpan ( p . id , p ) ) ) ;
1855+ const totalMs = total * 1000 ;
1856+ const sec = ( cutMs : number ) => ( cutMs / 1000 ) . toFixed ( 2 ) ;
1857+ return zoomRuns . flatMap ( ( r ) => {
1858+ const zoom = ms ( liveSpan ( r . ids [ 0 ] , r ) ) ;
1859+ const moves = zoomTransitions ( { ...zoom , scale : effectiveZoomScale ( r . member ) } , speeds ) ;
1860+ const cut = transitionCutsMs ( zoom , moves , trims ) ;
1861+ const outUntilMs = Math . min ( moves . outUntilMs , totalMs ) ;
1862+ const trails = [
1863+ {
1864+ side : "in" ,
1865+ fromMs : moves . inFromMs ,
1866+ toMs : zoom . startMs ,
1867+ cutFromMs : moves . inFromMs ,
1868+ cutToMs : moves . inFromMs + cut . inMs ,
1869+ text :
1870+ cut . inMs > 0 ? t ( "trails.zoomInCut" , { seconds : sec ( cut . inMs ) } ) : t ( "trails.zoomIn" ) ,
1871+ } ,
1872+ {
1873+ side : "out" ,
1874+ fromMs : zoom . endMs ,
1875+ toMs : outUntilMs ,
1876+ cutFromMs : moves . outUntilMs - cut . outMs ,
1877+ cutToMs : outUntilMs ,
1878+ text :
1879+ cut . outMs > 0
1880+ ? t ( "trails.zoomOutCut" , { seconds : sec ( cut . outMs ) } )
1881+ : t ( "trails.zoomOut" ) ,
1882+ } ,
1883+ ] ;
1884+ return trails
1885+ . filter ( ( trail ) => trail . toMs > trail . fromMs )
1886+ . map ( ( trail ) => {
1887+ const lengthMs = trail . toMs - trail . fromMs ;
1888+ const cutMs = trail . cutToMs - trail . cutFromMs ;
1889+ return (
1890+ < Tooltip key = { `${ r . ids [ 0 ] } :${ trail . side } ` } content = { trail . text } >
1891+ < div
1892+ role = "img"
1893+ aria-label = { trail . text }
1894+ data-zoom-trail = { trail . side }
1895+ className = { `${ styles . zoomTrail } ${ styles . laneZoom } ` }
1896+ style = { {
1897+ left : `${ pctAt ( trail . fromMs / 1000 ) } %` ,
1898+ width : `${ pctOf ( lengthMs / 1000 ) } %` ,
1899+ } }
1900+ >
1901+ { cutMs > 0 ? (
1902+ < span
1903+ data-zoom-trail-cut = ""
1904+ className = { styles . zoomTrailCut }
1905+ style = { {
1906+ left : `${ ( ( trail . cutFromMs - trail . fromMs ) / lengthMs ) * 100 } %` ,
1907+ width : `${ ( cutMs / lengthMs ) * 100 } %` ,
1908+ } }
1909+ />
1910+ ) : null }
1911+ </ div >
1912+ </ Tooltip >
1913+ ) ;
1914+ } ) ;
1915+ } ) ;
1916+ } ;
1917+
18341918 return (
18351919 < div className = { styles . tl } ref = { panelRef } >
18361920 < div className = { styles . tlToolbar } >
@@ -2174,6 +2258,8 @@ export function V4Timeline({
21742258 ) }
21752259 </ div >
21762260 < div className = { styles . tlLane } >
2261+ { /* Its own provider, like the toolbar's: see the note there. */ }
2262+ < TooltipProvider > { renderZoomTrails ( ) } </ TooltipProvider >
21772263 { renderPills (
21782264 zoomPills ,
21792265 t ( "hints.pressZoom" , { key : formatBinding ( shortcuts . addZoom , isMac ) } ) ,
0 commit comments