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>
)
}