Customizable presets

Create an editing extension that provides configurable presets.

A customizable preset is a preset with adjustable settings. This is in contrast to simple presets, which offer the binary choice of turning a preset off or on.

This tutorial explains how to create a customizable preset that:

  • Inverts the colors of the user's image.

  • Lets the user control the degree of inversion.

Step 1: Set up an editing extension

If you haven't already, set up an editing extension that renders the user's image. 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,
};
canva.onReady(async (opts) => {
// Download the user's image
state.image = await imageHelpers.fromElement(opts.element);
// Convert the user's image into a HTMLCanvasElement
state.canvas = await imageHelpers.toCanvas(state.image);
// Render the user's image
document.body.appendChild(state.canvas);
});
canva.onImageUpdate(async (opts) => {
state.image = opts.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');
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
}

Step 2: Enable presets

By default, editing extensions don't support presets. The feature needs to be enabled.

To enable presets:

  1. Navigate to an app via the Developer Portal.

  2. From the Extensions page, expand the Editing panel.

  3. Select Enable presets.

Step 3: Render a thumbnail for each preset

Register a callback with the onPresetsRequest method that returns an array of presets. Then, for each preset, set the kind property to "customizable":

canva.onPresetsRequest(async (opts) => {
return [
{
id: 'invert',
label: 'Invert',
image: opts.image,
kind: 'customizable',
},
];
});

This code uses the user's un-manipulated image as the thumbnail for the preset. To learn how to generate thumbnails for presets, see Dynamic preset thumbnails.

Preset thumbnails must be at least 256 × 256 pixels.

Canva runs the onReady callback immediately before the onPresetsRequest callback, so be mindful of any potential side-effects the onReady callback may cause.

Step 4: Keep track of the preset's values

The main difference between customizable and simple presets is that, in a customizable preset, the extension must keep track of the settings associated with each preset and not just the id of the selected preset.

You can, for instance, represent an "Invert" preset as a value from 0 to 100, with 0 meaning "not inverted" and 100 meaning "completely inverted".

To keep track of this value, add an invert property to the state object:

const state = {
image: null,
canvas: null,
invert: null,
};

When a user selects a preset, the extension must set the initial values for the preset. These values determine the (un-customized) effect that's applied to the user's image.

In the onReady callback, identify the selected preset and assign a value to the invert property:

canva.onReady(async (opts) => {
// Download the user's image
state.image = await imageHelpers.fromElement(opts.element);
// Convert the user's image into a HTMLCanvasElement
state.canvas = await imageHelpers.toCanvas(state.image);
// Render the user's image
document.body.appendChild(state.canvas);
if (opts.presetId === 'invert') {
state.invert = 100;
}
});

You also need to implement the same logic in the onPresetSelected callback:

canva.onPresetSelected(async (opts) => {
if (opts.presetId === 'invert') {
state.invert = 100;
}
});

A single preset could have more than one value associated with it.

Step 5: Re-render the user's image

When a user selects a preset, the extension must re-render the user's image with the relevant effects applied:

async function renderImage() {
// Convert the user's image into a HTMLImageElement
const img = await imageHelpers.toImageElement(state.image);
// Apply an effect to the user's image
const context = state.canvas.getContext('2d');
context.filter = `invert(${state.invert}%)`;
// Re-draw the user's image
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
}

You need to re-render the user's image in the onReady callback:

canva.onReady(async (opts) => {
// Download the user's image
state.image = await imageHelpers.fromElement(opts.element);
// Convert the user's image into a HTMLCanvasElement
state.canvas = await imageHelpers.toCanvas(state.image);
// Render the user's image
document.body.appendChild(state.canvas);
if (opts.presetId === 'invert') {
state.invert = 100;
}
if (opts.presetId) {
await renderImage();
}
});

…and in the onPresetSelected callback:

canva.onPresetSelected(async (opts) => {
if (opts.presetId === 'invert') {
state.invert = 100;
}
await renderImage();
});

Based on this code, users can open the control panel for the selected preset. Until the extension renders controls though, the control panel is empty.

Step 6: Render controls for the presets

To render the controls for the extension's presets, create a renderControls function:

function renderControls() {
const controls = [
canva.create('slider', {
id: 'invert',
min: 0,
max: 100,
step: 1,
value: state.invert,
label: 'Invert',
}),
];
canva.updateControlPanel(controls);
}

This function renders a Slider control that lets users adjust the degree of inversion applied to the user's image. The value of each control should map to a property in the state object.

You need to render the control panel in the onReady callback:

canva.onReady(async (opts) => {
// Download the user's image
state.image = await imageHelpers.fromElement(opts.element);
// Convert the user's image into a HTMLCanvasElement
state.canvas = await imageHelpers.toCanvas(state.image);
// Render the user's image
document.body.appendChild(state.canvas);
if (opts.presetId === 'invert') {
state.invert = 100;
}
if (opts.presetId) {
await renderImage();
}
renderControls();
});

…and in the onPresetSelected callback:

canva.onPresetSelected(async (opts) => {
if (opts.presetId === 'invert') {
state.invert = 100;
}
await renderImage();
renderControls();
});

When a user interacts with a control, the extension must:

  • Update the relevant values in the state object.

  • Re-render the user's image.

  • Re-render the control panel.

The following snippet demonstrates how to perform these actions in the onControlsEvent callback:

canva.onControlsEvent(async (opts) => {
// Do nothing if the user is actively interacting with the control
if (!opts.message.commit) {
return;
}
// Update the value of the control
state[opts.message.controlId] = opts.message.message.value;
// Re-render the user's image
await renderImage();
// Re-render the controls
renderControls();
});

This code assumes that the id of the control exists as a property in the state object.

The opts.presetId property in the onReady callback is sometimes undefined. In these cases, the extension must render the user's image without a preset applied.

Example

const { imageHelpers } = window.canva;
const canva = window.canva.init();
const state = {
image: null,
canvas: null,
invert: null,
};
canva.onReady(async (opts) => {
// Download the user's image
state.image = await imageHelpers.fromElement(opts.element);
// Convert the user's image into a HTMLCanvasElement
state.canvas = await imageHelpers.toCanvas(state.image);
// Render the user's image
document.body.appendChild(state.canvas);
// If the user selects the "invert" preset, set the initial value
if (opts.presetId === 'invert') {
state.invert = 100;
}
// If the user selects a preset, re-render the user's image
if (opts.presetId) {
await renderImage();
}
// Render the controls
renderControls();
});
canva.onControlsEvent(async (opts) => {
// Do nothing if the user is still interacting with the control
if (!opts.message.commit) {
return;
}
// Update the value of the control
state[opts.message.controlId] = opts.message.message.value;
// Re-render the user's image
await renderImage();
// Re-render the controls
renderControls();
});
canva.onPresetsRequest(async (opts) => {
return [
{
id: 'invert',
label: 'Invert',
image: opts.image,
kind: 'customizable',
},
];
});
canva.onPresetSelected(async (opts) => {
// If the user selects the "invert" preset, set the initial value
if (opts.presetId === 'invert') {
state.invert = 100;
}
// Re-render the user's image
await renderImage();
// Re-render the controls
renderControls();
});
canva.onImageUpdate(async (opts) => {
state.image = opts.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');
context.filter = `invert(${state.invert}%)`;
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
}
function renderControls() {
const controls = [
canva.create('slider', {
id: 'invert',
min: 0,
max: 100,
step: 1,
value: state.invert,
label: 'Invert',
}),
];
canva.updateControlPanel(controls);
}