@@ -122,6 +122,11 @@ function createMockWebGl(
122122 } as unknown as WebGLRenderingContext ;
123123}
124124
125+ // Each clip shows its pixels on a 2D canvas, copied from the one shared WebGL canvas.
126+ function mock2d ( ) : CanvasRenderingContext2D {
127+ return { clearRect : vi . fn ( ) , drawImage : vi . fn ( ) } as unknown as CanvasRenderingContext2D ;
128+ }
129+
125130function makeDrawableVideo ( ) : HTMLVideoElement {
126131 const video = document . createElement ( "video" ) ;
127132 video . id = "hero-video" ;
@@ -182,6 +187,8 @@ describe("createColorGradingRuntime", () => {
182187 let getContextSpy : ReturnType < typeof vi . spyOn > ;
183188 let runtime : RuntimeColorGradingApi | null = null ;
184189
190+ const webglContexts = ( ) => getContextSpy . mock . calls . filter ( ( [ type ] ) => type === "webgl" ) . length ;
191+
185192 beforeEach ( ( ) => {
186193 document . body . innerHTML = "" ;
187194 lastUniform1f = null ;
@@ -192,7 +199,7 @@ describe("createColorGradingRuntime", () => {
192199 getContextSpy = vi
193200 . spyOn ( HTMLCanvasElement . prototype , "getContext" )
194201 . mockImplementation ( ( type : string ) =>
195- type === "webgl" ? createMockWebGl ( ) : null ,
202+ type === "webgl" ? createMockWebGl ( ) : type === "2d" ? mock2d ( ) : null ,
196203 ) as ReturnType < typeof vi . spyOn > ;
197204 } ) ;
198205
@@ -355,19 +362,49 @@ describe("createColorGradingRuntime", () => {
355362 const recreated = document . querySelector < HTMLCanvasElement > ( "[data-hf-color-grading-canvas]" ) ;
356363 expect ( recreated ) . not . toBeNull ( ) ;
357364 expect ( recreated ) . toBe ( canvas ) ;
358- expect ( getContextSpy ) . toHaveBeenCalledTimes ( 1 ) ;
365+ expect ( webglContexts ( ) ) . toBe ( 1 ) ;
359366 expect ( video . style . getPropertyValue ( "opacity" ) ) . toBe ( "0" ) ;
360367 expect ( video . style . getPropertyPriority ( "opacity" ) ) . toBe ( "important" ) ;
361368 } ) ;
362369
370+ it ( "draws every graded clip through one WebGL context, however often a seek run shows and hides them" , ( ) => {
371+ const clips = Array . from ( { length : 6 } , ( _ , i ) => {
372+ const video = makeDrawableVideo ( ) ;
373+ video . id = `clip-${ i } ` ;
374+ video . style . display = "none" ;
375+ document . body . appendChild ( video ) ;
376+ return video ;
377+ } ) ;
378+ runtime = createColorGradingRuntime ( ) ;
379+
380+ // Each seek shows two neighbouring clips and hides the rest, as scenes come and go.
381+ for ( const at of [ 0 , 2 , 4 , 1 , 3 , 5 , 0 , 4 ] ) {
382+ clips . forEach ( ( video , i ) => {
383+ const shown = i === at || i === ( at + 1 ) % clips . length ;
384+ video . style . display = shown ? "block" : "none" ;
385+ runtime ?. setSourceVisibility ( video , shown ) ;
386+ } ) ;
387+ }
388+
389+ expect ( webglContexts ( ) ) . toBe ( 1 ) ;
390+ const shared = getContextSpy . mock . instances [ 0 ] ;
391+ const copies = getContextSpy . mock . results
392+ . filter ( ( _ , i ) => getContextSpy . mock . calls [ i ] ?. [ 0 ] === "2d" )
393+ . flatMap (
394+ ( { value } ) => ( value as { drawImage : ReturnType < typeof vi . fn > } ) . drawImage . mock . calls ,
395+ ) ;
396+ expect ( copies . length ) . toBeGreaterThan ( 0 ) ;
397+ expect ( copies . every ( ( [ source ] ) => source === shared ) ) . toBe ( true ) ;
398+ } ) ;
399+
363400 it ( "defers WebGL setup for hidden attributed media until it becomes visible" , ( ) => {
364401 const video = makeDrawableVideo ( ) ;
365402 video . style . display = "none" ;
366403 document . body . appendChild ( video ) ;
367404
368405 runtime = createColorGradingRuntime ( ) ;
369406
370- expect ( getContextSpy ) . not . toHaveBeenCalled ( ) ;
407+ expect ( webglContexts ( ) ) . toBe ( 0 ) ;
371408 expect ( document . querySelector ( "[data-hf-color-grading-canvas]" ) ) . toBeNull ( ) ;
372409 expect ( runtime . getStatus ( video ) ) . toEqual ( {
373410 state : "pending" ,
@@ -376,7 +413,7 @@ describe("createColorGradingRuntime", () => {
376413
377414 video . style . display = "block" ;
378415 expect ( runtime . setSourceVisibility ( video , true ) ) . toBe ( true ) ;
379- expect ( getContextSpy ) . toHaveBeenCalledTimes ( 1 ) ;
416+ expect ( webglContexts ( ) ) . toBe ( 1 ) ;
380417 expect ( document . querySelector ( "[data-hf-color-grading-canvas]" ) ) . not . toBeNull ( ) ;
381418 } ) ;
382419
@@ -406,12 +443,12 @@ describe("createColorGradingRuntime", () => {
406443 { id : "warm" , dataUrl : "data:image/png;base64,preview" } ,
407444 ] ,
408445 } ) ;
409- expect ( getContextSpy ) . toHaveBeenCalledTimes ( 1 ) ;
446+ expect ( webglContexts ( ) ) . toBe ( 1 ) ;
410447 expect ( document . querySelector ( "[data-hf-color-grading-canvas]" ) ) . toBeNull ( ) ;
411448 expect ( video . style . opacity ) . toBe ( "" ) ;
412449
413450 await runtime . renderPreviews ( "#hero-video" , [ { id : "mono" , grading : "mono-clean" } ] ) ;
414- expect ( getContextSpy ) . toHaveBeenCalledTimes ( 1 ) ;
451+ expect ( webglContexts ( ) ) . toBe ( 1 ) ;
415452
416453 const retinaBatch = await runtime . renderPreviews (
417454 "#hero-video" ,
@@ -717,7 +754,7 @@ describe("createColorGradingRuntime", () => {
717754
718755 runtime = createColorGradingRuntime ( ) ;
719756
720- expect ( getContextSpy ) . toHaveBeenCalledTimes ( 1 ) ;
757+ expect ( webglContexts ( ) ) . toBe ( 1 ) ;
721758 if ( ! lastUniform1f ) throw new Error ( "Expected WebGL uniform calls" ) ;
722759 expect ( lastUniform1f ) . toHaveBeenCalledWith ( "u_intensity" , 0 ) ;
723760 expect ( lastUniform1f ) . toHaveBeenCalledWith ( "u_kuwahara" , 0 ) ;
@@ -1037,7 +1074,7 @@ describe("createColorGradingRuntime", () => {
10371074
10381075 runtime = createColorGradingRuntime ( ) ;
10391076
1040- expect ( getContextSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1077+ expect ( webglContexts ( ) ) . toBe ( 1 ) ;
10411078 expect ( document . querySelector ( "[data-hf-color-grading-canvas]" ) ) . not . toBeNull ( ) ;
10421079 } ) ;
10431080
@@ -1079,7 +1116,7 @@ describe("createColorGradingRuntime", () => {
10791116 expect ( runtime . setSourceVisibility ( video , false ) ) . toBe ( true ) ;
10801117 runtime . refresh ( ) ;
10811118 expect ( canvas . isConnected ) . toBe ( false ) ;
1082- expect ( getContextSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1119+ expect ( webglContexts ( ) ) . toBe ( 1 ) ;
10831120 expect ( video . hasAttribute ( "data-hf-color-grading-source-hidden" ) ) . toBe ( false ) ;
10841121 } ) ;
10851122
@@ -1329,7 +1366,11 @@ describe("createColorGradingRuntime", () => {
13291366
13301367 it ( "falls back to the untreated shader output when half-float targets are unavailable" , ( ) => {
13311368 getContextSpy . mockImplementation ( ( type : string ) =>
1332- type === "webgl" ? createMockWebGl ( { halfFloatSupported : false } ) : null ,
1369+ type === "webgl"
1370+ ? createMockWebGl ( { halfFloatSupported : false } )
1371+ : type === "2d"
1372+ ? mock2d ( )
1373+ : null ,
13331374 ) ;
13341375 const video = makeDrawableVideo ( ) ;
13351376 video . setAttribute (
@@ -1785,7 +1826,11 @@ describe("createColorGradingRuntime", () => {
17851826
17861827 it ( "reports media texture upload failures in runtime status" , ( ) => {
17871828 getContextSpy . mockImplementation ( ( type : string ) =>
1788- type === "webgl" ? createMockWebGl ( { failMediaUpload : true } ) : null ,
1829+ type === "webgl"
1830+ ? createMockWebGl ( { failMediaUpload : true } )
1831+ : type === "2d"
1832+ ? mock2d ( )
1833+ : null ,
17891834 ) ;
17901835 const video = makeDrawableVideo ( ) ;
17911836 document . body . appendChild ( video ) ;
@@ -1800,9 +1845,10 @@ describe("createColorGradingRuntime", () => {
18001845
18011846 it ( "falls back to the source media on WebGL context loss and redraws after restore" , ( ) => {
18021847 const { video, canvas } = startRuntimeWithVideo ( ) ;
1848+ const shared = getContextSpy . mock . instances [ 0 ] as HTMLCanvasElement ;
18031849
18041850 const lost = new Event ( "webglcontextlost" , { cancelable : true } ) ;
1805- canvas . dispatchEvent ( lost ) ;
1851+ shared . dispatchEvent ( lost ) ;
18061852
18071853 expect ( lost . defaultPrevented ) . toBe ( true ) ;
18081854 expect ( canvas . style . display ) . toBe ( "none" ) ;
@@ -1813,7 +1859,7 @@ describe("createColorGradingRuntime", () => {
18131859 message : "WebGL context lost" ,
18141860 } ) ;
18151861
1816- canvas . dispatchEvent ( new Event ( "webglcontextrestored" ) ) ;
1862+ shared . dispatchEvent ( new Event ( "webglcontextrestored" ) ) ;
18171863
18181864 expect ( runtime ?. getStatus ( "#hero-video" ) . state ) . toBe ( "active" ) ;
18191865 expect ( video . hasAttribute ( "data-hf-color-grading-source-hidden" ) ) . toBe ( true ) ;
0 commit comments