| | import streamlit as st
|
| | import streamlit.components.v1 as components
|
| |
|
| | st.title("Mobile-Friendly Shader Demo")
|
| |
|
| |
|
| | html_code = """
|
| | <canvas id="glcanvas"></canvas>
|
| | <script type="text/javascript">
|
| | // Setup canvas
|
| | const canvas = document.getElementById('glcanvas');
|
| | const gl = canvas.getContext('webgl');
|
| | if (!gl) { alert('WebGL not supported'); }
|
| |
|
| | // Resize canvas to fit screen
|
| | function resizeCanvas() {
|
| | canvas.width = window.innerWidth * window.devicePixelRatio;
|
| | canvas.height = window.innerHeight * 0.6 * window.devicePixelRatio; // ~60% height
|
| | }
|
| | window.addEventListener('resize', resizeCanvas);
|
| | resizeCanvas();
|
| |
|
| | // Vertex shader
|
| | const vertCode = `
|
| | attribute vec4 position;
|
| | void main() { gl_Position = position; }
|
| | `;
|
| |
|
| | // Fragment shader
|
| | const fragCode = `
|
| | precision mediump float;
|
| | uniform float iTime;
|
| | uniform vec2 iMouse;
|
| | uniform vec2 iResolution;
|
| | void main() {
|
| | vec2 uv = gl_FragCoord.xy / iResolution.xy;
|
| | vec3 color = vec3(
|
| | uv.x + iMouse.x*0.5 + 0.5*sin(iTime),
|
| | uv.y + iMouse.y*0.5 + 0.5*cos(iTime),
|
| | 0.5 + 0.5*sin(iTime)
|
| | );
|
| | gl_FragColor = vec4(color, 1.0);
|
| | }
|
| | `;
|
| |
|
| | // Shader compile helper
|
| | function compileShader(gl, source, type) {
|
| | const shader = gl.createShader(type);
|
| | gl.shaderSource(shader, source);
|
| | gl.compileShader(shader);
|
| | if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
|
| | console.error(gl.getShaderInfoLog(shader));
|
| | return null;
|
| | }
|
| | return shader;
|
| | }
|
| |
|
| | // Compile shaders
|
| | const vertShader = compileShader(gl, vertCode, gl.VERTEX_SHADER);
|
| | const fragShader = compileShader(gl, fragCode, gl.FRAGMENT_SHADER);
|
| |
|
| | // Link program
|
| | const program = gl.createProgram();
|
| | gl.attachShader(program, vertShader);
|
| | gl.attachShader(program, fragShader);
|
| | gl.linkProgram(program);
|
| | gl.useProgram(program);
|
| |
|
| | // Fullscreen quad
|
| | const vertices = new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]);
|
| | const buffer = gl.createBuffer();
|
| | gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
| | gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
| | const position = gl.getAttribLocation(program, "position");
|
| | gl.enableVertexAttribArray(position);
|
| | gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
| |
|
| | // Uniform locations
|
| | const iTime = gl.getUniformLocation(program, "iTime");
|
| | const iResolution = gl.getUniformLocation(program, "iResolution");
|
| | const iMouse = gl.getUniformLocation(program, "iMouse");
|
| |
|
| | // Initial mouse
|
| | let mouseX = 0.5;
|
| | let mouseY = 0.5;
|
| |
|
| | // Mouse / touch events
|
| | canvas.addEventListener('mousemove', e => {
|
| | const rect = canvas.getBoundingClientRect();
|
| | mouseX = (e.clientX - rect.left) / rect.width;
|
| | mouseY = 1.0 - (e.clientY - rect.top) / rect.height;
|
| | });
|
| | canvas.addEventListener('touchmove', e => {
|
| | e.preventDefault();
|
| | const rect = canvas.getBoundingClientRect();
|
| | const touch = e.touches[0];
|
| | mouseX = (touch.clientX - rect.left) / rect.width;
|
| | mouseY = 1.0 - (touch.clientY - rect.top) / rect.height;
|
| | }, {passive:false});
|
| |
|
| | // Animation loop
|
| | function render() {
|
| | gl.viewport(0, 0, canvas.width, canvas.height);
|
| | gl.uniform2f(iResolution, canvas.width, canvas.height);
|
| | gl.uniform2f(iMouse, mouseX, mouseY);
|
| | gl.uniform1f(iTime, performance.now() / 1000.0);
|
| | gl.drawArrays(gl.TRIANGLES, 0, 6);
|
| | requestAnimationFrame(render);
|
| | }
|
| | render();
|
| | </script>
|
| | """
|
| |
|
| | components.html(html_code, height=400)
|
| | st.markdown("**Instructions:** Move your mouse (desktop) or touch (mobile) the canvas to change colors in real time.")
|
| |
|