Image Generator

An AI image generator with filters.

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.

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