<!--

UPD: This example is a part of a series of examples of shaders, and some parts of the code are inherited from the previous examples and they are not necessary for this particular effect. The cleaned version of this script, without unnecessary dependencies and objects is here:

https://github.com/sfi0zy/shaders-gallery

-->


<div class='canvas-container'></div>

<script src='https://unpkg.com/three@0.99.0/build/three.min.js'></script>
<!-- <script src='https://unpkg.com/three@0.99.0/examples/js/controls/OrbitControls.js'></script> -->
<script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/EffectComposer.js'></script>
<script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/RenderPass.js'></script>
<script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/ShaderPass.js'></script>
<script src='https://unpkg.com/three@0.99.0/examples/js/shaders/CopyShader.js'></script>
<!-- <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/LuminosityHighPassShader.js'></script> -->
<!-- <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/UnrealBloomPass.js'></script> -->

<script src='https://unpkg.com/animejs@2.2.0/anime.min.js'></script>

<script id='plane-vertex-shader' type='x-shader/x-vertex'>
    
    uniform sampler2D uImage1;
    uniform sampler2D uImage2;
    uniform vec2 uImageSize;
    uniform vec2 uWindowSize;
    uniform vec2 uMousePosition;
    uniform float uTime;
    uniform float uAnimationTime;
    
    varying vec3 vPosition;
    varying vec2 vUv;

    void main() {
        vec3 delta = vec3(0.0, 0.0, 0.0);
        vec3 newPosition = position + delta;
        
        vUv = uv;

        gl_Position = vec4(newPosition, 1.0);
    }

</script>


<script id='plane-fragment-shader' type='x-shader/x-fragment'>
    
    uniform sampler2D uImage1;
    uniform sampler2D uImage2;
    uniform vec2 uImageSize;
    uniform vec2 uWindowSize;
    uniform vec2 uMousePosition;
    uniform float uTime;
    uniform float uAnimationTime;
    
    varying vec2 vUv;
    
    vec2 rotate(vec2 v, float angle) {
	    float s = sin(angle);
	    float c = cos(angle);
        
	    mat2 m = mat2(c, -s, s, c);
        
	    return m * v;
    }
    
    
    void main() {
        vec2 ratio = vec2(
            min((uWindowSize.x / uWindowSize.y) / (uImageSize.x / uImageSize.y), 1.0),
            min((uWindowSize.y / uWindowSize.x) / (uImageSize.y / uImageSize.x), 1.0)
        );

        vec2 uv = vec2(
            vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
            vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
        );
        
        vec2 centeredUV = vec2(uv.x - 0.5, uv.y - 0.5);
        float centeredUVLength = length(centeredUV);
        
        vec2 scaledUV = centeredUV / cos(uAnimationTime * 1.2);
        
        vec2 temp1 = rotate(scaledUV, smoothstep(0.1, 1.0, uAnimationTime));
        vec2 rotatedUV1 = vec2(temp1.x + 0.5, temp1.y + 0.5);
        
        vec2 temp2 = rotate(centeredUV, (uAnimationTime - 1.0) / 3.0);
        vec2 rotatedUV2 = vec2(temp2.x + 0.5, temp2.y + 0.5);
        
        vec2 delta = vec2(-sin(centeredUV.x), -sin(centeredUV.y)) * centeredUVLength * 2.0;
        
        vec2 delta1 = uAnimationTime * delta;
        vec2 delta2 = (1.0 - uAnimationTime) * delta;
        
        float mask = smoothstep(
            0.6,
            1.0,
            uAnimationTime + uAnimationTime * centeredUVLength * 2.0);
        
        vec4 color1 = texture2D(uImage1, rotatedUV1 + delta1);
        vec4 color2 = texture2D(uImage2, rotatedUV2 + delta2);
        
        gl_FragColor = mix(color1, color2, mask);
    }

    
</script>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    overflow: hidden;
}

.canvas-container {
    height: 100vh;
    width: 100vw;
    background: #000000;
}
View Compiled
let SCENE;
let CAMERA;
let RENDERER;
let CONTROLS;
let COMPOSER;

let TIMES = {
    GLOBAL: 10, // Let it be non zero at start
    ANIMATION: 0
};

let WINDOW_SIZE = new THREE.Vector2(window.innerWidth, window.innerHeight);
let MOUSE_POSITION = new THREE.Vector2(WINDOW_SIZE.x / 2.0, WINDOW_SIZE.y / 2.0);


main();


function main() {
    init();
    onWindowResize();
    animate();

    /* ----- */

    let animation = anime({
        targets: TIMES,
        ANIMATION: 1,
        duration: 3000,
        delay: 1500,
        easing: 'easeInOutQuad'
    });

    setInterval(() => {
        SCENE.traverse(function(child) {
            if (child instanceof THREE.Mesh && child.material.type === 'ShaderMaterial') {
                const temp = child.material.uniforms.uImage1.value;
                
                child.material.uniforms.uImage1.value = child.material.uniforms.uImage2.value;
                child.material.uniforms.uImage2.value = temp;
            }
        });
        
        animation.restart();
    }, 4500);

    /* ----- */
}


function init() {
    initScene();
    initCamera();
    initRenderer();
    initComposer();
    initControls();
    initEventListeners();

    createObjects();

    document.querySelector('.canvas-container').appendChild(RENDERER.domElement);
}


function initScene() {
    SCENE = new THREE.Scene();

    //initLights();
}


function initLights() {
    //const point = new THREE.PointLight(0xffffff, 1, 0);
    //point.position.set(0, 100, 50);
    //SCENE.add(point);
}


function initCamera() {
    CAMERA = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
    CAMERA.position.z = 5;
}


function initRenderer() {
    RENDERER = new THREE.WebGLRenderer({ alpha: true });
    RENDERER.setPixelRatio(window.devicePixelRatio);
    RENDERER.setSize(window.innerWidth, window.innerHeight);
    RENDERER.shadowMap.enabled = true;
    RENDERER.shadowMapSort = true;
    RENDERER.setClearColor(0x000000, 0.3);
}


function initComposer() {
    COMPOSER = new THREE.EffectComposer(RENDERER);
    COMPOSER.setSize(window.innerWidth, window.innerHeight);

    const renderPass = new THREE.RenderPass(SCENE, CAMERA);
    COMPOSER.addPass(renderPass);

    renderPass.renderToScreen = true;
}


function initControls() {
    /*
    CONTROLS = new THREE.OrbitControls(CAMERA);
    CONTROLS.enableZoom = false;
    CONTROLS.minPolarAngle = Math.PI * 1 / 4;
    CONTROLS.maxPolarAngle = Math.PI * 3 / 4;
    CONTROLS.update();
    */
}


function initEventListeners() {
    window.addEventListener('resize', onWindowResize);
    document.addEventListener('mousemove', onMouseMove);

    onWindowResize();
}


function onWindowResize() {
    WINDOW_SIZE.set(window.innerWidth, window.innerHeight);

    CAMERA.aspect = window.innerWidth / window.innerHeight;
    CAMERA.updateProjectionMatrix();

    RENDERER.setSize(window.innerWidth, window.innerHeight);
    COMPOSER.setSize(window.innerWidth, window.innerHeight);
}


function onMouseMove(e) {
    MOUSE_POSITION.set(parseFloat(e.clientX), parseFloat(WINDOW_SIZE.y - e.clientY));    
}


function animate() {
    requestAnimationFrame(animate);
    // CONTROLS.update();
    TIMES.GLOBAL += 0.005;
    updateUniforms();
    render();
}


function updateUniforms() {
    SCENE.traverse(function(child) {
        if (child instanceof THREE.Mesh && child.material.type === 'ShaderMaterial') {
            child.material.uniforms.uTime.value = TIMES.GLOBAL;
            child.material.uniforms.uAnimationTime.value = TIMES.ANIMATION;
            child.material.uniforms.uWindowSize.value = WINDOW_SIZE;
            child.material.uniforms.uMousePosition.value = MOUSE_POSITION;
            child.material.needsUpdate = true;
        }
    });
}


function render() {
    CAMERA.lookAt(SCENE.position);
    COMPOSER.render(SCENE, CAMERA);
}


function createObjects() {
    const loader = new THREE.TextureLoader();

    const urls = [
        'https://picsum.photos/id/690/1920/1080.jpg',
        'https://picsum.photos/id/692/1920/1080.jpg'
    ];

    const promises = [];

    urls.forEach((url) => {
        promises.push(new Promise((resolve, reject) => {
            loader.load(url, (texture) => {
                texture.magFilter = THREE.NearestFilter;
                texture.minFilter = THREE.NearestFilter;
                resolve(texture);
            });
        }));
    });

    Promise.all(promises).then((loadedTextures) => {
        const geometry = new THREE.PlaneGeometry(2, 2, 100, 100);

        const shaderMaterial = new THREE.ShaderMaterial({
            uniforms: {
                uImage1: {
                    type: 't',
                    value: loadedTextures[0]
                },
                uImage2: {
                    type: 't',
                    value: loadedTextures[1]
                },
                uImageSize: {
                    type: 'v2',
                    value: new THREE.Vector2(1920, 1080)
                },
                uWindowSize: {
                    type: 'v2',
                    value: WINDOW_SIZE
                },
                uMousePosition: {
                    type: 'v2',
                    value: MOUSE_POSITION
                },
                uTime: {
                    type: 'f',
                    value: TIMES.GLOBAL
                },
                uAnimationTime: {
                    type: 'f',
                    value: TIMES.ANIMATION
                }
            },
            transparent: true,
            side: THREE.DoubleSide,
            vertexShader:   document.getElementById('plane-vertex-shader').textContent,
            fragmentShader: document.getElementById('plane-fragment-shader').textContent
        });

        const plane = new THREE.Mesh(geometry, shaderMaterial);

        SCENE.add(plane); 
    });
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.