Dynamic preset thumbnails

Use dynamically generated images as thumbnails for an editing extension's presets.

To provide the best user experience, editing extensions that support presets should dynamically generate the preset's thumbnails. This lets users preview effects before applying them, which increases engagement with the extension.

This tutorial explains how to dynamically generate preset thumbnails.

If an extension can't generate thumbnails, it must provide static thumbnails.

Step 1: Set up an editing extension

If you haven't already, set up an editing extension that supports presets. The following snippet provides the boilerplate of an extension that's suitable for this tutorial:

const { imageHelpers } = window.canva;
const canva = window.canva.init();
const state = {
image: null,
canvas: null,
selectedPreset: null,
};
canva.onReady(async (opts) => {
state.image = await imageHelpers.fromElement(opts.element);
state.canvas = await imageHelpers.toCanvas(state.image);
document.body.appendChild(state.canvas);
state.selectedPreset = opts.presetId;
if (opts.presetId) {
await renderImage();
}
});
canva.onPresetsRequest(async (opts) => {
return [
{
id: preset.id,
label: preset.label,
image: opts.image,
},
];
});
canva.onPresetSelected(async (opts) => {
state.selectedPreset = opts.presetId;
await renderImage();
});
canva.onImageUpdate(async (opts) => {
// Keep track of the updated image
state.image = opts.image;
// Re-render the user's image
await renderImage();
});
canva.onSaveRequest(async () => {
return await imageHelpers.fromCanvas("image/jpeg", state.canvas);
});
async function renderImage() {
const img = await imageHelpers.toImageElement(state.image);
const context = state.canvas.getContext("2d");
if (state.selectedPreset === "invert") {
context.filter = "invert(100%)";
}
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
}

If anything about this code is unfamiliar, see Simple presets.

Step 2: Create an array of presets

Before the onReady callback, create an array of presets:

const presets = [
{
id: "invert",
label: "Invert",
},
];

Each preset should have an id and a label.

Step 3: Create a function for each preset

For each preset in the array, create an apply function that accepts an image, width, and height:

const presets = [
{
id: "invert",
label: "Invert",
apply: async (image, width, height) => {
// code goes here
},
},
];

This function should apply an effect to the image and return a CanvaImageBlob:

const presets = [
{
id: "invert",
label: "Invert",
apply: async (image, width, height) => {
// Convert the image into a HTMLCanvasElement
const canvas = await imageHelpers.toCanvas(image, { width, height });
// Apply the effect
const context = canvas.getContext("2d");
context.filter = "invert(100%)";
context.drawImage(canvas, 0, 0, canvas.width, canvas.height);
// Return the image as a CanvaImage
return await imageHelpers.fromCanvas("image/jpeg", canvas);
},
},
];

Because of the width and height parameters, this function can apply effects to images of any size.

An array of presets with an apply function is not a strict requirement. It's just a useful pattern for keeping the presets organized.

Step 4: Apply the effect to each thumbnail

In the onPresetsRequest callback, map over the elements in the presets array and call the apply function for each preset:

canva.onPresetsRequest(async (opts) => {
const thumbnails = presets.map(async (preset) => {
return {
id: preset.id,
label: preset.label,
image: await preset.apply(
opts.image,
opts.image.width,
opts.image.height
),
};
});
return await Promise.all(thumbnails);
});

This creates an array of presets with dynamic thumbnails.

Preset thumbnails must be at least 256 × 256 pixels.

You can't call the remoteProcess method from the onPresetsRequest callback.

Step 5: Apply the effect to the user's full-sized image

In the renderImage function, find the selected preset, apply the preset to the full-sized image, and store the result in the state.image property:

async function renderImage() {
// Get the selected preset
const preset = presets.find((preset) => preset.id === state.selectedPreset);
// Do nothing if a preset isn't selected
if (!preset) {
return;
}
// Apply the preset to the full-sized image
state.image = await preset.apply(
state.image,
state.canvas.width,
state.canvas.height
);
// Re-render the user's image
const img = await imageHelpers.toImageElement(state.image);
const context = state.canvas.getContext("2d");
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
}

Example

const { imageHelpers } = window.canva;
const canva = window.canva.init();
const state = {
image: null,
canvas: null,
selectedPreset: null,
};
const presets = [
{
id: "invert",
label: "Invert",
apply: async (image, width, height) => {
// Convert the image into a HTMLCanvasElement
const canvas = await imageHelpers.toCanvas(image, { width, height });
// Apply the effect
const context = canvas.getContext("2d");
context.filter = "invert(100%)";
context.drawImage(canvas, 0, 0, canvas.width, canvas.height);
// Return the image as a CanvaImage
return await imageHelpers.fromCanvas("image/jpeg", canvas);
},
},
];
canva.onReady(async (opts) => {
state.image = await imageHelpers.fromElement(opts.element);
state.canvas = await imageHelpers.toCanvas(state.image);
document.body.appendChild(state.canvas);
if (opts.presetId) {
state.selectedPreset = opts.presetId;
await renderImage();
}
});
canva.onPresetsRequest(async (opts) => {
const thumbnails = presets.map(async (preset) => {
return {
id: preset.id,
label: preset.label,
image: await preset.apply(
opts.image,
opts.image.width,
opts.image.height
),
};
});
return await Promise.all(thumbnails);
});
canva.onPresetSelected(async (opts) => {
// Keep track of the selected preset
state.selectedPreset = opts.presetId;
// Re-render the user's image
await renderImage();
});
canva.onImageUpdate(async (opts) => {
// Keep track of the updated image
state.image = opts.image;
// Re-render the user's image
await renderImage();
});
canva.onSaveRequest(async () => {
return await imageHelpers.fromCanvas("image/jpeg", state.canvas);
});
async function renderImage() {
// Get the selected preset
const preset = presets.find((preset) => preset.id === state.selectedPreset);
// Do nothing if the user hasn't selected a preset
if (!preset) {
return;
}
// Apply the preset to the full-sized image
state.image = await preset.apply(
state.image,
state.canvas.width,
state.canvas.height
);
// Re-render the user's image
const img = await imageHelpers.toImageElement(state.image);
const context = state.canvas.getContext("2d");
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
}