@@ -14,16 +14,18 @@ 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 ,
19+ preview = true ,
1820) : HTMLCanvasElement | undefined => {
1921
2022 const { bgColor, textColor} = colors ;
2123 const { doubleSize, includeBackground, mainTitle,
2224 cbarLabel, cbarLoc, cbarNum, includeColorbar} = useImageExportStore . getState ( )
2325 const { valueScales, variable, metadata } = useGlobalStore . getState ( )
24-
2526 const ctx = compositeCanvas . getContext ( '2d' )
2627 if ( ! ctx ) { return }
28+
2729 ctx . imageSmoothingEnabled = true ;
2830 ctx . imageSmoothingQuality = 'high' ;
2931 if ( includeBackground ) {
@@ -35,38 +37,26 @@ const DrawComposite = (
3537
3638 ctx . drawImage ( gl . domElement , 0 , 0 , width , height )
3739
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-
4540 const cbarTickSize = doubleSize ? 36 : 18
4641 const unitSize = doubleSize ? 52 : 26
47-
48- // ---- COLORBAR ---- //
49- if ( includeColorbar ) {
50- const secondCanvas = document . getElementById ( 'colorbar-canvas' )
5142
52- let cbarWidth = doubleSize ? Math . min ( 1024 , width * 0.8 ) : Math . min ( 512 , width * 0.8 )
53- let cbarHeight = doubleSize ? 48 : 24 ;
43+ let cbarWidth = doubleSize ? Math . min ( 1024 , width * 0.8 ) : Math . min ( 512 , width * 0.8 )
44+ let cbarHeight = doubleSize ? 48 : 24 ;
5445
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- }
46+ let cbarStartPos = Math . round ( width / 2 - cbarWidth / 2 )
47+ let cbarTop = cbarLoc === 'top' ? ( doubleSize ? 140 : 70 ) : ( doubleSize ? height - 140 : height - 70 )
48+ const transpose = cbarLoc === 'right' || cbarLoc === 'left'
6749
50+ // ---- COLORBAR ---- //
51+ if ( includeColorbar ) {
52+ const secondCanvas = document . getElementById ( 'colorbar-canvas' )
6853 if ( secondCanvas instanceof HTMLCanvasElement ) {
6954 if ( transpose ) {
55+ const tempWidth = cbarWidth
56+ cbarWidth = cbarHeight
57+ cbarHeight = tempWidth
58+ cbarTop = Math . round ( height / 2 - cbarHeight / 2 )
59+ cbarStartPos = cbarLoc === 'right' ? ( doubleSize ? width - 140 : width - 70 ) : ( doubleSize ? 140 : 70 )
7060 // Save the current canvas state
7161 ctx . save ( )
7262
@@ -88,21 +78,129 @@ const DrawComposite = (
8878
8979 // Restore the canvas state
9080 ctx . restore ( )
91- } else if ( cbarLoc === 'top' ) {
81+ } else if ( cbarLoc === 'top' ) {
9282 ctx . drawImage ( secondCanvas , cbarStartPos , cbarTop , cbarWidth , cbarHeight )
93- } else {
83+ } else {
9484 ctx . drawImage ( secondCanvas , cbarStartPos , cbarTop , cbarWidth , cbarHeight )
9585 }
9686 }
87+ }
88+
89+ // ---- TEXT ---- //
90+ if ( ! animate ) { // If still image write text onto image
91+ // ---- TITLE ---- //
92+ const variableSize = doubleSize ? 72 : 36
93+ ctx . fillStyle = textColor
94+ ctx . font = `${ variableSize } px "Segoe UI"`
95+ ctx . textBaseline = 'middle'
96+ ctx . fillText ( mainTitle ?? variable , doubleSize ? 40 : 20 , doubleSize ? 100 : 50 ) // Variable in top Left
97+
98+ // ---- WATERMARK ---- //
99+ const waterMarkSize = doubleSize ? 40 : 20
100+ ctx . fillStyle = "#888888"
101+ ctx . font = `${ waterMarkSize } px "Segoe UI", serif `
102+ ctx . textAlign = 'left'
103+ ctx . textBaseline = 'bottom'
104+ ctx . fillText ( "browzarr.io" , doubleSize ? 20 : 10 , doubleSize ? height - 20 : height - 10 ) // Watermark
105+
106+ if ( includeColorbar ) {
107+ // ---- TickLabels ---- //
108+ ctx . font = `${ cbarTickSize } px "Segoe UI"`
109+ const labelNum = cbarNum ; // Number of cbar "ticks"
110+ const valRange = valueScales . maxVal - valueScales . minVal ;
111+ const valScale = 1 / ( labelNum - 1 )
112+ const posDelta = transpose ? 1 / ( labelNum - 1 ) * cbarHeight : 1 / ( labelNum - 1 ) * cbarWidth
113+ if ( transpose ) {
114+ const tempWidth = cbarWidth
115+ cbarWidth = cbarHeight
116+ cbarHeight = tempWidth
117+ cbarTop = Math . round ( height / 2 - cbarHeight / 2 )
118+ cbarStartPos = cbarLoc === 'right' ? ( doubleSize ? width - 140 : width - 70 ) : ( doubleSize ? 140 : 70 )
119+ ctx . textBaseline = 'middle'
120+ ctx . textAlign = cbarLoc == 'left' ? 'left' : 'right'
121+ for ( let i = 0 ; i < labelNum ; i ++ ) {
122+ if ( cbarLoc == 'left' ) {
123+ ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos + cbarWidth + 6 , cbarTop + cbarHeight - i * posDelta )
124+ } else {
125+ ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos - 6 , cbarTop + cbarHeight - i * posDelta )
126+ }
127+ }
128+ } else {
129+ ctx . textBaseline = 'top'
130+ ctx . textAlign = 'center'
131+ for ( let i = 0 ; i < labelNum ; i ++ ) {
132+ ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos + i * posDelta , cbarTop + cbarHeight + 6 )
133+ }
134+ }
135+
136+ // ---- Cbar Label/Units ---- //
137+ ctx . fillStyle = textColor
138+ ctx . font = `${ unitSize } px "Segoe UI" bold`
139+ ctx . textAlign = 'center'
140+ ctx . fillText ( cbarLabel ?? metadata ?. units , cbarStartPos + cbarWidth / 2 , cbarTop - unitSize - 4 )
141+ }
142+ }
143+
144+ }
145+
146+ async function DrawTextOverlay (
147+ width : number ,
148+ height : number ,
149+ textColor :string ,
150+ ffmpeg : FFmpeg
151+ ) {
152+ const scaling = 4 ;
153+ const { doubleSize, mainTitle,
154+ cbarLabel, cbarLoc, cbarNum, includeColorbar} = useImageExportStore . getState ( )
155+ const { valueScales, variable, metadata } = useGlobalStore . getState ( )
156+ const textCanvas = document . createElement ( "canvas" ) ;
157+ textCanvas . width = width * scaling ;
158+ textCanvas . height = height * scaling ;
159+ const ctx = textCanvas . getContext ( "2d" ) ;
160+ if ( ! ctx ) return ;
161+
162+ ctx . scale ( scaling , scaling )
163+ const cbarTickSize = doubleSize ? 36 : 18
164+ const unitSize = doubleSize ? 52 : 26
165+
166+ let cbarWidth = doubleSize ? Math . min ( 1024 , width * 0.8 ) : Math . min ( 512 , width * 0.8 )
167+ let cbarHeight = doubleSize ? 48 : 24 ;
168+
169+ let cbarStartPos = Math . round ( width / 2 - cbarWidth / 2 )
170+ let cbarTop = cbarLoc === 'top' ? ( doubleSize ? 140 : 70 ) : ( doubleSize ? height - 140 : height - 70 )
171+ const transpose = cbarLoc === 'right' || cbarLoc === 'left'
172+
173+ // ---- TEXT ---- //
174+
175+ // ---- TITLE ---- //
176+ const variableSize = doubleSize ? 72 : 36
177+ ctx . fillStyle = textColor
178+ ctx . font = `${ variableSize } px "Segoe UI"`
179+ ctx . textBaseline = 'middle'
180+ ctx . textAlign = 'left'
181+ ctx . fillText ( mainTitle ?? variable , doubleSize ? 40 : 20 , doubleSize ? 100 : 50 ) // Variable in top Left
182+
183+ // ---- WATERMARK ---- //
184+ const waterMarkSize = doubleSize ? 40 : 20
185+ ctx . fillStyle = "#888888"
186+ ctx . font = `${ waterMarkSize } px "Segoe UI", serif `
187+ ctx . textBaseline = 'bottom'
188+ ctx . fillText ( "browzarr.io" , doubleSize ? 20 : 10 , doubleSize ? height - 20 : height - 10 ) // Watermark
189+
190+ if ( includeColorbar ) {
191+ // ---- TickLabels ---- //
192+ ctx . font = `${ cbarTickSize } px "Segoe UI"` ;
193+ ctx . fillStyle = textColor ;
97194 const labelNum = cbarNum ; // Number of cbar "ticks"
98195 const valRange = valueScales . maxVal - valueScales . minVal ;
99196 const valScale = 1 / ( labelNum - 1 )
100197 const posDelta = transpose ? 1 / ( labelNum - 1 ) * cbarHeight : 1 / ( labelNum - 1 ) * cbarWidth
101-
102- // ---- TickLabels ---- //
103- ctx . font = `${ cbarTickSize } px "Segoe UI"`
104-
105198 if ( transpose ) {
199+ const tempWidth = cbarWidth
200+ cbarWidth = cbarHeight
201+ cbarHeight = tempWidth
202+ cbarTop = Math . round ( height / 2 - cbarHeight / 2 )
203+ cbarStartPos = cbarLoc === 'right' ? ( doubleSize ? width - 140 : width - 70 ) : ( doubleSize ? 140 : 70 )
106204 ctx . textBaseline = 'middle'
107205 ctx . textAlign = cbarLoc == 'left' ? 'left' : 'right'
108206 for ( let i = 0 ; i < labelNum ; i ++ ) {
@@ -112,30 +210,32 @@ const DrawComposite = (
112210 ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos - 6 , cbarTop + cbarHeight - i * posDelta )
113211 }
114212 }
115- } else {
213+ } else {
116214 ctx . textBaseline = 'top'
117215 ctx . textAlign = 'center'
118216 for ( let i = 0 ; i < labelNum ; i ++ ) {
119217 ctx . fillText ( String ( ( valueScales . minVal + ( i * valScale * valRange ) ) . toFixed ( 2 ) ) , cbarStartPos + i * posDelta , cbarTop + cbarHeight + 6 )
120218 }
121219 }
122220
221+ // ---- Cbar Label/Units ---- //
123222 ctx . fillStyle = textColor
124223 ctx . font = `${ unitSize } px "Segoe UI" bold`
125224 ctx . textAlign = 'center'
126- ctx . fillText ( cbarLabel ?? metadata ?. units , cbarStartPos + cbarWidth / 2 , cbarTop - unitSize - 4 ) // Cbar Units above middle of cbar
225+ ctx . fillText ( cbarLabel ?? metadata ?. units , cbarStartPos + cbarWidth / 2 , cbarTop - unitSize - 4 )
127226 }
128227
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
228+ const blob = await new Promise ( resolve => {
229+ textCanvas . toBlob ( resolve , 'image/png' ) ;
230+ } ) ;
231+ if ( blob ) {
232+ const buf = await ( blob as Blob ) . arrayBuffer ( ) ;
233+ // Write frames to internal ffMpeg filesystem
234+ await ffmpeg . writeFile ( `textOverlay.png` , new Uint8Array ( buf ) ) ;
235+ }
135236}
136237
137238
138-
139239const ExportCanvas = ( { show} :{ show : boolean } ) => {
140240 const { exportImg, enableExport, animate, frames, frameRate, useTime, timeRate, orbit, loopTime,
141241 animViz, initialState, finalState, preview, useCustomRes, customRes, doubleSize, setHideAxis, setHideAxisControls
@@ -161,7 +261,6 @@ const ExportCanvas = ({show}:{show: boolean}) => {
161261
162262 const origQuality = usePlotStore . getState ( ) . quality ;
163263 setQuality ( preview ? 50 : 1000 ) ;
164-
165264 const domWidth = gl . domElement . width ;
166265 const domHeight = gl . domElement . height ;
167266 let docWidth = useCustomRes ? customRes [ 0 ] : ( doubleSize ? domWidth * 2 : domWidth ) ;
@@ -180,22 +279,19 @@ const ExportCanvas = ({show}:{show: boolean}) => {
180279 const originalSize = gl . getSize ( new THREE . Vector2 ( ) )
181280 let originalCameraSettings : any = { } ;
182281
183- if ( useCustomRes || doubleSize ) {
282+ function SetCamera ( ) {
184283 if ( camera instanceof THREE . PerspectiveCamera ) {
185284 originalCameraSettings = { aspect : camera . aspect }
186285 camera . aspect = docWidth / docHeight
187- camera . updateProjectionMatrix ( )
188286 } else if ( camera instanceof THREE . OrthographicCamera ) {
189287 originalCameraSettings = {
190288 left : camera . left ,
191289 right : camera . right ,
192290 top : camera . top ,
193291 bottom : camera . bottom
194292 }
195-
196293 const newAspect = docWidth / docHeight
197294 const currentAspect = ( camera . right - camera . left ) / ( camera . top - camera . bottom )
198-
199295 if ( newAspect > currentAspect ) {
200296 // Wider - expand left/right
201297 const width = ( camera . top - camera . bottom ) * newAspect
@@ -209,9 +305,11 @@ const ExportCanvas = ({show}:{show: boolean}) => {
209305 camera . top = center + height / 2
210306 camera . bottom = center - height / 2
211307 }
212- camera . updateProjectionMatrix ( )
213308 }
309+
214310 gl . setSize ( docWidth , docHeight )
311+ camera . updateProjectionMatrix ( )
312+ invalidate ( ) ;
215313 }
216314
217315 if ( animate ) {
@@ -221,6 +319,7 @@ const ExportCanvas = ({show}:{show: boolean}) => {
221319 if ( ! ffmpeg . loaded ) {
222320 await ffmpeg . load ( ) ;
223321 }
322+
224323 ffmpeg . on ( 'progress' , ( { progress, time } ) => {
225324 // progress is a value between 0 and 1
226325 setProgress ( Math . round ( progress * 100 ) ) ;
@@ -247,7 +346,7 @@ const ExportCanvas = ({show}:{show: boolean}) => {
247346 camera . position . z = radius * Math . cos ( newAngle ) ;
248347 camera . lookAt ( 0 , 0 , 0 ) ;
249348 camera . updateProjectionMatrix ( ) ;
250- invalidate ( ) ;
349+ ! ( useCustomRes || doubleSize ) && invalidate ( ) ; // We will invalidate later if needed. Otherwise do it now
251350 }
252351 if ( useTime ) {
253352 let newProg = dt * Math . floor ( frame * timeRatio ) ;
@@ -283,11 +382,13 @@ const ExportCanvas = ({show}:{show: boolean}) => {
283382 } ) ;
284383 usePlotStore . setState ( lerpedState )
285384 }
286-
385+ if ( useCustomRes || doubleSize ) {
386+ SetCamera ( )
387+ }
287388 // ----- RENDER TO CANVAS---- //
288389 gl . render ( scene , camera ) ;
289390 DrawComposite ( compositeCanvasRef . current as HTMLCanvasElement , gl , docWidth , docHeight ,
290- { bgColor, textColor}
391+ { bgColor, textColor} , true
291392 )
292393
293394 const blob = await new Promise ( resolve => {
@@ -300,16 +401,21 @@ const ExportCanvas = ({show}:{show: boolean}) => {
300401 }
301402 }
302403 setStatus ( "Building Animation" )
303- // Generate Animation
404+ await DrawTextOverlay ( docWidth , docHeight , textColor , ffmpeg )
304405 const execResult = await ffmpeg . exec ( [
305406 '-framerate' , `${ frameRate } ` ,
306407 '-i' , 'frame%04d.png' ,
408+ '-i' , 'textOverlay.png' ,
409+ '-filter_complex' , `[1:v]scale=${ docWidth } :${ docHeight } [overlay];[0:v][overlay]overlay=0:0` ,
307410 '-c:v' , 'libx264' ,
308- '-pix_fmt' , 'yuv420p' ,
309- '-preset' , `${ preview ? 'ultrafast' : 'medium' } ` ,
310- '-crf' , `${ preview ? 28 : 16 } ` ,
411+ '-pix_fmt' , 'yuv444p' ,
412+ '-preset' , `${ preview ? 'ultrafast' : 'slow' } ` ,
413+ '-crf' , `${ preview ? 28 : 16 } ` ,
414+ '-tune' , 'stillimage' ,
415+ '-profile:v' , 'high444' ,
311416 'output.mp4'
312417 ] ) ;
418+ if ( execResult === 1 ) { setStatus ( null ) }
313419 setStatus ( "Fetching Animation" )
314420 const videoData = await ffmpeg . readFile ( 'output.mp4' ) ;
315421 setStatus ( null )
@@ -358,9 +464,9 @@ const ExportCanvas = ({show}:{show: boolean}) => {
358464 camera . right = originalCameraSettings . right ;
359465 camera . top = originalCameraSettings . top ;
360466 camera . bottom = originalCameraSettings . bottom ;
361- }
362- camera . updateProjectionMatrix ( ) ;
467+ }
363468 gl . setSize ( originalSize . x , originalSize . y ) ;
469+ camera . updateProjectionMatrix ( ) ;
364470 }
365471 setQuality ( origQuality ) ;
366472
0 commit comments