Automatic Design Generation with React
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:
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 variableconst [engine, setEngine] = useState();// a flag to specify whether the Creative Engine// has been loaded or notconst [isEngineLoaded, setIsEngineLoaded] = useState(false);// a flag to specify whether the Creative Engine// has loaded the scene or notconst [isSceneLoaded, setIsSceneLoaded] = useState(false);// the reference to the HTML div element// where the Creative Engine is mountedconst containerRef = useRef(null);// the quote written by the userconst [headline, setHeadline] = useState();// the background image URL selected by the userconst [image, setImage] = useState(null);// the font selected by the userconst [font, setFont] = useState(null);// the color in HEX format selected by the userconst [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 functionlet { r, g, b } = hexToRgba(colorHex);// to convert the [0, 255] values to the [0, 1] format used// expected by the Creative Enginereturn { 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 loadedif (!containerRef.current || isEngineLoaded) {return;}// the initialization config for the Creative Engineconst config = {page: {title: {show: false}}};// initializing the CE.SDK CreativeEnginelet engineToBeDisposed;CreativeEngine.init(config).then(async (instance) => {engineToBeDisposed = instance;// storing the engine variablesetEngine(instance);// keeping track of the engine statussetIsEngineLoaded(true);});// disposing of the Creative Engine on component unmountreturn 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 loadedif (!isEngineLoaded) {return;}const container = containerRef.current;// getting the canvas HTML element containing// the CE.SDK HTML componentconst canvas = engine.element;// defining the CE.SDK scene initialization functionasync function initializeScene() {// disabling a default CE.SDK behaviorengine.editor.setSettingBool('ubq://doubleClickToCropEnabled', false);// loading a scene from a local file into the Creative Engineawait engine.scene.loadFromURL(caseAssetPath('/example.scene'));// appending the canvas element produced by Creative Engine// to the HTML div elementcontainer.append(canvas);// zooming on the currently active sceneawait engine.scene.zoomToBlock(engine.scene.get(), 0, 0, 0, 0);// updating the status of the enginesetIsSceneLoaded(true);}// initializing the sceneinitializeScene();// removing the canvas element on component unmountreturn () => {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:
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 functionconst [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 sceneengine.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 sceneawait engine.scene.zoomToBlock(engine.scene.get(), 0, 0, 0, 0);});// if the engine canvas element gets resizedresizeObserver.observe(engine.element);// disconnecting the observer on component unmountreturn () => {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 sceneconst page = engine.block.findByType('page')[0];// extracing the first text block contained in the sceneconst 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 colorengine.block.setColorRGBA(page, 'fill/solid/color', r, g, b, 1.0);// changing the color of the text in the pageengine.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 sceneconst textBlock = engine.block.findByType('//ly.img.ubq/text')[0];// setting the font of the text blockengine.block.setString(textBlock, 'text/fontFileUri', font.value);}},[font, engine, isSceneLoaded]);useEffect(function updateImageFile() {if (isSceneLoaded && image) {// extracing the first image block contained in the sceneconst imageBlock = engine.block.findByType('image')[0];// setting the src attribute of the image block// the load the new background imageengine.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 imageengine.block.resetCrop(imageBlock);}},[image, engine, isSceneLoaded]);useEffect(function updateText() {if (isSceneLoaded && headline) {// updating the value of the "{{quote}}" variable// contained in the sceneengine.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}><imgsrc={someImage.thumb}className={classNames(classes.image, {[classes.imageActiveState]: someImage.thumb === image?.thumb})}alt={`Example ${i + 1}`}/></button>))}</div>// the text quote input component<inputtype="text"value={headline ?? ''}placeholder="Enter Text"onChange={(e) => setHeadline(e.target.value)}/>// the text font selector component<selectname="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<ColorPickerpositionX="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:
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" buttonconst randomizeParameters = () => {// selecting a random font, image, quote, and color// from a limited set of valuessetFont(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_5zQconst 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_BLOCKif (navigator.userAgent === 'ReactSnap') return;//END_HIDDEN_BLOCKif (!containerRef.current || isEngineLoaded) {return;}/** @type {import("@cesdk/engine").Configuration} */const config = {page: {title: {show: false}}};//START_HIDDEN_BLOCKif (process.env.REACT_APP_USE_LOCAL)config.baseURL = `${process.env.REACT_APP_URL_HOSTNAME}${process.env.PUBLIC_URL}/assets`;//END_HIDDEN_BLOCKlet engineToBeDisposed;CreativeEngine.init(config).then(async (instance) => {instance.addDefaultAssetSources();instance.addDemoAssetSources();//START_HIDDEN_BLOCKif (process.env.REACT_APP_ADD_CESDK_GLOBALS === 'true') {window.cyGlobals = { ...window.cyGlobals, cesdk: instance };}//END_HIDDEN_BLOCKengineToBeDisposed = 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}><imgsrc={someImage.thumb}className={classNames(classes.image, {[classes.imageActiveState]: someImage.thumb === image?.thumb})}alt={`Example ${i + 1}`}/></button>))}</div><inputtype="text"value={headline ?? ''}placeholder="Enter Text"onChange={(e) => setHeadline(e.target.value)}/><div className="flex space-x-2"><div className="select-wrapper flex-grow"><selectname="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><ColorPickerpositionX="left"positionY="bottom"theme="light"size="lg"onChange={(hex) => setColorHex(hex)}value={colorHex}/></div><div><buttonclassName="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_5zQconst 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 autogenerateready-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}><imgsrc={someImage.thumb}className={classNames(classes.image, {[classes.imageActiveState]: someImage.thumb === image?.thumb})}alt={`Example ${i + 1}`}/></button>))}</div><inputtype="text"value={headline ?? ''}placeholder="Enter Text"onChange={(e) => setHeadline(e.target.value)}/><div className="flex space-x-2"><div className="select-wrapper flex-grow"><selectname="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><ColorPickerpositionX="left"positionY="bottom"theme="light"size="lg"onChange={(hex) => setColorHex(hex)}value={colorHex}/></div><div><buttonclassName="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!