This example shows how to use the image generator. It uses the FAL API to generate images.
We use custom components in the DesignerToolbar
to show a prompt form, history controls and a cropper.
We've also added a custom DesignerPanel
to show image filters.
Note: Please note that the demo is limited to 5 image generations per day.
Image Generator
For this example, we only need to use the image
layer type. We use the layerTypes
prop to filter out the other layer types.
Then, we create a single image layer and set the value
to an empty string. We also set the cssVars
to the default width and height.
We use single
mode to only show a single image layer.
function ExampleImageGenerator() {
return (
<div className="aspect-video">
<Designer
mode="single"
layerTypes={DEFAULT_LAYER_TYPES.filter(
(layer) => layer.type === "image"
)}
defaultLayers={[
{
id: "image-1",
type: "image",
name: "Generated Image",
isLocked: true,
value: "",
cssVars: {
"--width": "1024px",
"--height": "1024px",
},
},
]}
>
<DesignerContent>
<DesignerCanvas>
<DesignerFrame />
</DesignerCanvas>
<DesignerPanel className="!border-0 absolute inset-y-6 right-6 h-auto w-auto bg-transparent">
<ActionImageFilters />
</DesignerPanel>
</DesignerContent>
<DesignerToolbar className="rounded-full shadow **:data-[slot=designer-toolbar-button]:rounded-full **:data-[slot=dialog-trigger]:rounded-full">
<DesignerToolbarGroup>
<ActionToolbarHistory />
</DesignerToolbarGroup>
<DesignerToolbarSeparator />
<DesignerToolbarGroup>
<ActionToolbarPromptForm />
</DesignerToolbarGroup>
<DesignerToolbarSeparator />
<DesignerToolbarGroup>
<ActionToolbarCropper />
</DesignerToolbarGroup>
</DesignerToolbar>
</Designer>
</div>
)
}
Prompt Form
The prompt form displayes a text input in the toolbar. When we receive the image from the API, we update the layer value
with the generated image and reset the crop.
function ActionToolbarPromptForm() {
const [isLoading, setIsLoading] = React.useState(false)
const setLayersProperty = useSetLayersProperty()
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const prompt = formData.get("prompt") as string
if (!prompt.trim()) {
return
}
setIsLoading(true)
try {
// Make request to generate image.
const response = await fetch("/api/images/generate")
const data = await response.json()
if (!data.image) {
throw new Error("No image generated")
}
if (data.image) {
// Update layer with generated image and reset crop.
setLayersProperty(["image-1"], "value", `data:image/jpeg;base64,${data.image}`)
setLayersProperty(["image-1"], "cssVars", {
"--width": "1024px",
"--height": "1024px",
})
}
} catch {
toast.error("Something went wrong. Please try again.")
} finally {
setIsLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="relative">
<Label htmlFor="prompt" className="sr-only">
Generate an image
</Label>
<Input
id="prompt"
name="prompt"
placeholder="Generate an image..."
className="h-7 min-w-56 bg-input px-2 shadow-none md:text-xs"
disabled={isLoading}
/>
<Button
type="submit"
disabled={isLoading}
className="absolute top-1 right-1 size-5 rounded-full"
size="icon"
>
{isLoading ? <IconLoader2 className="animate-spin" /> : <IconArrowUp />}
</Button>
</form>
)
}
Image Cropper
For the cropper, we use the built-in ActionImageCropper
component.
function ActionToolbarCropper() {
const { open, setOpen } = useActionImage()
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<DesignerToolbarButton tooltip="Crop">
<IconCrop />
</DesignerToolbarButton>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Image Cropper</DialogTitle>
<DialogDescription>Crop and resize the image.</DialogDescription>
<div className="pt-2">
<ActionImageCropper />
</div>
</DialogHeader>
</DialogContent>
</Dialog>
)
}
Image Filters
To show filters, we create a custom action component that updates the layer cssVars
with the filter values.
const filters = [
{
name: "vibrant",
cssVars: {
"--filter-brightness": "115%",
"--filter-contrast": "130%",
"--filter-saturate": "250%",
"--filter-hue-rotate": "0deg",
},
},
{
name: "noir",
cssVars: {
"--filter-grayscale": "100%",
"--filter-brightness": "90%",
"--filter-contrast": "150%",
},
},
{
name: "vintage",
cssVars: {
"--filter-sepia": "60%",
"--filter-brightness": "110%",
"--filter-contrast": "80%",
"--filter-saturate": "120%",
},
},
{
name: "dreamy",
cssVars: {
"--filter-blur": "2px",
"--filter-brightness": "120%",
"--filter-saturate": "150%",
"--filter-contrast": "90%",
},
},
{
name: "cool",
cssVars: {
"--filter-hue-rotate": "200deg",
"--filter-brightness": "100%",
"--filter-contrast": "100%",
"--filter-sepia": "10%",
},
},
]
function ActionImageFilters() {
const layers = useLayers()
const setLayersProperty = useSetLayersProperty()
if (!layers[0]) {
return null
}
return (
<div className="flex flex-1 flex-col justify-center gap-2 px-2">
{filters.map((filter) => (
<Button
key={filter.name}
onClick={() => {
const cssVarsWithoutFilter = Object.fromEntries(
Object.entries(layers[0]?.cssVars || {}).filter(
([key]) => !key.startsWith("--filter-")
)
)
setLayersProperty(["image-1"], "cssVars", {
...cssVarsWithoutFilter,
...filter.cssVars,
})
}}
variant="ghost"
size="icon"
className="size-14 overflow-hidden rounded-sm px-0 hover:scale-110"
style={{
filter: Object.entries(filter.cssVars)
.map(
([key, value]) => `${key.replace("--filter-", "")}(${value})`
)
.join(" "),
}}
>
<img
src={layers[0]?.value}
alt={filter.name}
className="aspect-square size-16"
/>
</Button>
))}
</div>
)
}