@@ -25,10 +25,12 @@ import { PromptComposer, type PromptComposerHandle, type UploadedReferenceImage,
export type ComposerMode = "agent" | "image" ;
export type ComposerImageQuality = "auto" | "high" | "medium" | "low" ;
export type ComposerImageRatio = "1:1" | "3:2" | "2:3" | "4:3" | "3:4" | "9:16" | "1:1-2k" | "16:9-2k" | "9:16-2k" | "16:9-4k" | "9:16-4k" | "auto ";
export type ComposerImageResolution = "1K" | "2K" | "4K ";
export type ComposerImageRatio = "1:1" | "3:2" | "2:3" | "4:3" | "3:4" | "16:9" | "9:16" | "4:5" | "5:4" | "auto" ;
export type ComposerImageSettings = {
quality : ComposerImageQuality ;
resolution : ComposerImageResolution ;
ratio : ComposerImageRatio ;
width : number ;
height : number ;
@@ -78,37 +80,141 @@ const qualityOptions: Array<{ id: ComposerImageQuality; labelKey: string }> = [
{ id : "low" , labelKey : "qualityLow" }
] ;
const resolutionOptions : Array < { id : ComposerImageResolution ; label : string } > = [
{ id : "1K" , label : "1K" } ,
{ id : "2K" , label : "2K" } ,
{ id : "4K" , label : "4K" }
] ;
export const composerRatioOptions : Array < { id : ComposerImageRatio ; label : string ; width : number ; height : number } > = [
{ id : "1:1" , label : "1:1" , width : 1024 , height : 1024 } ,
{ id : "3:2" , label : "3:2" , width : 1536 , height : 1024 } ,
{ id : "2:3" , label : "2:3" , width : 1024 , height : 1536 } ,
{ id : "4:3" , label : "4:3" , width : 1365 , height : 1024 } ,
{ id : "3:4" , label : "3:4" , width : 1024 , height : 1365 } ,
{ id : "9: 16" , label : "9: 16" , width : 1024 , height : 1820 } ,
{ id : "1 :1-2k " , label : "1 :1(2k) " , width : 2048 , height : 2048 } ,
{ id : "16:9-2k " , label : "16:9(2k) " , width : 2048 , height : 1152 } ,
{ id : "9:16-2k " , label : "9:16(2k) " , width : 1152 , height : 2048 } ,
{ id : "16:9-4k" , label : "16:9(4k)" , width : 4096 , height : 2304 } ,
{ id : "9:16-4k" , label : "9:16(4k)" , width : 2304 , height : 4096 } ,
{ id : "16:9 " , label : "16:9 " , width : 1824 , height : 1024 } ,
{ id : "9 :16 " , label : "9 :16 " , width : 1024 , height : 1824 } ,
{ id : "4:5 " , label : "4:5 " , width : 1024 , height : 1280 } ,
{ id : "5:4 " , label : "5:4 " , width : 1280 , height : 1024 } ,
{ id : "auto" , label : "auto" , width : 1024 , height : 1024 }
] ;
export const defaultComposerImageSettings : ComposerImageSettings = {
quality : "auto" ,
resolution : "1K" ,
ratio : "1:1" ,
width : 1024 ,
height : 1024 ,
count : 1
} ;
const sizePattern = /^\s*(\d+)\s*[xX× ]\s*(\d+)\s*$/ ;
const sizeMultiple = 16 ;
const maxEdge = 3840 ;
const maxAspectRatio = 3 ;
const minPixels = 655 _360 ;
const maxPixels = 8 _294_400 ;
function roundToMultiple ( value : number , multiple : number ) {
return Math . max ( multiple , Math . round ( value / multiple ) * multiple ) ;
}
function floorToMultiple ( value : number , multiple : number ) {
return Math . max ( multiple , Math . floor ( value / multiple ) * multiple ) ;
}
function ceilToMultiple ( value : number , multiple : number ) {
return Math . max ( multiple , Math . ceil ( value / multiple ) * multiple ) ;
}
function normalizeDimensions ( width : number , height : number ) {
let normalizedWidth = roundToMultiple ( width , sizeMultiple ) ;
let normalizedHeight = roundToMultiple ( height , sizeMultiple ) ;
const scaleToFit = ( scale : number ) = > {
normalizedWidth = floorToMultiple ( normalizedWidth * scale , sizeMultiple ) ;
normalizedHeight = floorToMultiple ( normalizedHeight * scale , sizeMultiple ) ;
} ;
const scaleToFill = ( scale : number ) = > {
normalizedWidth = ceilToMultiple ( normalizedWidth * scale , sizeMultiple ) ;
normalizedHeight = ceilToMultiple ( normalizedHeight * scale , sizeMultiple ) ;
} ;
for ( let index = 0 ; index < 4 ; index += 1 ) {
const currentMaxEdge = Math . max ( normalizedWidth , normalizedHeight ) ;
if ( currentMaxEdge > maxEdge ) {
scaleToFit ( maxEdge / currentMaxEdge ) ;
}
if ( normalizedWidth / normalizedHeight > maxAspectRatio ) {
normalizedWidth = floorToMultiple ( normalizedHeight * maxAspectRatio , sizeMultiple ) ;
} else if ( normalizedHeight / normalizedWidth > maxAspectRatio ) {
normalizedHeight = floorToMultiple ( normalizedWidth * maxAspectRatio , sizeMultiple ) ;
}
const pixels = normalizedWidth * normalizedHeight ;
if ( pixels > maxPixels ) {
scaleToFit ( Math . sqrt ( maxPixels / pixels ) ) ;
} else if ( pixels < minPixels ) {
scaleToFill ( Math . sqrt ( minPixels / pixels ) ) ;
}
}
return { width : normalizedWidth , height : normalizedHeight } ;
}
function normalizeImageSize ( size : string ) {
const match = size . trim ( ) . match ( sizePattern ) ;
if ( ! match ) return null ;
return normalizeDimensions ( Number ( match [ 1 ] ) , Number ( match [ 2 ] ) ) ;
}
function ratioParts ( ratio : ComposerImageRatio , fallbackWidth : number , fallbackHeight : number ) {
if ( ratio === "auto" ) {
return {
width : Math.max ( 1 , fallbackWidth ) ,
height : Math.max ( 1 , fallbackHeight )
} ;
}
const [ width , height ] = ratio . split ( ":" ) . map ( Number ) ;
return { width , height } ;
}
export function composerImageDimensionsFor ( resolution : ComposerImageResolution , ratio : ComposerImageRatio , fallbackWidth = 1024 , fallbackHeight = 1024 ) {
const { width : ratioWidth , height : ratioHeight } = ratioParts ( ratio , fallbackWidth , fallbackHeight ) ;
if ( ratio === "auto" ) {
const size = normalizeImageSize ( ` ${ ratioWidth } x ${ ratioHeight } ` ) ;
return size ? ? { width : 1024 , height : 1024 } ;
}
if ( ratioWidth === ratioHeight ) {
const side = resolution === "1K" ? 1024 : resolution === "2K" ? 2048 : 3840 ;
return normalizeImageSize ( ` ${ side } x ${ side } ` ) ? ? { width : side , height : side } ;
}
if ( resolution === "1K" ) {
const shortSide = 1024 ;
const width = ratioWidth > ratioHeight ? roundToMultiple ( ( shortSide * ratioWidth ) / ratioHeight , sizeMultiple ) : shortSide ;
const height = ratioWidth > ratioHeight ? shortSide : roundToMultiple ( ( shortSide * ratioHeight ) / ratioWidth , sizeMultiple ) ;
return { width , height } ;
}
const longSide = resolution === "2K" ? 2048 : 3840 ;
const width = ratioWidth > ratioHeight ? longSide : roundToMultiple ( ( longSide * ratioWidth ) / ratioHeight , sizeMultiple ) ;
const height = ratioWidth > ratioHeight ? roundToMultiple ( ( longSide * ratioHeight ) / ratioWidth , sizeMultiple ) : longSide ;
return normalizeImageSize ( ` ${ width } x ${ height } ` ) ? ? { width , height } ;
}
export function composerImageDimensions ( settings : ComposerImageSettings ) {
return composerImageDimensionsFor ( settings . resolution , settings . ratio , settings . width , settings . height ) ;
}
export function composerImageSize ( settings : ComposerImageSettings ) {
return ` ${ Math . max ( 1 , Math . round ( settings . width ) ) } x ${ Math . max ( 1 , Math . round ( settings . height ) ) } ` ;
const { width , height } = composerImageDimensions ( settings ) ;
return ` ${ width } x ${ height } ` ;
}
export function composerSettingsSummary ( settings : ComposerImageSettings ) {
const ratioLabel = composerRatioOptions . find ( ( option ) = > option . id === settings . ratio ) ? . label ? ? settings . ratio ;
const sizeLabel = settings . ratio === "auto" ? composerImageSize ( settings ) . replace ( "x" , "× " ) : ratioLabel ;
return ` ${ qualityLabel ( settings . quality ) } · ${ sizeLabel } · ${ settings . count } img ` ;
const sizeLabel = composerImageSize ( settings ) . replace ( "x" , "× " ) ;
return ` ${ qualityLabel ( settings . quality ) } · ${ settings . resolution } · ${ ratioLabel } · ${ sizeLabel } · ${ settings . count } img ` ;
}
function qualityLabel ( quality : ComposerImageQuality ) {
@@ -167,8 +273,8 @@ export const CanvasAgentComposer = forwardRef<PromptComposerHandle, CanvasAgentC
} ;
const selectRatio = ( ratio : ComposerImageRatio ) = > {
const nextRatio = composerRatioOptions . find ( ( option ) = > option . id === ratio ) ? ? composerRatioOptions [ 0 ] ;
patchSettings ( { ratio , width : nextRatio.width , height : nextRatio.height } ) ;
const nextSize = composerImageDimensionsFor ( settings . resolution , ratio , settings . width , settings . height ) ;
patchSettings ( { ratio , . . . nextSize } ) ;
} ;
const composerCard = (
@@ -372,6 +478,21 @@ export function ComposerSettingsPanel({
) ) }
< / div >
< / section >
< section >
< h4 > { t ( "imageResolutionLabel" ) } < / h4 >
< div className = "composer-resolution-control" >
{ resolutionOptions . map ( ( option ) = > (
< button
key = { option . id }
type = "button"
className = { settings . resolution === option . id ? "selected" : "" }
onClick = { ( ) = > onPatch ( { resolution : option.id , . . . composerImageDimensionsFor ( option . id , settings . ratio , settings . width , settings . height ) } ) }
>
{ option . label }
< / button >
) ) }
< / div >
< / section >
< section >
< h4 > { t ( "imageSizeLabel" ) } < / h4 >
< div className = "composer-dimensions" >
@@ -409,7 +530,7 @@ export function ComposerSettingsPanel({
}
function RatioGlyph ( { ratio } : { ratio : ComposerImageRatio } ) {
const portrait = ratio . startsWith ( "2:3" ) || ratio . startsWith ( "3:4" ) || ratio . startsWith ( "9:16" ) ;
const wide = ratio . startsWith ( "3:2" ) || ratio . startsWith ( "4:3" ) || ratio . startsWith ( "16:9" ) ;
const portrait = ratio . startsWith ( "2:3" ) || ratio . startsWith ( "3:4" ) || ratio . startsWith ( "9:16" ) || ratio . startsWith ( "4:5" ) ;
const wide = ratio . startsWith ( "3:2" ) || ratio . startsWith ( "4:3" ) || ratio . startsWith ( "16:9" ) || ratio . startsWith ( "5:4" ) ;
return < i className = { ` ratio-glyph ${ portrait ? "portrait" : wide ? "wide" : "" } ` } aria-hidden = "true" / > ;
}