🔍

SLITSCAN vs. GenAI


<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Webcam Slit-Scanner</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        body, html {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background-color: #000;
            font-family: 'Inter', sans-serif;
            color: white;
        }
        canvas {
            display: block;
            width: 100vw;
            height: 100vh;
        }
    </style>
</head>
<body class="bg-gray-900 text-white">

    <div id="ui-container" class="absolute top-4 left-4 z-10 flex flex-col items-start space-y-2 p-4 bg-gray-800 bg-opacity-70 rounded-xl shadow-lg">
        <h1 class="text-xl font-bold">Webcam Slit-Scanner</h1>
        
        <div id="sliders-container" class="mt-4 w-full space-y-4">
            <!-- Schieberegler werden hier dynamisch hinzugefügt -->
        </div>

        <div id="status-message" class="text-sm text-gray-300 mt-2">Warte auf Kamerazugriff...</div>
    </div>
    
    <div id="webgl-container"></div>

    <script>
        // Fragment-Shader für den Slit-Scan-Effekt (Buffer-Pass)
        const slitScanFragmentShader = `
            uniform vec2 iResolution;
            uniform sampler2D iChannel0; // Vorheriger Puffer (readBuffer)
            uniform sampler2D iChannel1; // Webcam-Feed (videoTexture)
            uniform float iTime;
            uniform float u_speed;
            uniform float u_delay;

            void main() {
                // Get the screen UV and the vertical 'slit' position
                vec2 uv = gl_FragCoord.xy / iResolution.xy;
                float slitpos = mod(iTime * u_speed, iResolution.y + u_delay);
                
                // Sample the previous frame
                vec3 last = texture2D(iChannel0, uv).rgb;
                vec3 col = last;
                
                // Draw the 1-pixel thin new slit
                float slit = step(gl_FragCoord.y, slitpos) - step(gl_FragCoord.y, slitpos - 1.0);
                col = mix(col, texture2D(iChannel1, uv).rgb, slit);
                
                gl_FragColor = vec4(col, 1.0);
            }
        `;

        // Fragment-Shader für das endgültige Rendern auf den Bildschirm (einfacher Passthrough)
        const screenPassFragmentShader = `
            uniform vec2 iResolution;
            uniform sampler2D iChannel0;

            void main() {
                vec2 uv = gl_FragCoord.xy / iResolution.xy;
                gl_FragColor = texture2D(iChannel0, uv);
            }
        `;

        // Einfacher Vertex-Shader, der für beide Materialien verwendet wird
        const vertexShader = `
            void main() {
                gl_Position = vec4(position, 1.0);
            }
        `;

        const uniforms = {
            u_speed: { label: "Geschwindigkeit", value: 50.0, min: 10.0, max: 200.0, step: 1.0 },
            u_delay: { label: "Verzögerung", value: 120.0, min: 0.0, max: 200.0, step: 1.0 }
        };

        let camera, scene, renderer, videoTexture;
        let video = document.createElement('video');
        let isCameraReady = false;
        
        // Ping-Pong-Puffer
        let readBuffer, writeBuffer;

        // Materialien für die beiden Render-Pässe
        let slitScanMaterial, screenPassMaterial;
        
        const container = document.getElementById('webgl-container');
        const statusMessage = document.getElementById('status-message');
        const slidersContainer = document.getElementById('sliders-container');

        function createSliders() {
            slidersContainer.innerHTML = '';
            for (const name in uniforms) {
                const uniform = uniforms[name];
                const sliderGroup = document.createElement('div');
                sliderGroup.className = 'w-full';
                sliderGroup.innerHTML = `
                    <label for="${name}" class="text-sm font-medium block">${uniform.label}</label>
                    <div class="flex items-center space-x-2 mt-1">
                        <input type="range" id="${name}" name="${name}" min="${uniform.min}" max="${uniform.max}" step="${uniform.step}" value="${uniform.value}"
                            class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer">
                        <span id="${name}-value" class="w-12 text-sm text-center font-mono">${uniform.value}</span>
                    </div>
                `;
                slidersContainer.appendChild(sliderGroup);
                
                const slider = document.getElementById(name);
                const valueSpan = document.getElementById(`${name}-value`);

                slider.addEventListener('input', (event) => {
                    const newValue = parseFloat(event.target.value);
                    uniform.value = newValue;
                    valueSpan.textContent = newValue.toFixed(3);
                });
            }
        }

        function init() {
            scene = new THREE.Scene();
            camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 100);
            camera.position.z = 1;

            renderer = new THREE.WebGLRenderer();
            renderer.setSize(window.innerWidth, window.innerHeight);
            container.appendChild(renderer.domElement);
            
            const options = {
                minFilter: THREE.LinearFilter,
                magFilter: THREE.LinearFilter,
                format: THREE.RGBAFormat,
                type: THREE.FloatType
            };
            readBuffer = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, options);
            writeBuffer = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, options);

            slitScanMaterial = new THREE.ShaderMaterial({
                uniforms: {
                    iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                    iChannel0: { value: null }, 
                    iChannel1: { value: null },
                    iTime: { value: 0.0 },
                    u_speed: { value: uniforms.u_speed.value },
                    u_delay: { value: uniforms.u_delay.value }
                },
                vertexShader: vertexShader,
                fragmentShader: slitScanFragmentShader
            });

            screenPassMaterial = new THREE.ShaderMaterial({
                uniforms: {
                    iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                    iChannel0: { value: null },
                },
                vertexShader: vertexShader,
                fragmentShader: screenPassFragmentShader
            });
            
            const geometry = new THREE.PlaneGeometry(2, 2);
            const quad = new THREE.Mesh(geometry, slitScanMaterial); 
            scene.add(quad);
            
            createSliders();
            
            if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
                const constraints = { video: { width: 1280, height: 720 } };
                navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
                    statusMessage.textContent = 'Kamera verbunden.';
                    isCameraReady = true;
                    video.srcObject = stream;
                    video.play();
                    videoTexture = new THREE.VideoTexture(video);
                }).catch(function(error) {
                    statusMessage.textContent = 'Fehler beim Zugriff auf die Kamera: ' + error.name;
                    console.error("Camera access denied or failed: ", error);
                });
            } else {
                statusMessage.textContent = 'Dein Browser unterstützt keine Webcam.';
            }

            window.addEventListener('resize', onWindowResize, false);
        }

        function onWindowResize() {
            const width = window.innerWidth;
            const height = window.innerHeight;
            renderer.setSize(width, height);
            slitScanMaterial.uniforms.iResolution.value.x = width;
            slitScanMaterial.uniforms.iResolution.value.y = height;
            screenPassMaterial.uniforms.iResolution.value.x = width;
            screenPassMaterial.uniforms.iResolution.value.y = height;
            readBuffer.setSize(width, height);
            writeBuffer.setSize(width, height);
        }
        
        function animate() {
            requestAnimationFrame(animate);

            if (isCameraReady) {
                if (videoTexture) {
                    videoTexture.needsUpdate = true;
                }
                
                slitScanMaterial.uniforms.iTime.value = performance.now() / 1000;
                slitScanMaterial.uniforms.u_speed.value = uniforms.u_speed.value;
                slitScanMaterial.uniforms.u_delay.value = uniforms.u_delay.value;
                
                renderer.setRenderTarget(writeBuffer);
                renderer.clear();
                slitScanMaterial.uniforms.iChannel0.value = readBuffer.texture;
                slitScanMaterial.uniforms.iChannel1.value = videoTexture;
                scene.children[0].material = slitScanMaterial;
                renderer.render(scene, camera);

                renderer.setRenderTarget(null);
                screenPassMaterial.uniforms.iChannel0.value = writeBuffer.texture;
                scene.children[0].material = screenPassMaterial;
                renderer.render(scene, camera);

                let temp = readBuffer;
                readBuffer = writeBuffer;
                writeBuffer = temp;
            }
        }

        window.onload = function() {
            init();
            animate();
        };

    </script>
</body>
</html>