<!--

UPD: This example is a part of a series of examples of shaders - https://codepen.io/collection/pgLwJp , and some parts of the code are inherited from the previous examples and they are not necessary for this particular effect.

-->


<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.