@@ -248,8 +248,8 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) {
248248 ) ;
249249 // (The "largest clip" pick lived here for the old renderer-side GIF path, which
250250 // sized to the best available footage independently of the quality tier. GIF now
251- // goes through the same native exporter as MP4 and shares its sizing , so only the
252- // smallest-clip pick below is still needed.)
251+ // goes through the same native exporter as MP4 and starts from its "Source" size , so
252+ // only the smallest-clip pick below is still needed.)
253253
254254 // Smallest clip's true (cropped) footprint on the timeline — a multiclip timeline can mix
255255 // crops/resolutions, so this is what "Source" quality actually targets: sizing to the
@@ -280,25 +280,6 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) {
280280 // quality actually uses these as its target size; 720p/1080p target a fixed short side
281281 // regardless (`calculateDimensionsForShortSide`), so this only changes what "Source"
282282 // resolves to.
283- // GIF is 8-bit indexed and grows fast with area, so the size preset caps the
284- // output height rather than following the quality tier. `original` keeps the
285- // tier's dims; the native side falls back to its own defaults when undefined.
286- const gifOutputDims = (
287- preset : GifSizePreset ,
288- tierDims : { width : number ; height : number } | null ,
289- ) : { width ?: number ; height ?: number } => {
290- if ( ! tierDims ) return { } ;
291- const maxHeight = GIF_SIZE_PRESETS [ preset ] . maxHeight ;
292- if ( ! Number . isFinite ( maxHeight ) || tierDims . height <= maxHeight ) {
293- return { width : tierDims . width , height : tierDims . height } ;
294- }
295- const scale = maxHeight / tierDims . height ;
296- // Even dimensions: the compositor rasterises to this size and the readback
297- // assumes a tightly-packed RGBA buffer.
298- const even = ( n : number ) => Math . max ( 2 , Math . round ( n * scale ) & ~ 1 ) ;
299- return { width : even ( tierDims . width ) , height : even ( tierDims . height ) } ;
300- } ;
301-
302283 const tierOutputDims = ( value : ExportQuality ) =>
303284 smallestSource
304285 ? calculateMp4ExportSettings ( {
@@ -310,6 +291,26 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) {
310291 } )
311292 : null ;
312293
294+ // GIF is 8-bit indexed and grows fast with area, so the size preset caps the output
295+ // height. It starts from the "Source" size, never from the quality tier: that control is
296+ // MP4-only and hidden while GIF is picked, yet the tier an MP4 choice left behind used to
297+ // size the GIF (a 640x360 clip gave 852x480 after Web / YouTube, 640x360 after Studio).
298+ // So no preset upscales and `original` is the source size. The native side falls back
299+ // to its own defaults when undefined.
300+ const gifOutputDims = ( preset : GifSizePreset ) : { width ?: number ; height ?: number } => {
301+ const source = tierOutputDims ( "source" ) ;
302+ if ( ! source ) return { } ;
303+ const maxHeight = GIF_SIZE_PRESETS [ preset ] . maxHeight ;
304+ if ( source . height <= maxHeight ) {
305+ return { width : source . width , height : source . height } ;
306+ }
307+ const scale = maxHeight / source . height ;
308+ // Even dimensions: the compositor rasterises to this size and the readback
309+ // assumes a tightly-packed RGBA buffer.
310+ const even = ( n : number ) => Math . max ( 2 , Math . round ( n * scale ) & ~ 1 ) ;
311+ return { width : even ( source . width ) , height : even ( source . height ) } ;
312+ } ;
313+
313314 useEffect ( ( ) => {
314315 if ( ! open ) {
315316 pickerGeneration . current += 1 ;
@@ -456,9 +457,7 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) {
456457 pickedPath ,
457458 sceneJson ,
458459 {
459- // GIF is 256-colour and grows fast; cap the long edge at the
460- // chosen preset rather than exporting at source size.
461- ...gifOutputDims ( gifSize , outDims ) ,
460+ ...gifOutputDims ( gifSize ) ,
462461 fps : gifFrameRate ,
463462 // 0 = infinite, the historical GIF default; 1 = play once.
464463 loopCount : gifLoop ? 0 : 1 ,
@@ -536,12 +535,12 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) {
536535 } ;
537536 /** What a destination produces, in the numbers the Advanced settings show. */
538537 const destinationSummary = ( set : Partial < ExportChoice > ) => {
539- const dims = tierOutputDims ( set . quality ?? quality ) ;
540538 if ( set . format === "gif" ) {
541- const gif = gifOutputDims ( set . gifSize ?? gifSize , dims ) ;
539+ const gif = gifOutputDims ( set . gifSize ?? gifSize ) ;
542540 const size = gif . width ? `${ gif . width } × ${ gif . height } · ` : "" ;
543541 return `GIF · ${ size } ${ set . gifFrameRate ?? gifFrameRate } fps` ;
544542 }
543+ const dims = tierOutputDims ( set . quality ?? quality ) ;
545544 const size = dims ? `${ dims . width } × ${ dims . height } · ` : "" ;
546545 return `MP4 · ${ size } ${ set . fps ?? fps } fps` ;
547546 } ;
0 commit comments