Customizable presets
Create an editing extension that provides configurable presets.
A customizable preset is a preset that has adjustable settings. This is in contrast to simple presets, which offer the binary choice of applying or un-applying a preset.
This topic explains how to create a customizable preset that inverts the colors of the user’s image and lets the user control the degree of inversion.

Prerequisites

  • You’re comfortable with JavaScript.
  • You know how to create an editing extension.

Step 1: Set up an editing extension

To begin, set up an editing extension that renders the user’s image.
You can use the following snippet as a starting point for this tutorial:
1
const { imageHelpers } = window.canva;
2
const canva = window.canva.init();
3
4
const state = {
5
element: null,
6
image: null,
7
canvas: null,
8
};
9
10
canva.onReady(async (opts) => {
11
// Keep track of the element
12
state.element = opts.element;
13
14
// Download the preview image
15
state.image = await imageHelpers.fromElement(state.element, "preview");
16
17
// Convert the preview image into an HTMLCanvasElement
18
state.canvas = await imageHelpers.toCanvas(state.image);
19
20
// Render the preview image
21
document.body.appendChild(state.canvas);
22
});
23
24
canva.onImageUpdate(async (opts) => {
25
// Keep track of the updated image
26
state.image = opts.image;
27
28
// Re-render the image
29
await renderImage();
30
});
31
32
canva.onSaveRequest(async () => {
33
// Download the full image
34
state.image = await imageHelpers.fromElement(state.element, "full");
35
36
// Re-render the image
37
await renderImage();
38
39
// Return the image to Canva
40
return await imageHelpers.fromCanvas(state.image.imageType, state.canvas);
41
});
42
43
async function renderImage() {
44
// Convert the image into an HTMLImageElement
45
const img = await imageHelpers.toImageElement(state.image);
46
47
// Draw the image into an HTMLCanvasElement
48
const context = state.canvas.getContext("2d");
49
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
50
}
Copied!

Step 2: Enable presets

By default, editing extensions don’t support presets. The feature needs to be enabled.
To enable presets, follow these steps::
  1. 1.
    Navigate to an app via the Developer Portal.
  2. 2.
    From the Extensions page, expand the Editing panel.
  3. 3.
    Select Enable presets.

Step 3: Create thumbnails for each preset

All presets need a thumbnail. Canva renders these thumbnails in the side panel and users can select them to apply the preset. The process of creating thumbnails is a little different depending on whether the extension's presets are static or dynamic.

Static presets

To create a thumbnail for a static preset, follow these steps:
  1. 1.
    Create an image file that represents the preset.
  2. 2.
    Upload the image file to the extension's Assets field.
The image files must meet the following requirements:
  • JPEG, JPG, or PNG format
  • 1:1 aspect ratio (square)
  • Minimum dimensions of 256 x 256 pixels
  • Represent the preset’s behavior
This is an example of a thumbnail for an "Invert" preset:

Dynamic presets

To learn how to create thumbnails for dynamic presets, see Dynamic preset thumbnails.

Step 4: Create a Presets JSON file

This step is only necessary for extensions that offer static presets. You can skip this step if the Use dynamic presets option is enabled.
A Presets JSON file is a JSON file that defines the extension's presets.
For this tutorial, copy the following JSON into a file named presets.json:
1
[
2
{
3
"id": "invert",
4
"label": "Invert",
5
"asset": "invert.png",
6
"kind": "customizable"
7
}
8
]
Copied!
Then upload it to the extension's Assets field.
To learn how to create a Presets JSON file from scratch, see Presets JSON.

Step 5: Handle preset requests

This step is only necessary while Canva migrates away from an old way of handling presets. In the future, this step will only be necessary for dynamic presets.
When a user opens the Edit image panel, Canva emits an event. Extensions that support presets must listen for this event by registering a callback with the onPresetsRequest method:
1
canva.onPresetsRequest(async (opts) => {
2
// code goes here
3
});
Copied!
This callback must return an array of objects with the following properties:
  • id - A unique ID for the preset.
  • label - A human readable label for the preset.
  • image - The thumbnail to use for the preset, as a CanvaImageBlob.
  • kind - The type of preset. To create a customizable preset, set this to"customizable". (You can also omit this property, as "customizable" is the default value.)
Each object creates a preset in the Edit image panel.
To use the an asset for a preset's thumbnail, add an assets property to the state object:
1
const state = {
2
assets: null,
3
element: null,
4
image: null,
5
canvas: null,
6
};
Copied!
In the onReady callback, keep track of the assets:
1
canva.onReady(async (opts) => {
2
// Keep track of the assets
3
state.assets = opts.assets;
4
5
// Keep track of the element
6
state.element = opts.element;
7
8
// Download the image
9
state.image = await imageHelpers.fromElement(state.element, "preview");
10
11
// Convert the image into an HTMLCanvasElement
12
state.canvas = await imageHelpers.toCanvas(state.image);
13
14
// Render the image
15
document.body.appendChild(state.canvas);
16
});
Copied!
Then use the fromUrl method to convert the asset into a CanvaImageBlob:
1
canva.onPresetsRequest(async (opts) => {
2
const assetUrl = state.assets["invert.png"];
3
const image = await imageHelpers.fromUrl(assetUrl);
4
5
return [
6
{
7
id: "invert",
8
label: "Invert",
9
kind: "customizable",
10
image,
11
},
12
];
13
});
Copied!

Step 6: Keep track of the preset’s settings

The main difference between simple and customizable presets is that, when an extension has customizable presets, it must keep track of the settings associated with the presets, rather than just the id of the selected preset.
For example, you can represent an “Invert” preset as a value from 0 to 100, with 0 meaning “not inverted” and 100 meaning “completely inverted”. A more complex preset may combine multiple variables (e.g. hue, saturation, and brightness) to determine the preset’s effect.
To keep track of a preset’s settings, add a property for each setting to the state object. The following snippet adds an invert property to the state object:
1
const state = {
2
assets: null,
3
element: null,
4
image: null,
5
canvas: null,
6
invert: null,
7
};
Copied!
When a user selects a preset, the extension must set the initial values for the preset. These values determine the default, un-customized effect.
The extension needs to handle the selection of a preset in two places: in the onReady callback and in the onPresetSelected callback.

In the onReady callback

1
canva.onReady(async (opts) => {
2
// Keep track of the assets
3
state.assets = opts.assets;
4
5
// Keep track of the element
6
state.element = opts.element;
7
8
// Download the image
9
state.image = await imageHelpers.fromElement(state.element, "preview");
10
11
// Convert the preview image into an HTMLCanvasElement
12
state.canvas = await imageHelpers.toCanvas(state.image);
13
14
// Render the preview image
15
document.body.appendChild(state.canvas);
16
17
// If the user selects the "invert" preset, set the initial value
18
if (opts.presetId === "invert") {
19
state.invert = 100;
20
}
21
});
Copied!
In some situations, the opts.presetId parameter may be undefined. In these cases, the extension must render the user’s image without a preset applied.

In the onPresetSelected callback

1
canva.onPresetSelected(async (opts) => {
2
// If the user selects the "invert" preset, set the initial value
3
if (opts.presetId === "invert") {
4
state.invert = 100;
5
}
6
});
Copied!

Step 7: Apply the selected preset

When rendering the user’s image, use the preset’s settings to apply an effect to the user’s image. This is most easily achieved by refactoring the logic for rendering a user’s image into a reusable function.
The following snippet demonstrates how to add this logic to the renderImage function:
1
async function renderImage() {
2
// Convert the image into an HTMLImageElement
3
const img = await imageHelpers.toImageElement(state.image);
4
5
// Apply an effect to the image
6
const context = state.canvas.getContext("2d");
7
context.filter = `invert(${state.invert}%)`;
8
9
// Draw the image into an HTMLCanvasElement
10
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
11
}
Copied!
Until the user selects a preset, the state.invert property is null and no preset is applied to the user’s image.
An extension needs to handle the application of a preset in two places: in the onReady callback and in the onPresetSelected callback.

In the onReady callback

1
canva.onReady(async (opts) => {
2
// Keep track of the assets
3
state.assets = opts.assets;
4
5
// Keep track of the element
6
state.element = opts.element;
7
8
// Download the image
9
state.image = await imageHelpers.fromElement(state.element, "preview");
10
11
// Render the preview image
12
document.body.appendChild(state.canvas);
13
14
// If the user selects the "invert" preset, set the initial value
15
if (opts.presetId === "invert") {
16
state.invert = 100;
17
}
18
19
// If the user selects a preset, re-render the image
20
if (opts.presetId) {
21
await renderImage();
22
}
23
});
Copied!

In the onPresetSelected callback

1
canva.onPresetSelected(async (opts) => {
2
// If the user selects the "invert" preset, set the initial value
3
if (opts.presetId === "invert") {
4
state.invert = 100;
5
}
6
7
// Re-render the image
8
await renderImage();
9
});
Copied!

Step 8: Render rich controls for the presets

If you select a customizable preset, an icon appears on top of the preset’s thumbnail. This indicates that you can select the preset again to open a control panel to adjust the preset’s settings.
To render rich controls for the extension’s presets, create a renderControls function:
1
function renderControls() {
2
const controls = [
3
canva.create("slider", {
4
id: "invert",
5
min: 0,
6
max: 100,
7
step: 1,
8
value: state.invert,
9
label: "Invert",
10
}),
11
];
12
canva.updateControlPanel(controls);
13
}
Copied!
This function renders a Slider control that lets the user adjust the degree of inversion applied to the user’s image. The value of the control should map to a property in the state object.
You need to render the rich controls in two places: in the onReady callback and in the onPresetSelected callback.

In the onReady callback

1
canva.onReady(async (opts) => {
2
// Keep track of the assets
3
state.assets = opts.assets;
4
5
// Keep track of the element
6
state.element = opts.element;
7
8
// Download the image
9
state.image = await imageHelpers.fromElement(state.element, "preview");
10
11
// Render the preview image
12
document.body.appendChild(state.canvas);
13
14
// If the user selects the "invert" preset, set the initial value
15
if (opts.presetId === "invert") {
16
state.invert = 100;
17
}
18
19
// If the user selects a preset, re-render the image
20
if (opts.presetId) {
21
await renderImage();
22
}
23
24
// Re-render the controls
25
renderControls();
26
});
Copied!

In the onPresetSelected callback

1
canva.onPresetSelected(async (opts) => {
2
// If the user selects the "invert" preset, set the initial value
3
if (opts.presetId === "invert") {
4
state.invert = 100;
5
}
6
7
// Re-render the image
8
await renderImage();
9
10
// Re-render the controls
11
renderControls();
12
});
Copied!

Step 9: Handle rich control events

When a user interacts with a rich control, the extension should:
  • Update the relevant values in the state object.
  • Re-render the image.
  • Re-render the controls.
The following snippet demonstrates how to handle control events with the onControlsEvent callback:
1
canva.onControlsEvent(async (opts) => {
2
// Do nothing if the user is actively interacting with the control
3
if (!opts.message.commit) {
4
return;
5
}
6
7
// Update the value of the control
8
state[opts.message.controlId] = opts.message.message.value;
9
10
// Re-render the image
11
await renderImage();
12
13
// Re-render the controls
14
renderControls();
15
});
Copied!
This code assumes the controlId of the control exists as a property in the state object.

Example

index.js

1
const { imageHelpers } = window.canva;
2
const canva = window.canva.init();
3
4
const state = {
5
assets: null,
6
element: null,
7
image: null,
8
canvas: null,
9
invert: null,
10
};
11
12
canva.onReady(async (opts) => {
13
// Keep track of the assets
14
state.assets = opts.assets;
15
16
// Keep track of the element
17
state.element = opts.element;
18
19
// Download the image
20
state.image = await imageHelpers.fromElement(state.element, "preview");
21
22
// Convert the preview image into an HTMLCanvasElement
23
state.canvas = await imageHelpers.toCanvas(state.image);
24
25
// Render the preview image
26
document.body.appendChild(state.canvas);
27
28
// If the user selects the "invert" preset, set the initial value
29
if (opts.presetId === "invert") {
30
state.invert = 100;
31
}
32
33
// If the user selects a preset, re-render the image
34
if (opts.presetId) {
35
await renderImage();
36
}
37
38
// Re-render the controls
39
renderControls();
40
});
41
42
canva.onControlsEvent(async (opts) => {
43
// Do nothing if the user is still interacting with the control
44
if (!opts.message.commit) {
45
return;
46
}
47
48
// Update the value of the control
49
state[opts.message.controlId] = opts.message.message.value;
50
51
// Re-render the image
52
await renderImage();
53
54
// Re-render the controls
55
renderControls();
56
});
57
58
canva.onPresetsRequest(async (opts) => {
59
const assetUrl = state.assets["invert.png"];
60
const image = await imageHelpers.fromUrl(assetUrl);
61
62
return [
63
{
64
id: "invert",
65
label: "Invert",
66
kind: "customizable",
67
image,
68
},
69
];
70
});
71
72
canva.onPresetSelected(async (opts) => {
73
// If the user selects the "invert" preset, set the initial value
74
if (opts.presetId === "invert") {
75
state.invert = 100;
76
}
77
78
// Re-render the image
79
await renderImage();
80
81
// Re-render the controls
82
renderControls();
83
});
84
85
canva.onImageUpdate(async (opts) => {
86
// Keep track of the updated image
87
state.image = opts.image;
88
89
// Re-render the image
90
await renderImage();
91
});
92
93
canva.onSaveRequest(async () => {
94
// Download the full image
95
state.image = await imageHelpers.fromElement(state.element, "full");
96
97
// Re-render the image
98
await renderImage();
99
100
// Return the image to Canva
101
return await imageHelpers.fromCanvas(state.image.imageType, state.canvas);
102
});
103
104
async function renderImage() {
105
// Convert the image into an HTMLImageElement
106
const img = await imageHelpers.toImageElement(state.image);
107
108
// Apply an effect to the image
109
const context = state.canvas.getContext("2d");
110
context.filter = `invert(${state.invert}%)`;
111
112
// Draw the image into an HTMLCanvasElement
113
context.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
114
}
115
116
function renderControls() {
117
const controls = [
118
canva.create("slider", {
119
id: "invert",
120
min: 0,
121
max: 100,
122
step: 1,
123
value: state.invert,
124
label: "Invert",
125
}),
126
];
127
canva.updateControlPanel(controls);
128
}
Copied!

presets.json

1
[
2
{
3
"id": "invert",
4
"label": "Invert",
5
"asset": "invert.png",
6
"kind": "customizable"
7
}
8
]
Copied!

invert.png

Last modified 27d ago