<div
  id="js-app"
  class="c-container"
></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.7.2/prop-types.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/101/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
<script>(function(){var script=document.createElement('script');script.onload=function(){var stats=new Stats();document.body.appendChild(stats.dom);requestAnimationFrame(function loop(){stats.update();requestAnimationFrame(loop)});};script.src='//rawgit.com/mrdoob/stats.js/master/build/stats.min.js';document.head.appendChild(script);})()</script>

<script id="hsv2rgb" type="shader/fragment">
vec3 hsv2rgb (vec3 c) {
  vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
</script>
<script id="noise2d" type="shader/fragment">
//
// Description : Array and textureless GLSL 2D/3D/4D simplex 
//               noise functions.
//      Author : Ian McEwan, Ashima Arts.
//  Maintainer : stegu
//     Lastmod : 20110822 (ijm)
//     License : Copyright (C) 2011 Ashima Arts. All rights reserved.
//               Distributed under the MIT License. See LICENSE file.
//               https://github.com/ashima/webgl-noise
//               https://github.com/stegu/webgl-noise
// 

vec3 mod289(vec3 x) {
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec4 mod289(vec4 x) {
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec4 permute(vec4 x) {
     return mod289(((x*34.0)+1.0)*x);
}

vec4 taylorInvSqrt(vec4 r)
{
  return 1.79284291400159 - 0.85373472095314 * r;
}

float snoise(vec3 v)
  { 
  const vec2  C = vec2(1.0/6.0, 1.0/3.0) ;
  const vec4  D = vec4(0.0, 0.5, 1.0, 2.0);

// First corner
  vec3 i  = floor(v + dot(v, C.yyy) );
  vec3 x0 =   v - i + dot(i, C.xxx) ;

// Other corners
  vec3 g = step(x0.yzx, x0.xyz);
  vec3 l = 1.0 - g;
  vec3 i1 = min( g.xyz, l.zxy );
  vec3 i2 = max( g.xyz, l.zxy );

  //   x0 = x0 - 0.0 + 0.0 * C.xxx;
  //   x1 = x0 - i1  + 1.0 * C.xxx;
  //   x2 = x0 - i2  + 2.0 * C.xxx;
  //   x3 = x0 - 1.0 + 3.0 * C.xxx;
  vec3 x1 = x0 - i1 + C.xxx;
  vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
  vec3 x3 = x0 - D.yyy;      // -1.0+3.0*C.x = -0.5 = -D.y

// Permutations
  i = mod289(i); 
  vec4 p = permute( permute( permute( 
             i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
           + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) 
           + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));

// Gradients: 7x7 points over a square, mapped onto an octahedron.
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
  float n_ = 0.142857142857; // 1.0/7.0
  vec3  ns = n_ * D.wyz - D.xzx;

  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);  //  mod(p,7*7)

  vec4 x_ = floor(j * ns.z);
  vec4 y_ = floor(j - 7.0 * x_ );    // mod(j,N)

  vec4 x = x_ *ns.x + ns.yyyy;
  vec4 y = y_ *ns.x + ns.yyyy;
  vec4 h = 1.0 - abs(x) - abs(y);

  vec4 b0 = vec4( x.xy, y.xy );
  vec4 b1 = vec4( x.zw, y.zw );

  //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
  //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
  vec4 s0 = floor(b0)*2.0 + 1.0;
  vec4 s1 = floor(b1)*2.0 + 1.0;
  vec4 sh = -step(h, vec4(0.0));

  vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
  vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;

  vec3 p0 = vec3(a0.xy,h.x);
  vec3 p1 = vec3(a0.zw,h.y);
  vec3 p2 = vec3(a1.xy,h.z);
  vec3 p3 = vec3(a1.zw,h.w);

//Normalise gradients
  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
  p0 *= norm.x;
  p1 *= norm.y;
  p2 *= norm.z;
  p3 *= norm.w;

// Mix final noise value
  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
  m = m * m;
  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), 
                                dot(p2,x2), dot(p3,x3) ) );
  }
</script>
body {
  margin: 0;
  padding: 0;
  background-color: #000;
}

canvas {
  display: block;
  margin: 0 auto;
}

.c-container {
  height: 100vh;
}
const images = {
  'In the back': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/photo-41.JPG',
  'Quatro doggo': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/photo-26.JPG',
  'Puppy puddle': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/hot%20rod%20vic%20001.jpg',
  'In a field': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/photo-1.JPG',
  'Cat caution': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/img_2.jpg'
}

setTimeout(() => {
  const props = {
    imageSrc: images['In the back'],
    tileSize: 20,
    scopeSize: 0.4,
    showScope: true,
    filterEnabled: true
  }
  const render = () => {
    ReactDOM.render((
      <Shader
          {...props}
          {...{
            vertexShader,
            fragmentShader
          }}
      />
    ), document.getElementById('js-app'))
  }
  setupGui(props, render)
  render()
}, 0)

const setupGui = (props, render) => {
  const gui = new dat.GUI()
  const imageSrcField = gui.add(props, 'imageSrc', images)
  const tileSizeField = gui.add(props, 'tileSize', 1, 40).step(1)
  const scopeSizeField = gui.add(props, 'scopeSize', 0.1, 2.0).step(0.05)
  const showScopeField = gui.add(props, 'showScope')
  const filterEnabledField = gui.add(props, 'filterEnabled')
  imageSrcField.onChange(() => render())
  tileSizeField.onChange(() => render())
  scopeSizeField.onChange(() => render())
  showScopeField.onChange(() => render())
  filterEnabledField.onChange(() => render())
}

const vertexShader = `

void main () {
  gl_Position = vec4(position, 1.0);
}

`

const fragmentShader = `
${document.getElementById('noise2d').text}
${document.getElementById('hsv2rgb').text}

uniform vec2 uResolution;
uniform float uTime;
uniform vec2 uMouse;
uniform sampler2D uImage; 
uniform float uGridWidth;
uniform float uMagRadius;
uniform bool uShowScope;
uniform bool uFilterEnabled;

const float EPSILON = 0.01;

vec3 cubeRound (vec3 cube) {
  float rx = floor(cube.x + 0.5);
  float ry = floor(cube.y + 0.5);
  float rz = floor(cube.z + 0.5);
  float xd = abs(rx - cube.x);
  float yd = abs(ry - cube.y);
  float zd = abs(rz - cube.z);
  if (xd > yd && xd > zd) {
    rx = -ry - rz;
  } else if (yd > zd) {
    ry = -rx - rz;
  } else {
    rz = -rx - ry;
  }
  return vec3(rx, ry, rz);
}

vec2 cubeToAxial (vec3 cube) {
  return cube.xz;
}

vec3 axialToCube (vec2 hex) {
  float x = hex.x;
  float z = hex.y;
  float y = -x - z;
  return vec3(x, y, z);
}

vec2 hexRound (vec2 hex) {
  return cubeToAxial(cubeRound(axialToCube(hex)));
}

vec2 pointToAxial (vec2 point) {
  float q = (sqrt(3.0) / 3.0 * point.x - 1.0 / 3.0 * point.y);
  float r = (                            2.0 / 3.0 * point.y);
  return vec2(q, r);
}

vec2 axialToPoint (vec2 axial) {
  float y = axial.y / (2.0 / 3.0);
  float x = (((1.0 / 3.0) * y) + axial.x) / (sqrt(3.0) / 3.0);
  return (vec2(x, y)) / 2.0 - 0.5;
}

vec2 pointToHex (vec2 point) {
  vec2 qr = pointToAxial(point);
  return hexRound(vec3(qr.x, qr.y, 5625463739.0).xy);
}

float circle (vec2 pos, float r) {
	return 1.0 - smoothstep(r - (r * EPSILON), r + (r * EPSILON), dot(pos, pos) * 4.0);
}

vec2 convertPoint (vec2 point) {
  vec2 pt = point.xy / uResolution.xy;
  pt = (pt * 2.0) - 1.0;
  return pt;
}

vec2 getMagPos () {
  return convertPoint(uMouse);
}

float getMag (vec2 pt, vec2 pos, float radius) {
  return circle(((pt - pos)), radius);
}

void main () {
  vec2 pt = convertPoint(gl_FragCoord.xy);
  vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);
  vec2 aspectPt = aspect * pt;
  vec2 magPos = getMagPos() * aspect;
  float magnification = getMag(aspectPt, magPos, uMagRadius);
  vec2 sample = pointToAxial(pt);
  if ((!uShowScope || magnification == 0.0) && uFilterEnabled) {
    sample = pointToHex(pt * uGridWidth) / uGridWidth;
  }
  vec3 color = texture2D(uImage, axialToPoint(sample)).rgb;
  float outline = (getMag(aspectPt, magPos, uMagRadius) - getMag(aspectPt, magPos, uMagRadius * 0.9)) / 4.0;
  if (!uShowScope) {
    outline = 0.0;
  }
  gl_FragColor = vec4(color, 1.0) + outline;
}

`

class Shader extends React.Component {
  static propTypes = {
    imageSrc: PropTypes.string.isRequired,
    tileSize: PropTypes.number.isRequired,
    fragmentShader: PropTypes.string.isRequired,
    vertexShader: PropTypes.string.isRequired,
    scopeSize: PropTypes.number.isRequired,
    showScope: PropTypes.bool.isRequired,
    filterEnabled: PropTypes.bool.isRequired
  }

  shouldComponentUpdate () {
    return false
  }

  async componentDidMount () {
    const camera = new THREE.Camera()
    camera.position.z = 1
    const scene = new THREE.Scene()
    const geometry = new THREE.PlaneBufferGeometry(2, 2)
    this.loader = new THREE.TextureLoader()
    const image = await (new Promise(resolve => this.loader.load(this.props.imageSrc, resolve)))
    image.wrapS = THREE.RepeatWrapping
    image.wrapT = THREE.RepeatWrapping
    this.uniforms = {
      uTime: {
        type: "f",
        value: 1
      },
      uResolution: {
        type: "v2",
        value: new THREE.Vector2()
      },
      uMouse: {
        type: "v2",
        value: new THREE.Vector2()
      },
      uImage: {
        value: image
      },
      uGridWidth: {
        value: 0
      },
      uMagRadius: {
        value: this.props.scopeSize
      },
      uFilterEnabled: {
        value: this.props.filterEnabled
      },
      uShowScope: {
        value: this.props.showScope
      }
    }
    const material = new THREE.ShaderMaterial({
      uniforms: this.uniforms,
      vertexShader: this.props.vertexShader,
      fragmentShader: this.props.fragmentShader
    })
    const mesh = new THREE.Mesh(geometry, material)
    scene.add(mesh)
    const renderer = new THREE.WebGLRenderer({
      antialias: false,
      alpha: false,
      canvas: this.canvas
    })
    // renderer.setPixelRatio(window.devicePixelRatio)
    this.handleWindowResize = this.onWindowResize(camera, renderer)
    this.handleWindowResize(this.props.tileSize)
    window.addEventListener('resize', () => this.handleWindowResize(this.props.tileSize), false)
    const handleCursor = (e) => {
      this.uniforms.uMouse.value.x = e.clientX - this.canvas.offsetLeft // * window.devicePixelRatio
      this.uniforms.uMouse.value.y = this.canvas.height - (e.clientY - this.canvas.offsetTop) // * window.devicePixelRatio
    }
    this.canvas.onmousemove = e => handleCursor(e)
    this.canvas.ontouchstart = this.canvas.ontouchmove = (e) => {
      handleCursor(e.targetTouches[0])
      e.preventDefault()
    }
    this.uniforms.uMouse.value = new THREE.Vector2(this.canvas.width / 2, this.canvas.height / 2)
    this.animate(renderer, scene, camera)
  }

  async componentWillReceiveProps (nextProps) {
    if (nextProps.imageSrc !== this.props.imageSrc) {
      const image = await (new Promise(resolve => this.loader.load(nextProps.imageSrc, resolve)))
      image.wrapS = THREE.RepeatWrapping
      image.wrapT = THREE.RepeatWrapping
      this.uniforms.uImage.value = image
      this.handleWindowResize(nextProps.tileSize)
    } else if (nextProps.tileSize !== this.props.tileSize) {
      this.handleWindowResize(nextProps.tileSize)
    }
    if (nextProps.scopeSize !== this.props.scopeSize) {
      this.uniforms.uMagRadius.value = nextProps.scopeSize
    }
    if (nextProps.showScope !== this.props.showScope) {
      this.uniforms.uShowScope.value = nextProps.showScope
    }
    if (nextProps.filterEnabled !== this.props.filterEnabled) {
      this.uniforms.uFilterEnabled.value = nextProps.filterEnabled
    }
  }

  animate (renderer, scene, camera) {
    requestAnimationFrame(() => {
      this.animate(renderer, scene, camera, this.uniforms)
    })
    this.uniforms.uTime.value += 0.05
    renderer.render(scene, camera)
  }

  onWindowResize (camera, renderer) {
    return (tileSize) => {
      // const size = Math.min(window.innerWidth, window.innerHeight)
      const { image } = this.uniforms.uImage.value
      const aspect = image.width / image.height
      const width = renderer.domElement.parentElement.offsetHeight * aspect
      const height = renderer.domElement.parentElement.offsetHeight
      renderer.setSize(width, height)
      this.uniforms.uResolution.value.x = renderer.domElement.width
      this.uniforms.uResolution.value.y = renderer.domElement.height
      this.uniforms.uGridWidth.value = width / tileSize
    }
  }

  render () {
    return (
      <canvas ref={c => this.canvas = c} />
    )
  }
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.