Touch mode
Learn how an extension can receive and handle pointer events.
By default, editing extensions don't receive pointer events. To receive pointer events, an extension must enable touch mode.
When touch mode is enabled:
  • The iframe that contains the user's image:
    • stretches to the edge of the page
    • is rotated the right way up
    • isn't cropped
  • Canva hides the controls that appear around the edges of the page.
  • Done and Cancel buttons appear in the toolbar.
  • The background color of the editor changes.
This creates a more focused editing experience that is ideal for making precise edits to an image.
This tutorial explains how to add touch mode to an editing extension.
An editing extension that enables touch mode can receive all types of pointer events—not just touch events.

Step 1: Enable touch interactions

  1. 1.
    Navigate to an app via the Developer Portal.
  2. 2.
    From the Extensions page, expand the Editing panel.
  3. 3.
    Click Enable touch interactions.

Step 2: Enable touch mode

When Enable touch interactions is turned on, touch mode is still disabled by default. This lets the extension control exactly when touch mode is enabled.
To enable touch mode, call the toggleTouchMode method with an argument of true. To turn it off, call the same method with an argument of false.
The following snippet demonstrates how to create a button for toggling touch mode:
1
const { imageHelpers } = window.canva;
2
const canva = window.canva.init();
3
4
const state = {
5
canvas: null,
6
touchModeEnabled: false,
7
};
8
9
canva.onReady(async (opts) => {
10
const image = await imageHelpers.fromElement(opts.element);
11
state.canvas = await imageHelpers.toCanvas(image);
12
document.body.appendChild(state.canvas);
13
renderControls();
14
});
15
16
canva.onControlsEvent(async (opts) => {
17
if (opts.message.controlId === "touchModeButton") {
18
if (state.touchModeEnabled) {
19
await disableTouchMode();
20
} else {
21
await enableTouchMode();
22
}
23
}
24
});
25
26
function renderControls() {
27
const controls = [
28
canva.create("button", {
29
id: "touchModeButton",
30
label: state.touchModeEnabled
31
? "Disable touch mode"
32
: "Enable touch mode",
33
}),
34
];
35
36
canva.updateControlPanel(controls);
37
}
38
39
async function enableTouchMode() {
40
// Enable touch mode
41
state.touchModeEnabled = true;
42
canva.toggleTouchMode(true);
43
44
// Re-render the controls
45
renderControls();
46
}
47
48
async function disableTouchMode() {
49
// Disable touch mode
50
state.touchModeEnabled = false;
51
canva.toggleTouchMode(false);
52
53
// Re-render the controls
54
renderControls();
55
}
Copied!
This is what the extension looks like with touch mode disabled:
This is what the extension looks like with touch mode enabled:

Step 4: Position the user's image

The previous code snippet has a couple of problems:
  • The user's image is stretched to the edges of the iframe.
  • The image isn't perfectly centered.
The first problem happens because the toCanvas method assigns style.width and style.height attributes to the HTMLCanvasElement:
1
canvas.style.width = "100%";
2
canvas.style.height = "100%";
Copied!
When an extension doesn't use touch mode, this is convenient, since the user's image is always the same aspect ratio as the iframe. In touch mode though, the iframe stretches to the edges of the page, which means the image doesn't necessarily have the same aspect ratio as the iframe.
The second problem happens because the HTMLCanvasElement is in the top-left corner of the iframe. This doesn't matter when touch mode is off, but it looks strange when touch mode is on.
The following snippet demonstrates one way to solve these problems via the enableTouchMode function:
1
async function enableTouchMode() {
2
// Enable touch mode
3
state.touchModeEnabled = true;
4
canva.toggleTouchMode(true);
5
6
// Set the height of the "html" and "body" elements
7
document.querySelector("html").style.height = "100%";
8
document.body.style.height = "100%";
9
10
// Center everything in the "body" element
11
document.body.style.display = "grid";
12
document.body.style.alignItems = "center";
13
document.body.style.justifyContent = "center";
14
15
// Don't stretch the canvas to the edges of the iframe
16
state.canvas.style.width = null;
17
state.canvas.style.height = null;
18
19
// Don't let the canvas extend beyond the dimensions of the iframe
20
state.canvas.style.maxWidth = "100%";
21
state.canvas.style.maxHeight = "100%";
22
23
// Re-render the controls
24
renderControls();
25
}
Copied!
This snippet:
  • Sets the height of the html and body element to "100%".
  • Aligns the HTMLCanvasElement in the center of the page.
  • Sets the width and height of the HTMLCanvasElement to null.
  • Sets max width and max height of the HTMLCanvasElement to "100%".
You also need to add the following statements to the disableTouchMode function to reset the width and height of the HTMLCanvasElement when the user disables touch mode:
1
state.canvas.style.width = "100%";
2
state.canvas.style.height = "100%";
Copied!
Based on these changes, enabling touch mode doesn't distort or oddly position the user's image.

Step 5: Handle pointer events

When touch mode is enabled, the extension can receive pointer events. There are no strict requirements about what an extension does with these pointer events, but a common use-case is to add drawing features to an extension.
To add drawing features to an extension, add an isDrawing property to the state object:
1
const state = {
2
canvas: null,
3
isDrawing: false,
4
touchModeEnabled: false,
5
};
Copied!
This property can track whether or not the user is pressing down on their left-mouse button.
Next, create event handlers for the following events:
  • onpointerdown
  • onpointermove
  • onpointerup
The following snippet demonstrates how to create, register, and deregister these events:
1
async function enableTouchMode() {
2
// Enable touch mode
3
state.touchModeEnabled = true;
4
canva.toggleTouchMode(true);
5
6
// Register event handlers
7
state.canvas.onpointerdown = handlePointerDown;
8
state.canvas.onpointermove = handlePointerMove;
9
state.canvas.onpointerup = handlePointerUp;
10
11
// Re-render the controls
12
renderControls();
13
}
14
15
async function disableTouchMode() {
16
// Disable touch mode
17
state.touchModeEnabled = false;
18
canva.toggleTouchMode(false);
19
20
// Remove event handlers
21
state.canvas.onpointerdown = null;
22
state.canvas.onpointermove = null;
23
state.canvas.onpointerup = null;
24
25
// Re-render the controls
26
renderControls();
27
}
28
29
function handlePointerDown(event) {
30
state.isDrawing = true;
31
const context = state.canvas.getContext("2d");
32
const { x, y } = getMousePosition(state.canvas, event);
33
context.moveTo(x, y);
34
}
35
36
function handlePointerMove(event) {
37
if (state.isDrawing) {
38
const context = state.canvas.getContext("2d");
39
const { x, y } = getMousePosition(state.canvas, event);
40
context.lineTo(x, y);
41
context.stroke();
42
}
43
}
44
45
function handlePointerUp() {
46
state.isDrawing = false;
47
}
48
49
function getMousePosition(canvas, event) {
50
const rect = canvas.getBoundingClientRect();
51
52
const scaleX = canvas.width / rect.width;
53
const scaleY = canvas.height / rect.height;
54
55
return {
56
x: (event.clientX - rect.left) * scaleX,
57
y: (event.clientY - rect.top) * scaleY,
58
};
59
}
Copied!
This code is based on snippets from Exploring canvas drawing techniques. To learn about how the code works, see the linked page.
Something worth highlighting is the getMousePosition function:
1
function getMousePosition(canvas, event) {
2
const rect = canvas.getBoundingClientRect();
3
4
const scaleX = canvas.width / rect.width;
5
const scaleY = canvas.height / rect.height;
6
7
return {
8
x: (event.clientX - rect.left) * scaleX,
9
y: (event.clientY - rect.top) * scaleY,
10
};
11
}
Copied!
This function accurately calculates the X and Y coordinates of the pointer device, even if:
  • The HTMLCanvasElement is scaled using CSS attributes.
  • The user adjusts the zoom level of the document.
Without this logic, interacting with the HTMLCanvasElement may result in surprising behavior, such as clicks not registering in the expected position. To learn more, see this StackOverflow answer.
Based on these changes, it's now possible to draw over the image:

Step 6: Reset the user's image

When a user enables touch mode, Done and Cancel buttons appear in the toolbar.
If the user clicks the Cancel button, the extension should reset the user's image to the state it was in before touch mode was enabled.
To do this, add a previousBlob property to the state object:
1
const state = {
2
canvas: null,
3
isDrawing: false,
4
previousBlob: null,
5
touchModeEnabled: false,
6
};
Copied!
Then, when a user enables touch mode, store a copy of the user's image in the previousBlob property:
1
async function enableTouchMode() {
2
// Keep track of the previous image
3
canva.toggleSpinner("preview", true);
4
state.previousBlob = await imageHelpers.fromCanvas(
5
"image/jpeg",
6
state.canvas
7
);
8
canva.toggleSpinner("preview", false);
9
10
// Enable touch mode
11
state.touchModeEnabled = true;
12
canva.toggleTouchMode(true);
13
14
// Register event handlers
15
state.canvas.onpointerdown = handlePointerDown;
16
state.canvas.onpointermove = handlePointerMove;
17
state.canvas.onpointerup = handlePointerUp;
18
19
// Re-render the controls
20
renderControls();
21
}
Copied!
This snippet uses the toggleSpinner method to show and hide a spinner, as storing a copy of the user's image is not instant.
Next, add a commit parameter to the disableTouchMode function. If this parameter is false, replace the user's image with the previous version of the user's image:
1
async function disableTouchMode(commit) {
2
// Disable touch mode
3
state.touchModeEnabled = false;
4
canva.toggleTouchMode(false);
5
6
// Reset the image if the user clicks the "Cancel" button
7
if (!commit) {
8
document.body.removeChild(state.canvas);
9
state.canvas = await imageHelpers.toCanvas(state.previousBlob);
10
document.body.appendChild(state.canvas);
11
}
12
13
// Remove event handlers
14
state.canvas.onpointerdown = null;
15
state.canvas.onpointermove = null;
16
state.canvas.onpointerup = null;
17
18
// Re-render the controls
19
renderControls();
20
}
Copied!
To detect when a user clicks the Done or Cancel button, register a callback with the onTouchModeExit method:
1
canva.onTouchModeExit((opts) => {
2
console.log(opts);
3
});
Copied!
This callback receives an opts object that contains a commit property. If the user selects the Done button, commit is true. If the user selects the Cancel button, commit is false.
Pass the commit property directly into the disableTouchMode function:
1
canva.onTouchModeExit((opts) => {
2
disableTouchMode(opts.commit);
3
});
Copied!
Based on these changes, selecting the Done button persists the user's changes to the image and selecting the Cancel button resets the image.
But if the user selects the Disable touch mode button, the extension resets the image.
To fix this, pass true into the disableTouchMode function:
1
canva.onControlsEvent(async (opts) => {
2
if (opts.message.controlId === "touchModeButton") {
3
if (state.touchModeEnabled) {
4
await disableTouchMode(true);
5
} else {
6
await enableTouchMode();
7
}
8
}
9
});
Copied!
After making this change, the user's image is only reset when they select the Cancel button.

Example

1
const { imageHelpers } = window.canva;
2
const canva = window.canva.init();
3
4
const state = {
5
canvas: null,
6
isDrawing: false,
7
previousBlob: null,
8
touchModeEnabled: false,
9
};
10
11
canva.onReady(async (opts) => {
12
const image = await imageHelpers.fromElement(opts.element);
13
state.canvas = await imageHelpers.toCanvas(image);
14
document.body.appendChild(state.canvas);
15
renderControls();
16
});
17
18
canva.onControlsEvent(async (opts) => {
19
if (!opts.message.commit) {
20
return;
21
}
22
23
if (opts.message.controlId === "touchModeButton") {
24
if (state.touchModeEnabled) {
25
await disableTouchMode(true);
26
} else {
27
await enableTouchMode();
28
}
29
}
30
});
31
32
canva.onTouchModeExit((opts) => {
33
disableTouchMode(opts.commit);
34
});
35
36
canva.onSaveRequest(async () => {
37
return await imageHelpers.fromCanvas("image/jpeg", state.canvas);
38
});
39
40
function renderControls() {
41
const controls = [
42
canva.create("button", {
43
id: "touchModeButton",
44
label: state.touchModeEnabled
45
? "Disable touch mode"
46
: "Enable touch mode",
47
}),
48
];
49
canva.updateControlPanel(controls);
50
}
51
52
async function enableTouchMode() {
53
// Keep track of the previous image
54
canva.toggleSpinner("preview", true);
55
state.previousBlob = await imageHelpers.fromCanvas(
56
"image/jpeg",
57
state.canvas
58
);
59
canva.toggleSpinner("preview", false);
60
61
// Enable touch mode
62
state.touchModeEnabled = true;
63
canva.toggleTouchMode(true);
64
65
// Set the height of the "html" and "body" elements
66
document.querySelector("html").style.height = "100%";
67
document.body.style.height = "100%";
68
69
// Center everything in the "body" element
70
document.body.style.display = "grid";
71
document.body.style.alignItems = "center";
72
document.body.style.justifyContent = "center";
73
74
// Don't stretch the canvas to the edges of the iframe
75
state.canvas.style.width = null;
76
state.canvas.style.height = null;
77
78
// Don't let the canvas extend beyond the dimensions of the iframe
79
state.canvas.style.maxWidth = "100%";
80
state.canvas.style.maxHeight = "100%";
81
82
// Register event handlers
83
state.canvas.onpointerdown = handlePointerDown;
84
state.canvas.onpointermove = handlePointerMove;
85
state.canvas.onpointerup = handlePointerUp;
86
87
// Re-render the controls
88
renderControls();
89
}
90
91
async function disableTouchMode(commit) {
92
// Disable touch mode
93
state.touchModeEnabled = false;
94
canva.toggleTouchMode(false);
95
96
// Reset the image if the user clicks the "Cancel" button
97
if (!commit) {
98
document.body.removeChild(state.canvas);
99
state.canvas = await imageHelpers.toCanvas(state.previousBlob);
100
document.body.appendChild(state.canvas);
101
}
102
103
// Reset the width of the canvas
104
state.canvas.style.width = "100%";
105
state.canvas.style.height = "100%";
106
107
// Remove event handlers
108
state.canvas.onpointerdown = null;
109
state.canvas.onpointermove = null;
110
state.canvas.onpointerup = null;
111
112
// Re-render the controls
113
renderControls();
114
}
115
116
function handlePointerDown(event) {
117
state.isDrawing = true;
118
const context = state.canvas.getContext("2d");
119
const { x, y } = getMousePosition(state.canvas, event);
120
context.moveTo(x, y);
121
}
122
123
function handlePointerMove(event) {
124
if (state.isDrawing) {
125
const context = state.canvas.getContext("2d");
126
const { x, y } = getMousePosition(state.canvas, event);
127
context.lineTo(x, y);
128
context.stroke();
129
}
130
}
131
132
function handlePointerUp() {
133
state.isDrawing = false;
134
}
135
136
function getMousePosition(canvas, event) {
137
const rect = canvas.getBoundingClientRect();
138
139
const scaleX = canvas.width / rect.width;
140
const scaleY = canvas.height / rect.height;
141
142
return {
143
x: (event.clientX - rect.left) * scaleX,
144
y: (event.clientY - rect.top) * scaleY,
145
};
146
}
Copied!