const { imageHelpers } = window.canva;
const canva = window.canva.init();
// Vertex shader source code
const vertexShaderSource = `
gl_Position = vec4(vertex, 0.0, 1.0);
// Fragment shader source code
const fragmentShaderSource = `
uniform sampler2D uImage;
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
image: await imageHelpers.fromElement(opts.element),
// 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);
const gl = canvas.getContext("webgl");
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.");
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);
const program = gl.createProgram();
throw new Error("Failed to create WebGL program.");
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
"Failed to link WebGL program: " + gl.getProgramInfoLog(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);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
canva.onControlsEvent(({ message: event }) => {
if (event.controlType === "slider") {
state[event.controlId] = event.message.value;
gl.uniform1f(uMixLocation, state.mix);
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.");
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 () => {
gl.uniform1f(uMixLocation, state.mix);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
return await imageHelpers.fromCanvas(state.image.imageType, canvas);
function renderControls() {
canva.updateControlPanel(controls);