Skip to main content
CESDK/CE.SDK/Cookbook

Automatic Design Generation with React

Learn how to build an automatic design generation app on top of the Creative Engine API

CreativeEditor SDK (CE.SDK) is a fully-featured, easy-to-use, customizable design editor SDK. CE.SDK can also run in headless mode. Specifically, the headless Creative Engine API enables performing any editing operation programmatically, such as filling templates with data, and rendering images. You can take advantage of this functionality to automatically create designs from a set of inputs provided by the user. This data can be combined with templates and layouts to create unique, professional-looking, visually-appealing designs to jumpstart your users' creative workflow.

This is exactly what the following automatic design generation showcase application is about!

The Automatic Design Generation Use Case#

The automatic design generation showcase is built with React and uses IMG.LY’s headless Creative Engine APIs to automatically produce ready-to-use designs based on input parameters entered by the user. This is what the showcase application looks like:

The Automatic Design Generation showcase app

Specifically, this application allows you to select a background image from the five available, write your inspirational quote, choose a font, and select the desired color for the text and image frame.

The automatic design generation app gives users the ability to produce customized, unique quote cards. A "Shuffle" option generates a random immediate result.

Essential CE.SDK Features for Automatic Design Generation#

Before digging into the code, let’s take a look at the main IMG.LY features the Automatic Design Generation app relies on:

  • Scenes: the image generation process is based on a design that accepts a customizable quote variable and cannot be changed by the user.
  • Variables: the customizable quote is part of the scene and represents a variable whose value can be set via the headless API.
  • Block Editing: to modify the background image, color, and font of the blocks in the scene.
  • Zoom Control: to reset the zoom and focus of the scene after any change in screen size.

Let’s now see how all these features are employed in the automatic design generation React app.

Walking through the Showcase Code#

The showcase application consists of many different UI components and relies on several utility functions. However, the main logic is contained within the CaseComponent. So, let’s delve into it and understand how it works by going through its commented code in a step-by-step analysis.

First, let’s take a look at the React state variables and refs it relies on:

// the Creative Engine variable
const [engine, setEngine] = useState();
// a flag to specify whether the Creative Engine
// has been loaded or not
const [isEngineLoaded, setIsEngineLoaded] = useState(false);
// a flag to specify whether the Creative Engine
// has loaded the scene or not
const [isSceneLoaded, setIsSceneLoaded] = useState(false);
// the reference to the HTML div element
// where the Creative Engine is mounted
const containerRef = useRef(null);
// the quote written by the user
const [headline, setHeadline] = useState();
// the background image URL selected by the user
const [image, setImage] = useState(null);
// the font selected by the user
const [font, setFont] = useState(null);
// the color in HEX format selected by the user
const [colorHex, setColorHex] = useState('#C0C3C5');

The first three state variables are required to get access to the Creative Engine APIs and keep track of the status the engine is in, while the ref variable will be used to mount the Creative Engine component on the web page.

The remaining four React state variables are not directly related to CE.SDK, and will be used to implement the data selection logic. This data then will be passed to the CE.SDK API to generate images as prompted by the user.

Next, the CaseComponent contains a useMemo() hook to convert the colorHEX variable into the equivalent RGBA representation used by CE.SDK:

const colorRGBA = useMemo(() => {
// converting the HEX color into RGBA with a utility function
let { r, g, b } = hexToRgba(colorHex);
// to convert the [0, 255] values to the [0, 1] format used
// expected by the Creative Engine
return { r: r / 255, g: g / 255, b: b / 255 };
}, [colorHex]);

Every time colorHex changes, this conversion function will be executed and colorRGBA will be updated accordingly. This is required because IMG.LY works with color with red, blue, and green components in the [0, 1] range.

The CaseComponent code proceeds with the Creative Engine initialization logic:

useEffect(() => {
// if containerRef has not yet been initialized or the engine
// has already been loaded
if (!containerRef.current || isEngineLoaded) {
return;
}
// the initialization config for the Creative Engine
const config = {
page: {
title: {
show: false
}
}
};
// initializing the CE.SDK CreativeEngine
let engineToBeDisposed;
CreativeEngine.init(config).then(async (instance) => {
engineToBeDisposed = instance;
// storing the engine variable
setEngine(instance);
// keeping track of the engine status
setIsEngineLoaded(true);
});
// disposing of the Creative Engine on component unmount
return function shutdownCreativeEngine() {
engineToBeDisposed?.dispose();
};
}, [])

Here, CreativeEngine is initialized by calling the async init() method with a Configuration object, as explained in the headless API guides. Keep in mind that the Creative Engine SDK is built with WebAssembly, which is not part of JavaScript garbage collection. So, when you no longer need a CE.SDK instance anymore, you should call its dispose() method to free up the resources used by the engine.

Then, the logic to load the quote scene into the Creative Engine is defined:

useEffect(
function initializeScene() {
// do nothing if the engine has not yet been loaded
if (!isEngineLoaded) {
return;
}
const container = containerRef.current;
// getting the canvas HTML element containing
// the CE.SDK HTML component
const canvas = engine.element;
// defining the CE.SDK scene initialization function
async function initializeScene() {
// disabling a default CE.SDK behavior
engine.editor.setSettingBool('ubq://doubleClickToCropEnabled', false);
// loading a scene from a local file into the Creative Engine
await engine.scene.loadFromURL(caseAssetPath('/example.scene'));
// appending the canvas element produced by Creative Engine
// to the HTML div element
container.append(canvas);
// zooming on the currently active scene
await engine.scene.zoomToBlock(engine.scene.get(), 0, 0, 0, 0);
// updating the status of the engine
setIsSceneLoaded(true);
}
// initializing the scene
initializeScene();
// removing the canvas element on component unmount
return () => {
canvas.remove();
};
},
[engine, isEngineLoaded]
);

In this hook, the quote CE.SDK scene stored in the /public/cases/headless-design/example.scene is loaded with the loadFromURL() function into the Creative Engine initialized before. Note that if you want to load a CE.SDK scene from a local file, you have to place it in the /public folder. Otherwise, you could not access the scene file via JavaScript.

This scene contains a quote variable and a block for the background image. Note that this quote scene was produced in CE.SDK by a Creator user and exported into a file to be loaded into this app.

After loading the scene, the canvas element produced by CE.SDK after initialization is appended to the container div. The canvas contains everything required to interact with CE.SDK and will show the loaded scene:

The <cesdk-canva /> HTML element

Appending the canvas stored in the element engine property is required to embed the Creative Engine's <cesdk-canvas /> component on the page.

Then, the CaseComponent contains the following two hooks:

// getting the current device pixel ratio (DPR) with an
// utility function
const [dpr] = useDevicePixelRatio();
// zooming on the scene when DPR changes
// (e.g. when the window is moved between monitors)
useEffect(
function refocusAfterDPRChange() {
if (isSceneLoaded && dpr) {
// focusing back on the scene
engine.scene.zoomToBlock(engine.scene.get(), 0, 0, 0, 0);
}
},
[dpr, engine, isSceneLoaded]
);
// zomming on the scene if the canvas size changes.
useEffect(
function refocusAfterCanvasSizeChange() {
if (isSceneLoaded) {
const resizeObserver = new ResizeObserver(async () => {
// zooming back on the scene
await engine.scene.zoomToBlock(engine.scene.get(), 0, 0, 0, 0);
});
// if the engine canvas element gets resized
resizeObserver.observe(engine.element);
// disconnecting the observer on component unmount
return () => {
resizeObserver.disconnect();
};
}
},
[engine, isSceneLoaded]
);

Since the engine canvas was defined to occupy 100% of the available space, these two functions make sure to zoom the scene accordingly in case the DPR or the canvas size changes. If the engine canvas had a fixed height and width, these two functions would not be necessary.

Finally, it is time to look at how the color, font, background image, and quote text chosen by the user affect the CE.SDK scene:

useEffect(
function updateSceneColors() {
if (isSceneLoaded && colorRGBA) {
// getting the page of the scene
const page = engine.block.findByType('page')[0];
// extracing the first text block contained in the scene
const text = engine.block.findByType('//ly.img.ubq/text')[0];
let { r, g, b } = colorRGBA;
// changing the color of the page to make the iamge frame change color
engine.block.setColorRGBA(page, 'fill/solid/color', r, g, b, 1.0);
// changing the color of the text in the page
engine.block.setColorRGBA(text, 'fill/solid/color', r, g, b, 1.0);
}
},
[colorRGBA, engine, isSceneLoaded]
);
useEffect(
function updateFontFamily() {
if (isSceneLoaded && font) {
// extracing the first text block contained in the scene
const textBlock = engine.block.findByType('//ly.img.ubq/text')[0];
// setting the font of the text block
engine.block.setString(textBlock, 'text/fontFileUri', font.value);
}
},
[font, engine, isSceneLoaded]
);
useEffect(
function updateImageFile() {
if (isSceneLoaded && image) {
// extracing the first image block contained in the scene
const imageBlock = engine.block.findByType('image')[0];
// setting the src attribute of the image block
// the load the new background image
engine.block.setString(imageBlock, 'image/imageFileURI', image.full);
// resetting the crop of the block because the aspect ratio of the image
// might change from image to image and you do not want the new
// image to have the aspect ratio of the previous image
engine.block.resetCrop(imageBlock);
}
},
[image, engine, isSceneLoaded]
);
useEffect(
function updateText() {
if (isSceneLoaded && headline) {
// updating the value of the "{{quote}}" variable
// contained in the scene
engine.variable.setString('quote', headline);
}
},
[headline, engine, isSceneLoaded]
);

In a CE.SDK context, a page is a parent element that contains one or more child blocks. Note that the image block has padding and does not occupy the entire scene. Thus, by changing the color of the page, you are basically changing the color of the image frame. This is what happens in the first hook, where the text color also gets updated.

Then, the second hook takes care of updating the text font of the CE.SDK scene, as described in the official documentation. The third hook replaces the image stored in the image block used as a background for the quote in the scene. While the fourth block simply sets the value of the “{{quote}}” variable defined in the scene.

All these hooks would never be called if colorRGBA, font, image, and headline never changed. The selection logic for this React state values is defined in the following four JSX components:

// the background image selector component
<div className={classes.imageSelectionWrapper}>
{IMAGES.map((someImage, i) => (
<button onClick={() => setImage(someImage)} key={someImage.thumb}>
<img
src={someImage.thumb}
className={classNames(classes.image, {
[classes.imageActiveState]: someImage.thumb === image?.thumb
})}
alt={`Example ${i + 1}`}
/>
</button>
))}
</div>
// the text quote input component
<input
type="text"
value={headline ?? ''}
placeholder="Enter Text"
onChange={(e) => setHeadline(e.target.value)}
/>
// the text font selector component
<select
name="font"
id="font"
className={classNames(
'select',
!font?.value && 'select--placeholder'
)}
value={font?.value || 'placeholder'}
onChange={(e) =>
setFont(FONTS.find((font) => font.value === e.target.value))
}
>
<option value="placeholder" disabled>
Select Font
</option>
{FONTS.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
// the custom HEX color picker component
<ColorPicker
positionX="left"
positionY="bottom"
theme="light"
size="lg"
onChange={(hex) => setColorHex(hex)}
value={colorHex}
/>

As you can see, all these components are pretty straightforward, except for ColorPicker. These simply equip the user with the ability to select or specify parameters to provide to CE.SDK to generate the image they want.

In detail, these JSX components correspond to the following HTML elements:

inputs for data selection

This section of the application is then followed by the “Shuffle” button, which executes the following function on click:

// defining the click handler function for the
// "Shuffle" button
const randomizeParameters = () => {
// selecting a random font, image, quote, and color
// from a limited set of values
setFont(FONTS[Math.floor(Math.random() * FONTS.length)]);
setImage(IMAGES[Math.floor(Math.random() * IMAGES.length)]);
setHeadline(QUOTES[Math.floor(Math.random() * QUOTES.length)]);
setColorHex(COLORS[Math.floor(Math.random() * COLORS.length)]);
};

As you can see, the randomizeParameters() function relies on the following four constants you can find at the end of the CaseComponent file:

const COLORS = ['#2B3B52', '#4700BB', '#72332E', '#BA9820', '#FF6363'];
const FONTS = [
{
value:
'/extensions/ly.img.cesdk.fonts/fonts/Playfair_Display/PlayfairDisplay-SemiBold.ttf',
label: 'Playfair display'
},
{
label: 'Poppins',
value: '/extensions/ly.img.cesdk.fonts/fonts/Poppins/Poppins-Bold.ttf'
},
{
label: 'Rasa',
value: '/extensions/ly.img.cesdk.fonts/fonts/Rasa/Rasa-Bold.ttf'
},
{
label: 'Courier Prime',
value:
'/extensions/ly.img.cesdk.fonts/fonts/CourierPrime/CourierPrime-Bold.ttf'
},
{
label: 'Caveat',
value: '/extensions/ly.img.cesdk.fonts/fonts/Caveat/Caveat-Bold.ttf'
}
];
// https://unsplash.com/photos/9COU9FyUIMU
// https://unsplash.com/photos/A2BMfcZH_Ig
// https://unsplash.com/photos/I7NNdMspF0M
// https://unsplash.com/photos/Ay_HG60pHHw
// https://unsplash.com/photos/FIKD9t5_5zQ
const IMAGES = [
{
full: caseAssetPath('/images/wall-1200.jpg'),
thumb: caseAssetPath('/images/wall-300.jpg')
},
{
full: caseAssetPath('/images/paint-1200.jpg'),
thumb: caseAssetPath('/images/paint-300.jpg')
},
{
full: caseAssetPath('/images/window-1200.jpg'),
thumb: caseAssetPath('/images/window-300.jpg')
},
{
full: caseAssetPath('/images/face-1200.jpg'),
thumb: caseAssetPath('/images/face-300.jpg')
},
{
full: caseAssetPath('/images/clouds-1200.jpg'),
thumb: caseAssetPath('/images/clouds-300.jpg')
}
];
const QUOTES = [
'Good design is honest. — Dieter Rams',
'Try not to become a man of success. Rather become a man of value. — Albert Einstein',
'Imagination creates reality. — Richard Wagner',
'Time you enjoy wasting, was not wasted. — John Lennon',
'May the Force be with you. — Obi-Wan Kenobi'
];

These constants store the predefined values for the background image and font selector components, as well as some limited values for the shuffling feature.

In conclusion, this is what the entire CaseComponent JSX component looks like:

import CreativeEngine from '@cesdk/engine';
import classNames from 'classnames';
import { ColorPicker } from 'components/ui/ColorPicker/ColorPicker';
import LoadingSpinner from 'components/ui/LoadingSpinner/LoadingSpinner';
import { useEffect, useMemo, useRef, useState } from 'react';
import { hexToRgba } from './convert';
import classes from './CaseComponent.module.css';
import { caseAssetPath, useDevicePixelRatio } from './util';
const CaseComponent = () => {
/** @type {[import("@cesdk/engine").default, Function]} CreativeEngine */
const [engine, setEngine] = useState();
const [isEngineLoaded, setIsEngineLoaded] = useState(false);
const [isSceneLoaded, setIsSceneLoaded] = useState(false);
const containerRef = useRef(null);
const [headline, setHeadline] = useState();
const [image, setImage] = useState(null);
const [font, setFont] = useState(null);
const [colorHex, setColorHex] = useState('#C0C3C5');
const colorRGBA = useMemo(() => {
let { r, g, b } = hexToRgba(colorHex);
// The engine works with color values from 0 to 1 instead of 0 to 255.
return { r: r / 255, g: g / 255, b: b / 255 };
}, [colorHex]);
useEffect(() => {
//START_HIDDEN_BLOCK
if (navigator.userAgent === 'ReactSnap') return;
//END_HIDDEN_BLOCK
if (!containerRef.current || isEngineLoaded) {
return;
}
/** @type {import("@cesdk/engine").Configuration} */
const config = {
page: {
title: {
show: false
}
}
};
//START_HIDDEN_BLOCK
if (process.env.REACT_APP_USE_LOCAL)
config.baseURL = `${process.env.REACT_APP_URL_HOSTNAME}${process.env.PUBLIC_URL}/assets`;
//END_HIDDEN_BLOCK
let engineToBeDisposed;
CreativeEngine.init(config).then(async (instance) => {
instance.addDefaultAssetSources();
instance.addDemoAssetSources();
//START_HIDDEN_BLOCK
if (process.env.REACT_APP_ADD_CESDK_GLOBALS === 'true') {
window.cyGlobals = { ...window.cyGlobals, cesdk: instance };
}
//END_HIDDEN_BLOCK
engineToBeDisposed = instance;
setEngine(instance);
setIsEngineLoaded(true);
});
return function shutdownCreativeEngine() {
engineToBeDisposed?.dispose();
};
// eslint-disable-next-line
}, []);
useEffect(
function initializeScene() {
if (!isEngineLoaded) {
return;
}
const container = containerRef.current;
const canvas = engine.element;
async function initializeScene() {
engine.editor.setSettingBool('ubq://doubleClickToCropEnabled', false);
await engine.scene.loadFromURL(caseAssetPath('/example.scene'));
container.append(canvas);
await engine.scene.zoomToBlock(engine.scene.get(), 0, 0, 0, 0);
setIsSceneLoaded(true);
}
initializeScene();
return () => {
canvas.remove();
};
},
[engine, isEngineLoaded]
);
// We need to refocus the scene when the DPR changes, e.g when the window is moved between monitors.
const [dpr] = useDevicePixelRatio();
useEffect(
function refocusAfterDPRChange() {
if (isSceneLoaded && dpr) {
engine.scene.zoomToBlock(engine.scene.get(), 0, 0, 0, 0);
}
},
[dpr, engine, isSceneLoaded]
);
// We need to refocus the scene when the canvas size changes.
useEffect(
function refocusAfterCanvasSizeChange() {
if (isSceneLoaded) {
const resizeObserver = new ResizeObserver(async () => {
await engine.scene.zoomToBlock(engine.scene.get(), 0, 0, 0, 0);
});
resizeObserver.observe(engine.element);
return () => {
resizeObserver.disconnect();
};
}
},
[engine, isSceneLoaded]
);
useEffect(
function updateSceneColors() {
if (isSceneLoaded && colorRGBA) {
const page = engine.block.findByType('page')[0];
const text = engine.block.findByType('//ly.img.ubq/text')[0];
let { r, g, b } = colorRGBA;
engine.block.setColorRGBA(page, 'fill/solid/color', r, g, b, 1.0);
engine.block.setColorRGBA(text, 'fill/solid/color', r, g, b, 1.0);
}
},
[colorRGBA, engine, isSceneLoaded]
);
useEffect(
function updateFontFamily() {
if (isSceneLoaded && font) {
const textBlock = engine.block.findByType('//ly.img.ubq/text')[0];
engine.block.setString(textBlock, 'text/fontFileUri', font.value);
}
},
[font, engine, isSceneLoaded]
);
useEffect(
function updateImageFile() {
if (isSceneLoaded && image) {
const imageBlock = engine.block.findByType('image')[0];
engine.block.setString(imageBlock, 'image/imageFileURI', image.full);
// We need to reset the crop after changing an image file to ensure that it is shown in full.
engine.block.resetCrop(imageBlock);
}
},
[image, engine, isSceneLoaded]
);
useEffect(
function updateText() {
if (isSceneLoaded && headline) {
engine.variable.setString('quote', headline);
}
},
[headline, engine, isSceneLoaded]
);
const randomizeParameters = () => {
setFont(FONTS[Math.floor(Math.random() * FONTS.length)]);
setImage(IMAGES[Math.floor(Math.random() * IMAGES.length)]);
setHeadline(QUOTES[Math.floor(Math.random() * QUOTES.length)]);
setColorHex(COLORS[Math.floor(Math.random() * COLORS.length)]);
};
return (
<div className={classes.wrapper}>
<div className={classes.inputsWrapper}>
<h4 className={'h4'}>Select Content</h4>
<div className={classes.imageSelectionWrapper}>
{IMAGES.map((someImage, i) => (
<button onClick={() => setImage(someImage)} key={someImage.thumb}>
<img
src={someImage.thumb}
className={classNames(classes.image, {
[classes.imageActiveState]: someImage.thumb === image?.thumb
})}
alt={`Example ${i + 1}`}
/>
</button>
))}
</div>
<input
type="text"
value={headline ?? ''}
placeholder="Enter Text"
onChange={(e) => setHeadline(e.target.value)}
/>
<div className="flex space-x-2">
<div className="select-wrapper flex-grow">
<select
name="font"
id="font"
className={classNames(
'select',
!font?.value && 'select--placeholder'
)}
value={font?.value || 'placeholder'}
onChange={(e) =>
setFont(FONTS.find((font) => font.value === e.target.value))
}
>
<option value="placeholder" disabled>
Select Font
</option>
{FONTS.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
<ColorPicker
positionX="left"
positionY="bottom"
theme="light"
size="lg"
onChange={(hex) => setColorHex(hex)}
value={colorHex}
/>
</div>
<div>
<button
className="button button--primary"
onClick={() => randomizeParameters()}
>
Shuffle
</button>
</div>
</div>
<div className="flex-grow space-y-2">
<h4 className="h4">Generated Design</h4>
<div ref={containerRef} className={classes.canvas}>
{!isSceneLoaded && <LoadingSpinner />}
</div>
</div>
</div>
);
};
const COLORS = ['#2B3B52', '#4700BB', '#72332E', '#BA9820', '#FF6363'];
const FONTS = [
{
value:
'/extensions/ly.img.cesdk.fonts/fonts/Playfair_Display/PlayfairDisplay-SemiBold.ttf',
label: 'Playfair display'
},
{
label: 'Poppins',
value: '/extensions/ly.img.cesdk.fonts/fonts/Poppins/Poppins-Bold.ttf'
},
{
label: 'Rasa',
value: '/extensions/ly.img.cesdk.fonts/fonts/Rasa/Rasa-Bold.ttf'
},
{
label: 'Courier Prime',
value:
'/extensions/ly.img.cesdk.fonts/fonts/CourierPrime/CourierPrime-Bold.ttf'
},
{
label: 'Caveat',
value: '/extensions/ly.img.cesdk.fonts/fonts/Caveat/Caveat-Bold.ttf'
}
];
// https://unsplash.com/photos/9COU9FyUIMU
// https://unsplash.com/photos/A2BMfcZH_Ig
// https://unsplash.com/photos/I7NNdMspF0M
// https://unsplash.com/photos/Ay_HG60pHHw
// https://unsplash.com/photos/FIKD9t5_5zQ
const IMAGES = [
{
full: caseAssetPath('/images/wall-1200.jpg'),
thumb: caseAssetPath('/images/wall-300.jpg')
},
{
full: caseAssetPath('/images/paint-1200.jpg'),
thumb: caseAssetPath('/images/paint-300.jpg')
},
{
full: caseAssetPath('/images/window-1200.jpg'),
thumb: caseAssetPath('/images/window-300.jpg')
},
{
full: caseAssetPath('/images/face-1200.jpg'),
thumb: caseAssetPath('/images/face-300.jpg')
},
{
full: caseAssetPath('/images/clouds-1200.jpg'),
thumb: caseAssetPath('/images/clouds-300.jpg')
}
];
const QUOTES = [
'Good design is honest. — Dieter Rams',
'Try not to become a man of success. Rather become a man of value. — Albert Einstein',
'Imagination creates reality. — Richard Wagner',
'Time you enjoy wasting, was not wasted. — John Lennon',
'May the Force be with you. — Obi-Wan Kenobi'
];
export default CaseComponent;
<div className={classes.wrapper}>
<div className="caseHeader">
<h3>Automatic Design Generation</h3>
<p>
Use our API and underlying creative engine to autogenerate
ready-to-use designs by selecting input parameters.
</p>
</div>
<div className={classes.inputsWrapper}>
<h4 className={classes.headline}>Select Content</h4>
<div className={classes.imageSelectionWrapper}>
{IMAGES.map((someImage, i) => (
<button onClick={() => setImage(someImage)} key={someImage.thumb}>
<img
src={someImage.thumb}
className={classNames(classes.image, {
[classes.imageActiveState]: someImage.thumb === image?.thumb
})}
alt={`Example ${i + 1}`}
/>
</button>
))}
</div>
<input
type="text"
value={headline ?? ''}
placeholder="Enter Text"
onChange={(e) => setHeadline(e.target.value)}
/>
<div className="flex space-x-2">
<div className="select-wrapper flex-grow">
<select
name="font"
id="font"
className={classNames(
'select',
!font?.value && 'select--placeholder'
)}
value={font?.value || 'placeholder'}
onChange={(e) =>
setFont(FONTS.find((font) => font.value === e.target.value))
}
>
<option value="placeholder" disabled>
Select Font
</option>
{FONTS.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
<ColorPicker
positionX="left"
positionY="bottom"
theme="light"
size="lg"
onChange={(hex) => setColorHex(hex)}
value={colorHex}
/>
</div>
<div>
<button
className="button button--light-white"
onClick={() => randomizeParameters()}
>
Shuffle
</button>
</div>
</div>
<div className="space-y-2">
<h4 className={classes.headline}>Generated Design</h4>
<div ref={containerRef} className={classes.canvas}>
{!isSceneLoaded && <LoadingSpinner />}
</div>
</div>
</div>

Note that before the Creative Engine gets loaded, a custom LoadingSpinner component is shown. This is useful to give users feedback before the CE.SDK is ready to be used.

Conclusion#

In this article, you saw what IMG.LY’s automatic design generation showcase application is, and how it works. This is, of course, a toy example, however, it demonstrates essential Creative Engine APIs and how to interact with them to build any design generation use case.

Note that this was just one of the many showcases developed by the IMG.LY team to demonstrate what you can achieve with the powerful CE.SDK. Try them all out!