def construct_embed(source_url): shader_id = source_url.split("/")[-1] return f'<iframe width="640" height="360" frameborder="0" src="https://www.shadertoy.com/embed/{shader_id}?gui=true&t=0&paused=true&muted=true" allowfullscreen></iframe>' def make_iframe(shader_code): #keep a single function? script = make_script(shader_code) return f"""<iframe width="640" height="420" srcdoc=\'{script}\' allowfullscreen></iframe>""" def make_script(shader_code): # code copied and fixed(escaping single quotes to double quotes!!!) from https://webglfundamentals.org/webgl/webgl-shadertoy.html script = (""" <!-- Licensed under a BSD license. See license.html for license --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> <title>WebGL - Shadertoy</title> <link type="text/css" href="https://webglfundamentals.org/webgl/resources/webgl-tutorials.css" rel="stylesheet" /> <style> .divcanvas { position: relative; display: inline-block; } canvas { display: block; } .playpause { position: absolute; left: 10px; top: 10px; width: 100%; height: 100%; font-size: 60px; justify-content: center; align-items: center; color: rgba(255, 255, 255, 0.3); transition: opacity 0.2s ease-in-out; } .playpausehide, .playpause:hover { opacity: 0; } .iframe .divcanvas { display: block; } </style> </head> <body> <div class="divcanvas"> <canvas id="canvas"></canvas> <div class="playpause">▶</div> </div> \nblank canvas here indicates that some of the shadertoy specific functions are not yet supported with this implementation (like #define I believe). you can always copy and paste the code into a shadertoy.com window to try. </body> <!-- for most samples webgl-utils only provides shader compiling/linking and canvas resizing because why clutter the examples with code thats the same in every sample. See https://webglfundamentals.org/webgl/lessons/webgl-boilerplate.html and https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html for webgl-utils, m3, m4, and webgl-lessons-ui. --> <script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script> <script> "use strict"; function main() { // Get A WebGL context /** @type {HTMLCanvasElement} */ const canvas = document.querySelector("#canvas"); const gl = canvas.getContext("webgl"); if (!gl) { return; } const vs = ` // an attribute will receive data from a buffer attribute vec4 a_position; // all shaders have a main function void main() { // gl_Position is a special variable a vertex shader // is responsible for setting gl_Position = a_position; } `; const fs = ` precision highp float; uniform vec2 iResolution; uniform vec2 iMouse; uniform float iTime; """ + shader_code + """ void main() { mainImage(gl_FragColor, gl_FragCoord.xy); } `; // setup GLSL program const program = webglUtils.createProgramFromSources(gl, [vs, fs]); // look up where the vertex data needs to go. const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); // look up uniform locations const resolutionLocation = gl.getUniformLocation(program, "iResolution"); const mouseLocation = gl.getUniformLocation(program, "iMouse"); const timeLocation = gl.getUniformLocation(program, "iTime"); // Create a buffer to put three 2d clip space points in const positionBuffer = gl.createBuffer(); // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer) gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // fill it with a 2 triangles that cover clipspace gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1, -1, // first triangle 1, -1, -1, 1, -1, 1, // second triangle 1, -1, 1, 1, ]), gl.STATIC_DRAW); const playpauseElem = document.querySelector(".playpause"); const inputElem = document.querySelector(".divcanvas"); inputElem.addEventListener("mouseover", requestFrame); inputElem.addEventListener("mouseout", cancelFrame); let mouseX = 0; let mouseY = 0; function setMousePosition(e) { const rect = inputElem.getBoundingClientRect(); mouseX = e.clientX - rect.left; mouseY = rect.height - (e.clientY - rect.top) - 1; // bottom is 0 in WebGL } inputElem.addEventListener("mousemove", setMousePosition); inputElem.addEventListener("touchstart", (e) => { e.preventDefault(); playpauseElem.classList.add("playpausehide"); requestFrame(); }, {passive: false}); inputElem.addEventListener("touchmove", (e) => { e.preventDefault(); setMousePosition(e.touches[0]); }, {passive: false}); inputElem.addEventListener("touchend", (e) => { e.preventDefault(); playpauseElem.classList.remove("playpausehide"); cancelFrame(); }, {passive: false}); let requestId; function requestFrame() { if (!requestId) { requestId = requestAnimationFrame(render); } } function cancelFrame() { if (requestId) { cancelAnimationFrame(requestId); requestId = undefined; } } let then = 0; let time = 0; function render(now) { requestId = undefined; now *= 0.001; // convert to seconds const elapsedTime = Math.min(now - then, 0.1); time += elapsedTime; then = now; webglUtils.resizeCanvasToDisplaySize(gl.canvas); // Tell WebGL how to convert from clip space to pixels gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // Tell it to use our program (pair of shaders) gl.useProgram(program); // Turn on the attribute gl.enableVertexAttribArray(positionAttributeLocation); // Bind the position buffer. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER) gl.vertexAttribPointer( positionAttributeLocation, 2, // 2 components per iteration gl.FLOAT, // the data is 32bit floats false, // dont normalize the data 0, // 0 = move forward size * sizeof(type) each iteration to get the next position 0, // start at the beginning of the buffer ); gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); gl.uniform2f(mouseLocation, mouseX, mouseY); gl.uniform1f(timeLocation, time); gl.drawArrays( gl.TRIANGLES, 0, // offset 6, // num vertices to process ); requestFrame(); } requestFrame(); requestAnimationFrame(cancelFrame); } main(); </script> </html> """) return script