body {
  margin: 0;
  padding: 0;
}
// Port from Shadertoy to THREE.js: https://www.shadertoy.com/view/4sG3WV

const VERTEX_SHADER = `
    varying vec2 vUv;
    
    void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
`

const BUFFER_A_FRAG = `
    uniform vec4 iMouse;
    uniform sampler2D iChannel0;
    uniform sampler2D iChannel1;
    uniform vec2 iResolution;
    uniform float iFrame;
    varying vec2 vUv;
    
    #define mousedata(a,b) texture2D( iChannel1, (0.5+vec2(a,b)) / iResolution.xy, -0.0 )
    #define backbuffer(uv) texture2D( iChannel0, uv ).xy
    
    float lineDist(vec2 p, vec2 start, vec2 end, float width) {
        vec2 dir = start - end;
        float lngth = length(dir);
        dir /= lngth;
        vec2 proj = max(0.0, min(lngth, dot((start - p), dir))) * dir;
        return length( (start - p) - proj ) - (width / 2.0);
    }
    
    void main() {
        vec2 uv = vUv;
        vec2 col = uv;
        if (iFrame > 2.) {
            col = texture2D(iChannel0,uv).xy;
            vec2 mouse = iMouse.xy/iResolution.xy;
            vec2 p_mouse = mousedata(2.,0.).xy;
            if (mousedata(4.,0.).x > 0.) {
                col = backbuffer(uv+((p_mouse-mouse)*clamp(1.-(lineDist(uv,mouse,p_mouse,0.)*20.),0.,1.)*.7));
            }
        }
        gl_FragColor = vec4(col,0.0,1.0);
    }

`

const BUFFER_B_FRAG = `
    uniform vec4 iMouse;
    uniform sampler2D iChannel0;
    uniform vec3 iResolution;
    varying vec2 vUv;
    
    bool pixelAt(vec2 coord, float a, float b) {
        return (floor(coord.x) == a && floor(coord.y) == b);
    }
    
    vec4 backbuffer(float a,float b) {
      return texture2D( iChannel0, (0.5+vec2(a,b)) / iResolution.xy, -100.0 );
    }
    
    void main( ) {
    
        vec2 uv = vUv;// / iResolution.xy;
        vec4 color = texture2D(iChannel0,uv);
    
        if (pixelAt(gl_FragCoord.xy,0.,0.)) { //Surface position
            gl_FragColor = vec4(backbuffer(0.,0.).rg+(backbuffer(4.,0.).r*(backbuffer(2.,0.).rg-backbuffer(1.,0.).rg)),0.,1.);
        } else if (pixelAt(gl_FragCoord.xy,1.,0.)) { //New mouse position
            gl_FragColor = vec4(iMouse.xy/iResolution.xy,0.,1.);
        } else if (pixelAt(gl_FragCoord.xy,2.,0.)) { //Old mouse position
            gl_FragColor = vec4(backbuffer(1.,0.).rg,0.,1.);
        } else if (pixelAt(gl_FragCoord.xy,3.,0.)) { //New mouse holded
            gl_FragColor = vec4(clamp(iMouse.z,0.,1.),0.,0.,1.);
        } else if (pixelAt(gl_FragCoord.xy,4.,0.)) { //Old mouse holded
            gl_FragColor = vec4(backbuffer(3.,0.).r,0.,0.,1.);
        } else {
            gl_FragColor = vec4(0.,0.,0.,1.);
        }
    
    }
`

const BUFFER_FINAL_FRAG = `
    uniform sampler2D iChannel0;
    uniform sampler2D iChannel1;
    varying vec2 vUv;
    
    void main() {
        vec2 uv = vUv;
        vec2 a = texture2D(iChannel1,uv).xy;
        gl_FragColor = vec4(texture2D(iChannel0,a).rgb,1.0);
    }
`

class App {

    private width = 1024
    private height = 512

    private renderer = new THREE.WebGLRenderer()
    private loader = new THREE.TextureLoader()
    private mousePosition = new THREE.Vector4()
    private orthoCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
    private counter = 0

    constructor() {

        this.renderer.setSize(this.width, this.height)
        document.body.appendChild(this.renderer.domElement)

        this.renderer.domElement.addEventListener('mousedown', () => {
            this.mousePosition.setZ(1)
            this.counter = 0
        })

        this.renderer.domElement.addEventListener('mouseup', () => {
            this.mousePosition.setZ(0)
        })

        this.renderer.domElement.addEventListener('mousemove', event => {
            this.mousePosition.setX(event.clientX)
            this.mousePosition.setY(this.height - event.clientY)
        })

    }

    private targetA = new BufferManager(this.renderer, { width: this.width, height: this.height })
    private targetB = new BufferManager(this.renderer, { width: this.width, height: this.height })
    private targetC = new BufferManager(this.renderer, { width: this.width, height: this.height })

    private bufferA: BufferShader
    private bufferB: BufferShader
    private bufferImage: BufferShader

    public start() {

        const resolution = new THREE.Vector3(this.width, this.height, window.devicePixelRatio)
        const channel0 = this.loader.load('https://res.cloudinary.com/di4jisedp/image/upload/v1523722553/wallpaper.jpg')
        this.loader.setCrossOrigin('')

        this.bufferA = new BufferShader(BUFFER_A_FRAG, {
            iFrame: { value: 0 },
            iResolution: { value: resolution },
            iMouse: { value: this.mousePosition },
            iChannel0: { value: null },
            iChannel1: { value: null }
        })

        this.bufferB = new BufferShader(BUFFER_B_FRAG, {
            iFrame: { value: 0 },
            iResolution: { value: resolution },
            iMouse: { value: this.mousePosition },
            iChannel0: { value: null }
        })

        this.bufferImage = new BufferShader(BUFFER_FINAL_FRAG, {
            iResolution: { value: resolution },
            iMouse: { value: this.mousePosition },
            iChannel0: { value: channel0 },
            iChannel1: { value: null }
        })

        this.animate()

    }

    private animate() {
        requestAnimationFrame(() => {

            this.bufferA.uniforms[ 'iFrame' ].value = this.counter++

            this.bufferA.uniforms[ 'iChannel0' ].value = this.targetA.readBuffer.texture
            this.bufferA.uniforms[ 'iChannel1' ].value = this.targetB.readBuffer.texture
            this.targetA.render(this.bufferA.scene, this.orthoCamera)

            this.bufferB.uniforms[ 'iChannel0' ].value = this.targetB.readBuffer.texture
            this.targetB.render(this.bufferB.scene, this.orthoCamera)

            this.bufferImage.uniforms[ 'iChannel1' ].value = this.targetA.readBuffer.texture
            this.targetC.render(this.bufferImage.scene, this.orthoCamera, true)

            this.animate()

        })

    }

}

class BufferShader {

    public material: THREE.ShaderMaterial
    public scene: THREE.Scene

    constructor(fragmentShader: string, public uniforms: {} = {}) {
        this.material = new THREE.ShaderMaterial({
            fragmentShader,
            vertexShader: VERTEX_SHADER,
            uniforms
        })
        this.scene = new THREE.Scene()
        this.scene.add(
            new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), this.material)
        )
    }

}

class BufferManager {

    public readBuffer: THREE.WebGLRenderTarget
    public writeBuffer: THREE.WebGLRenderTarget

    constructor(private renderer: THREE.WebGLRenderer, { width, height }) {

        this.readBuffer = new THREE.WebGLRenderTarget(width, height, {
            minFilter: THREE.LinearFilter,
            magFilter: THREE.LinearFilter,
            format: THREE.RGBAFormat,
            type: THREE.FloatType,
            stencilBuffer: false
        })

        this.writeBuffer = this.readBuffer.clone()

    }

    public swap() {
        const temp = this.readBuffer
        this.readBuffer = this.writeBuffer
        this.writeBuffer = temp
    }

    public render(scene: THREE.Scene, camera: THREE.Camera, toScreen: boolean = false) {
        if (toScreen) {
            this.renderer.render(scene, camera)
        } else {
            this.renderer.render(scene, camera, this.writeBuffer, true)
        }
        this.swap()
    }

}

document.addEventListener('DOMContentLoaded', () => {
    (new App()).start()
})
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/91/three.min.js