Skip to content

Commit 030d119

Browse files
authored
Merge pull request #424 from EarthyScience/jp/build-progress
Crispy Text
2 parents 50c282d + 4a0ec47 commit 030d119

File tree

1 file changed

+162
-57
lines changed

1 file changed

+162
-57
lines changed

src/utils/ExportCanvas.tsx

Lines changed: 162 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
139238
const 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

Comments
 (0)