WebGL

Learn how (and why) to enable WebGL for an extension.

In the Developer Portal, some extensions have a Requires WebGL option. If you enable this option, Canva only shows the extension to users who are using a WebGL-enabled browser. If your extension depends on WebGL, you must enable this option.

Notes

  • Canva only supports WebGL 1.x, not WebGL 2.x and beyond. This is because some modern web browsers, such as Safari, don't enable WebGL 2.x by default.

  • You can't get the "webgl" context of a HTMLCanvasElement that's created with the toCanvas method. This is because the toCanvas method creates a "2d" context and a single HTMLCanvasElement can't have more than one type of context.

Example

This is an example of an editing extension that uses WebGL to transform the user's image.

const { imageHelpers } = window.canva;
const canva = window.canva.init();
// Vertex shader source code
const vertexShaderSource = `
attribute vec2 vertex;
varying highp vec2 uv;
void main(void)
{
gl_Position = vec4(vertex, 0.0, 1.0);
uv = vertex * 0.5 + 0.5;
uv.y = 1.0 - uv.y;
}
`;
// Fragment shader source code
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D uImage;
uniform vec2 uImageSize;
uniform float uMix;
varying highp vec2 uv;
void main(void)
{
vec4 source = texture2D(uImage, uv);
vec4 target = source * vec4(0.0, 0.76, 0.8, 1.0);
gl_FragColor = mix(source, target, uMix);
}
`;
canva.onReady(async (opts) => {
// Set up the initial state
const state = {
mix: 1.0,
image: await imageHelpers.fromElement(opts.element),
};
// Create a canvas
// Set the dimensions of the canvas to match the image
// use styles to scale to the element size
const canvas = document.createElement("canvas");
canvas.width = state.image.width;
canvas.height = state.image.height;
canvas.style.width = "100%";
canvas.style.height = "100%";
document.body.append(canvas);
// Create WebGL context
const gl = canvas.getContext("webgl");
if (gl == null) {
throw new Error("Failed to create WebGL context.");
}
// Ensure we can render images this size
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const maxRenderBufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);
const maxSize = Math.max(state.image.width, state.image.height);
if (maxSize > maxTextureSize || maxSize > maxRenderBufferSize) {
throw new Error("Failed to render image. The image is too large.");
}
// Compile vertex shader
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
if (vertexShader == null) {
throw new Error("Failed to create vertex shader.");
}
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);
let message = gl.getShaderInfoLog(vertexShader);
if (message != null && message.length > 0) {
throw new Error(message);
}
// Compile fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
if (fragmentShader == null) {
throw new Error("Failed to create fragment shader.");
}
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);
message = gl.getShaderInfoLog(fragmentShader);
if (message != null && message.length > 0) {
throw new Error(message);
}
// Compile program
const program = gl.createProgram();
if (program == null) {
throw new Error("Failed to create WebGL program.");
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(
"Failed to link WebGL program: " + gl.getProgramInfoLog(program)
);
}
gl.useProgram(program);
// Create quad geometry to render the image onto
const vertexAttributeLocation = gl.getAttribLocation(program, "vertex");
const vertexBuffer = gl.createBuffer();
const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(vertexAttributeLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vertexAttributeLocation);
// Upload the image to the GPU
const image = await imageHelpers.toImageElement(state.image);
const uImageLocation = gl.getUniformLocation(program, "uImage");
const uImage = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, uImage);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.uniform1i(uImageLocation, 0);
// Set the image size uniform
const uImageSizeLocation = gl.getUniformLocation(program, "uImageSize");
gl.uniform2fv(uImageSizeLocation, [state.image.width, state.image.height]);
// Set the mix uniform amount
const uMixLocation = gl.getUniformLocation(program, "uMix");
gl.uniform1f(uMixLocation, state.mix);
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
renderControls();
canva.onControlsEvent(({ message: event }) => {
if (event.controlType === "slider") {
state[event.controlId] = event.message.value;
}
if (event.commit) {
renderControls();
}
// Update the uniform
gl.uniform1f(uMixLocation, state.mix);
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
});
canva.onImageUpdate(async ({ image: newImage }) => {
// Check that the image can be rendered
const newImageSize = Math.max(newImage.width, newImage.height);
if (newImageSize > maxTextureSize || newImageSize > maxRenderBufferSize) {
throw new Error("Failed to render image. The image is too large.");
}
// Update the state
state.image = newImage;
// Resize the viewport
canvas.width = state.image.width;
canvas.height = state.image.height;
gl.viewport(0, 0, canvas.width, canvas.height);
// Upload the new image to the GPU
const img = await imageHelpers.toImageElement(newImage);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, uImage);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// Set the image size uniform
gl.uniform2fv(uImageSizeLocation, [state.image.width, state.image.height]);
});
canva.onSaveRequest(async () => {
// Update the uniform
gl.uniform1f(uMixLocation, state.mix);
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
return await imageHelpers.fromCanvas(state.image.imageType, canvas);
});
function renderControls() {
const controls = [
canva.create("slider", {
id: "mix",
label: "mix",
value: state.mix,
min: 0,
max: 1,
step: 0.01,
}),
];
canva.updateControlPanel(controls);
}
});
Contents
Notes
Example