@@ -14,16 +14,17 @@ const DrawComposite = (
1414 gl : THREE . WebGLRenderer ,
1515 width : number ,
1616 height : number ,
17- colors : { bgColor : string , textColor : string }
17+ colors : { bgColor : string , textColor : string } ,
18+ animate = false ,
1819) : HTMLCanvasElement | undefined => {
1920
2021 const { bgColor, textColor} = colors ;
2122 const { doubleSize, includeBackground, mainTitle,
2223 cbarLabel, cbarLoc, cbarNum, includeColorbar} = useImageExportStore . getState ( )
2324 const { valueScales, variable, metadata } = useGlobalStore . getState ( )
24-
2525 const ctx = compositeCanvas . getContext ( '2d' )
2626 if ( ! ctx ) { return }
27+
2728 ctx . imageSmoothingEnabled = true ;
2829 ctx . imageSmoothingQuality = 'high' ;
2930 if ( includeBackground ) {
@@ -35,38 +36,26 @@ const DrawComposite = (
3536
3637 ctx . drawImage ( gl . domElement , 0 , 0 , width , height )
3738
38- // ---- TITLE ---- //
39- const variableSize = doubleSize ? 72 : 36
40- ctx . fillStyle = textColor
41- ctx . font = `${ variableSize } px "Segoe UI"`
42- ctx . textBaseline = 'middle'
43- ctx . fillText ( mainTitle ?? variable , doubleSize ? 40 : 20 , doubleSize ? 100 : 50 ) // Variable in top Left
44-
4539 const cbarTickSize = doubleSize ? 36 : 18
4640 const unitSize = doubleSize ? 52 : 26
47-
48- // ---- COLORBAR ---- //
49- if ( includeColorbar ) {
50- const secondCanvas = document . getElementById ( 'colorbar-canvas' )
5141
52- let cbarWidth = doubleSize ? Math . min ( 1024 , width * 0.8 ) : Math . min ( 512 , width * 0.8 )
53- let cbarHeight = doubleSize ? 48 : 24 ;
42+ let cbarWidth = doubleSize ? Math . min ( 1024 , width * 0.8 ) : Math . min ( 512 , width * 0.8 )
43+ let cbarHeight = doubleSize ? 48 : 24 ;
5444
55- let cbarStartPos = Math . round ( width / 2 - cbarWidth / 2 )
56- let cbarTop = cbarLoc === 'top' ? ( doubleSize ? 140 : 70 ) : ( doubleSize ? height - 140 : height - 70 )
57-
58- const transpose = cbarLoc === 'right' || cbarLoc === 'left'
59-
60- if ( transpose ) {
61- const tempWidth = cbarWidth
62- cbarWidth = cbarHeight
63- cbarHeight = tempWidth
64- cbarTop = Math . round ( height / 2 - cbarHeight / 2 )
65- cbarStartPos = cbarLoc === 'right' ? ( doubleSize ? width - 140 : width - 70 ) : ( doubleSize ? 140 : 70 )
66- }
45+ let cbarStartPos = Math . round ( width / 2 - cbarWidth / 2 )
46+ let cbarTop = cbarLoc === 'top' ? ( doubleSize ? 140 : 70 ) : ( doubleSize ? height - 140 : height - 70 )
47+ const transpose = cbarLoc === 'right' || cbarLoc === 'left'
6748
49+ // ---- COLORBAR ---- //
50+ if ( includeColorbar ) {
51+ const secondCanvas = document . getElementById ( 'colorbar-canvas' )
6852 if ( secondCanvas instanceof HTMLCanvasElement ) {
6953 if ( transpose ) {
54+ const tempWidth = cbarWidth
55+ cbarWidth = cbarHeight
56+ cbarHeight = tempWidth
57+ cbarTop = Math . round ( height / 2 - cbarHeight / 2 )
58+ cbarStartPos = cbarLoc === 'right' ? ( doubleSize ? width - 140 : width - 70 ) : ( doubleSize ? 140 : 70 )
7059 // Save the current canvas state
7160 ctx . save ( )
7261
@@ -88,21 +77,129 @@ const DrawComposite = (
8877
8978 // Restore the canvas state
9079 ctx . restore ( )
91- } else if ( cbarLoc === 'top' ) {
80+ } else if ( cbarLoc === 'top' ) {
9281 ctx . drawImage ( secondCanvas , cbarStartPos , cbarTop , cbarWidth , cbarHeight )
93- } else {
82+ } else {
9483 ctx . drawImage ( secondCanvas , cbarStartPos , cbarTop , cbarWidth , cbarHeight )
9584 }
9685 }
86+ }
87+
88+ // ---- TEXT ---- //
89+ if ( ! animate ) { // If still image write text onto image
90+ // ---- TITLE ---- //
91+ const variableSize = doubleSize ? 72 : 36
92+ ctx . fillStyle = textColor
93+ ctx . font = `${ variableSize } px "Segoe UI"`
94+ ctx . textBaseline = 'middle'
95+ ctx . fillText ( mainTitle ?? variable , doubleSize ? 40 : 20 , doubleSize ? 100 : 50 ) // Variable in top Left
96+
97+ // ---- WATERMARK ---- //
98+ const waterMarkSize = doubleSize ? 40 : 20
99+ ctx . fillStyle = "#888888"
100+ ctx . font = `${ waterMarkSize } px "Segoe UI", serif `
101+ ctx . textAlign = 'left'
102+ ctx . textBaseline = 'bottom'
103+ ctx . fillText ( "browzarr.io" , doubleSize ? 20 : 10 , doubleSize ? height - 20 : height - 10 ) // Watermark
104+
105+ if ( includeColorbar ) {
106+ // ---- TickLabels ---- //
107+ ctx . font = `${ cbarTickSize } px "Segoe UI"`
108+ const labelNum = cbarNum ; // Number of cbar "ticks"
109+ const valRange = valueScales . maxVal - valueScales . minVal ;
110+ const valScale = 1 / ( labelNum - 1 )
111+ const posDelta = transpose ? 1 / ( labelNum - 1 ) * cbarHeight : 1 / ( labelNum - 1 ) * cbarWidth
112+ if ( transpose ) {
113+ const tempWidth = cbarWidth
114+ cbarWidth = cbarHeight
115+ cbarHeight = tempWidth
116+ cbarTop = Math . round ( height / 2 - cbarHeight / 2 )
117+ cbarStartPos = cbarLoc === 'right' ? ( doubleSize ? width - 140 : width - 70 ) : ( doubleSize ? 140 : 70 )
118+ ctx . textBaseline = 'middle'
119+ ctx . textAlign = cbarLoc == 'left' ? 'left' : 'right'
120+ for ( let i = 0 ; i < labelNum ; i ++ ) {
121+ if ( cbarLoc == 'left' ) {
122+ ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos + cbarWidth + 6 , cbarTop + cbarHeight - i * posDelta )
123+ } else {
124+ ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos - 6 , cbarTop + cbarHeight - i * posDelta )
125+ }
126+ }
127+ } else {
128+ ctx . textBaseline = 'top'
129+ ctx . textAlign = 'center'
130+ for ( let i = 0 ; i < labelNum ; i ++ ) {
131+ ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos + i * posDelta , cbarTop + cbarHeight + 6 )
132+ }
133+ }
134+
135+ // ---- Cbar Label/Units ---- //
136+ ctx . fillStyle = textColor
137+ ctx . font = `${ unitSize } px "Segoe UI" bold`
138+ ctx . textAlign = 'center'
139+ ctx . fillText ( cbarLabel ?? metadata ?. units , cbarStartPos + cbarWidth / 2 , cbarTop - unitSize - 4 )
140+ }
141+ }
142+
143+ }
144+
145+ async function DrawTextOverlay (
146+ width : number ,
147+ height : number ,
148+ textColor :string ,
149+ ffmpeg : FFmpeg
150+ ) {
151+ const scaling = 4 ;
152+ const { doubleSize, mainTitle,
153+ cbarLabel, cbarLoc, cbarNum, includeColorbar} = useImageExportStore . getState ( )
154+ const { valueScales, variable, metadata } = useGlobalStore . getState ( )
155+ const textCanvas = document . createElement ( "canvas" ) ;
156+ textCanvas . width = width * scaling ;
157+ textCanvas . height = height * scaling ;
158+ const ctx = textCanvas . getContext ( "2d" ) ;
159+ if ( ! ctx ) return ;
160+
161+ ctx . scale ( scaling , scaling )
162+ const cbarTickSize = doubleSize ? 36 : 18
163+ const unitSize = doubleSize ? 52 : 26
164+
165+ let cbarWidth = doubleSize ? Math . min ( 1024 , width * 0.8 ) : Math . min ( 512 , width * 0.8 )
166+ let cbarHeight = doubleSize ? 48 : 24 ;
167+
168+ let cbarStartPos = Math . round ( width / 2 - cbarWidth / 2 )
169+ let cbarTop = cbarLoc === 'top' ? ( doubleSize ? 140 : 70 ) : ( doubleSize ? height - 140 : height - 70 )
170+ const transpose = cbarLoc === 'right' || cbarLoc === 'left'
171+
172+ // ---- TEXT ---- //
173+
174+ // ---- TITLE ---- //
175+ const variableSize = doubleSize ? 72 : 36
176+ ctx . fillStyle = textColor
177+ ctx . font = `${ variableSize } px "Segoe UI"`
178+ ctx . textBaseline = 'middle'
179+ ctx . textAlign = 'left'
180+ ctx . fillText ( mainTitle ?? variable , doubleSize ? 40 : 20 , doubleSize ? 100 : 50 ) // Variable in top Left
181+
182+ // ---- WATERMARK ---- //
183+ const waterMarkSize = doubleSize ? 40 : 20
184+ ctx . fillStyle = "#888888"
185+ ctx . font = `${ waterMarkSize } px "Segoe UI", serif `
186+ ctx . textBaseline = 'bottom'
187+ ctx . fillText ( "browzarr.io" , doubleSize ? 20 : 10 , doubleSize ? height - 20 : height - 10 ) // Watermark
188+
189+ if ( includeColorbar ) {
190+ // ---- TickLabels ---- //
191+ ctx . font = `${ cbarTickSize } px "Segoe UI"` ;
192+ ctx . fillStyle = textColor ;
97193 const labelNum = cbarNum ; // Number of cbar "ticks"
98194 const valRange = valueScales . maxVal - valueScales . minVal ;
99195 const valScale = 1 / ( labelNum - 1 )
100196 const posDelta = transpose ? 1 / ( labelNum - 1 ) * cbarHeight : 1 / ( labelNum - 1 ) * cbarWidth
101-
102- // ---- TickLabels ---- //
103- ctx . font = `${ cbarTickSize } px "Segoe UI"`
104-
105197 if ( transpose ) {
198+ const tempWidth = cbarWidth
199+ cbarWidth = cbarHeight
200+ cbarHeight = tempWidth
201+ cbarTop = Math . round ( height / 2 - cbarHeight / 2 )
202+ cbarStartPos = cbarLoc === 'right' ? ( doubleSize ? width - 140 : width - 70 ) : ( doubleSize ? 140 : 70 )
106203 ctx . textBaseline = 'middle'
107204 ctx . textAlign = cbarLoc == 'left' ? 'left' : 'right'
108205 for ( let i = 0 ; i < labelNum ; i ++ ) {
@@ -112,30 +209,32 @@ const DrawComposite = (
112209 ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos - 6 , cbarTop + cbarHeight - i * posDelta )
113210 }
114211 }
115- } else {
212+ } else {
116213 ctx . textBaseline = 'top'
117214 ctx . textAlign = 'center'
118215 for ( let i = 0 ; i < labelNum ; i ++ ) {
119216 ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos + i * posDelta , cbarTop + cbarHeight + 6 )
120217 }
121218 }
122219
220+ // ---- Cbar Label/Units ---- //
123221 ctx . fillStyle = textColor
124222 ctx . font = `${ unitSize } px "Segoe UI" bold`
125223 ctx . textAlign = 'center'
126- ctx . fillText ( cbarLabel ?? metadata ?. units , cbarStartPos + cbarWidth / 2 , cbarTop - unitSize - 4 ) // Cbar Units above middle of cbar
224+ ctx . fillText ( cbarLabel ?? metadata ?. units , cbarStartPos + cbarWidth / 2 , cbarTop - unitSize - 4 )
127225 }
128226
129- const waterMarkSize = doubleSize ? 40 : 20
130- ctx . fillStyle = "#888888"
131- ctx . font = `${ waterMarkSize } px "Segoe UI", serif `
132- ctx . textAlign = 'left'
133- ctx . textBaseline = 'bottom'
134- ctx . fillText ( "browzarr.io" , doubleSize ? 20 : 10 , doubleSize ? height - 20 : height - 10 ) // Watermark
227+ const blob = await new Promise ( resolve => {
228+ textCanvas . toBlob ( resolve , 'image/png' ) ;
229+ } ) ;
230+ if ( blob ) {
231+ const buf = await ( blob as Blob ) . arrayBuffer ( ) ;
232+ // Write frames to internal ffMpeg filesystem
233+ await ffmpeg . writeFile ( `textOverlay.png` , new Uint8Array ( buf ) ) ;
234+ }
135235}
136236
137237
138-
139238const ExportCanvas = ( { show} :{ show : boolean } ) => {
140239 const { exportImg, enableExport, animate, frames, frameRate, useTime, timeRate, orbit, loopTime,
141240 animViz, initialState, finalState, preview, useCustomRes, customRes, doubleSize, setHideAxis, setHideAxisControls
@@ -161,7 +260,6 @@ const ExportCanvas = ({show}:{show: boolean}) => {
161260
162261 const origQuality = usePlotStore . getState ( ) . quality ;
163262 setQuality ( preview ? 50 : 1000 ) ;
164-
165263 const domWidth = gl . domElement . width ;
166264 const domHeight = gl . domElement . height ;
167265 let docWidth = useCustomRes ? customRes [ 0 ] : ( doubleSize ? domWidth * 2 : domWidth ) ;
@@ -180,22 +278,19 @@ const ExportCanvas = ({show}:{show: boolean}) => {
180278 const originalSize = gl . getSize ( new THREE . Vector2 ( ) )
181279 let originalCameraSettings : any = { } ;
182280
183- if ( useCustomRes || doubleSize ) {
281+ function SetCamera ( ) {
184282 if ( camera instanceof THREE . PerspectiveCamera ) {
185283 originalCameraSettings = { aspect : camera . aspect }
186284 camera . aspect = docWidth / docHeight
187- camera . updateProjectionMatrix ( )
188285 } else if ( camera instanceof THREE . OrthographicCamera ) {
189286 originalCameraSettings = {
190287 left : camera . left ,
191288 right : camera . right ,
192289 top : camera . top ,
193290 bottom : camera . bottom
194291 }
195-
196292 const newAspect = docWidth / docHeight
197293 const currentAspect = ( camera . right - camera . left ) / ( camera . top - camera . bottom )
198-
199294 if ( newAspect > currentAspect ) {
200295 // Wider - expand left/right
201296 const width = ( camera . top - camera . bottom ) * newAspect
@@ -209,9 +304,11 @@ const ExportCanvas = ({show}:{show: boolean}) => {
209304 camera . top = center + height / 2
210305 camera . bottom = center - height / 2
211306 }
212- camera . updateProjectionMatrix ( )
213307 }
308+
214309 gl . setSize ( docWidth , docHeight )
310+ camera . updateProjectionMatrix ( )
311+ invalidate ( ) ;
215312 }
216313
217314 if ( animate ) {
@@ -221,6 +318,7 @@ const ExportCanvas = ({show}:{show: boolean}) => {
221318 if ( ! ffmpeg . loaded ) {
222319 await ffmpeg . load ( ) ;
223320 }
321+
224322 ffmpeg . on ( 'progress' , ( { progress, time } ) => {
225323 // progress is a value between 0 and 1
226324 setProgress ( Math . round ( progress * 100 ) ) ;
@@ -247,7 +345,7 @@ const ExportCanvas = ({show}:{show: boolean}) => {
247345 camera . position . z = radius * Math . cos ( newAngle ) ;
248346 camera . lookAt ( 0 , 0 , 0 ) ;
249347 camera . updateProjectionMatrix ( ) ;
250- invalidate ( ) ;
348+ ! ( useCustomRes || doubleSize ) && invalidate ( ) ; // We will invalidate later if needed. Otherwise do it now
251349 }
252350 if ( useTime ) {
253351 let newProg = dt * Math . floor ( frame * timeRatio ) ;
@@ -283,11 +381,13 @@ const ExportCanvas = ({show}:{show: boolean}) => {
283381 } ) ;
284382 usePlotStore . setState ( lerpedState )
285383 }
286-
384+ if ( useCustomRes || doubleSize ) {
385+ SetCamera ( )
386+ }
287387 // ----- RENDER TO CANVAS---- //
288388 gl . render ( scene , camera ) ;
289389 DrawComposite ( compositeCanvasRef . current as HTMLCanvasElement , gl , docWidth , docHeight ,
290- { bgColor, textColor}
390+ { bgColor, textColor} , true
291391 )
292392
293393 const blob = await new Promise ( resolve => {
@@ -300,16 +400,21 @@ const ExportCanvas = ({show}:{show: boolean}) => {
300400 }
301401 }
302402 setStatus ( "Building Animation" )
303- // Generate Animation
403+ await DrawTextOverlay ( docWidth , docHeight , textColor , ffmpeg )
304404 const execResult = await ffmpeg . exec ( [
305405 '-framerate' , `${ frameRate } ` ,
306406 '-i' , 'frame%04d.png' ,
407+ '-i' , 'textOverlay.png' ,
408+ '-filter_complex' , `[1:v]scale=${ docWidth } :${ docHeight } [overlay];[0:v][overlay]overlay=0:0` ,
307409 '-c:v' , 'libx264' ,
308- '-pix_fmt' , 'yuv420p' ,
309- '-preset' , `${ preview ? 'ultrafast' : 'medium' } ` ,
310- '-crf' , `${ preview ? 28 : 16 } ` ,
410+ '-pix_fmt' , 'yuv444p' ,
411+ '-preset' , `${ preview ? 'ultrafast' : 'slow' } ` ,
412+ '-crf' , `${ preview ? 28 : 16 } ` ,
413+ '-tune' , 'stillimage' ,
414+ '-profile:v' , 'high444' ,
311415 'output.mp4'
312416 ] ) ;
417+ if ( execResult === 1 ) { setStatus ( null ) }
313418 setStatus ( "Fetching Animation" )
314419 const videoData = await ffmpeg . readFile ( 'output.mp4' ) ;
315420 setStatus ( null )
@@ -358,9 +463,9 @@ const ExportCanvas = ({show}:{show: boolean}) => {
358463 camera . right = originalCameraSettings . right ;
359464 camera . top = originalCameraSettings . top ;
360465 camera . bottom = originalCameraSettings . bottom ;
361- }
362- camera . updateProjectionMatrix ( ) ;
466+ }
363467 gl . setSize ( originalSize . x , originalSize . y ) ;
468+ camera . updateProjectionMatrix ( ) ;
364469 }
365470 setQuality ( origQuality ) ;
366471
0 commit comments