<script id="vertexShader" type="x-shader/x-vertex">
varying vec2 vUv;
void main( void ) {
  vUv = uv;
	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
  varying vec2 vUv;
  uniform vec3 uColor;
  uniform vec3 fogColor;
  uniform float fogNear;
  uniform float fogFar;
  float box(vec2 _st, vec2 _size){
    _size = vec2(0.5)-_size*0.5;
    vec2 uv = smoothstep(_size,_size+vec2(1e-4),_st);
    uv *= smoothstep(_size,_size+vec2(1e-4),vec2(1.0)-_st);
    return uv.x*uv.y;
  }
  void main() {
    float count = 30.;
    vec3 color = uColor;
    vec2 st = vUv;
    st.y *= 3.;
    float cell = box(fract(st * 16.0), vec2(0.9625));

    gl_FragColor = vec4(vec3(cell) * uColor, 1.);
    #ifdef USE_FOG
      #ifdef USE_LOGDEPTHBUF_EXT
        float depth = gl_FragDepthEXT / gl_FragCoord.w;
      #else
        float depth = gl_FragCoord.z / gl_FragCoord.w;
      #endif
      float fogFactor = smoothstep( fogNear, fogFar, depth );
      gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );
    #endif
  }
</script>
<canvas id="scene"></canvas>

<svg
     id="cursor"
     viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"
 >
  <circle cx="15" cy="15" r="13" class="holder"/>
  <circle cx="15" cy="15" r="13" class="focus"/>
</svg>
@import url('https://fonts.googleapis.com/css?family=Roboto+Condensed')
html,
body
  padding: 0
  margin: 0
  height: 100%
  overflow: hidden
  font-family: 'Roboto Condensed', sans-serif
  cursor: none
  &:hover
    #cursor
      opacity: 1
    
    
canvas
  display: block
  
$size: 30px
#cursor
  position: absolute
  top: 0%
  left: 0%
  margin-left: - $size * .5
  margin-top: - $size * .5
  width: $size
  height: $size
  opacity: 1
  will-change: transform
  pointer-events: none
  opacity: 0
  transition: opacity .3s
  will-change: transform, opacity
  &.visible
    opacity: 1
  circle
    fill: none
    stroke: rgba(#f9e1d5,.95)
    stroke-width: 3
    &.focus
      stroke: rgba(#1c1c1c, .625)
      stroke-dasharray: 82
      stroke-dashoffset: -82
      will-change: stroke-dashoffset
View Compiled
const API = {
  exposure: 1.208,
  bloomStrength: 1,
  bloomThreshold: 0.5,
  bloomRadius: 0.24
};
var startTime = Date.now();
var worldWidth = 256,
  worldDepth = 256,
  worldHalfWidth = worldWidth / 2,
  worldHalfDepth = worldDepth / 2;
var matrix = new THREE.Matrix4();
var quaternion = new THREE.Quaternion();
const cursor = document.body.querySelector("#cursor")
const thumb = document.body.querySelector("#cursor .focus")
class App {
  constructor({ container, caption }) {
    this.caption = caption;
    this.zoom = false
    this.focusOffset = 0
    this.sizes = {
      width: document.body.offsetWidth,
      height: document.body.offsetHeight,
      halfWidth: document.body.offsetWidth * 0.5,
      halfHeight: document.body.offsetHeight * 0.5
    };
    this.currentName = null;
    this.mouse = new THREE.Vector2(0.0625, 0.0625);
    this.target = new THREE.Vector2(this.mouse.x, this.mouse.y);
    this.renderer = new THREE.WebGLRenderer({
      canvas: container,
      alpha: false,
      stencil: false,
      // depth: false,
      powerPreference: "high-performance",
      antialias: true
    });
    this.renderer.shadowMap.enabled = true;
    this.scene = new THREE.Scene();
    {

      const near = 1;
      const far = 160;
      const color = 0xf1f5f9;
      this.scene.fog = new THREE.Fog(color, near, far);
      this.scene.background = new THREE.Color(color);
    }

    this.camera = new THREE.PerspectiveCamera(
      125,
      this.sizes.width / this.sizes.hegiht,
      0.1,
      1000
    );
    this.camera.focus = 20
    this.camera.rotation.order = "YXZ";
    this.wagon = new THREE.Object3D();
    this.camera.position.set(0, 0, 0);
    this.wagon.add(this.camera);
    this.camera.lookAt(0, 1, -.4);
    this.scene.add(this.wagon);
    // this.setSphereMap();
    // this.setGround();
    // this.setLight();
    this.setBuildings();
    this.setMain();
    this.render();
    window.addEventListener("resize", () => this.handleResize(), {
      passive: true
    });
    window.addEventListener("mousemove", e => this.handleMousemove(e), false);
    window.addEventListener("touchmove", e => this.handleMousemove(e), false);
  }
  setBuildings() {
    const buildings = [
      {
        size: {x: 50, y: 160, z: 50},
        position: new THREE.Vector3(-50, 0, -50),
        rotation: new THREE.Vector3(0, Math.PI * 0.215, 0),
      },
      {
        size: {x: 50, y: 160, z: 50},
        position: new THREE.Vector3(-50, 0, 50),
        rotation: new THREE.Vector3(0, Math.PI * 0.215, 0),
      },
      {
        size: {x: 70, y: 160, z: 70},
        position: new THREE.Vector3(60, 0, 65),
        rotation: new THREE.Vector3(0, Math.PI * 0.215, 0),
      },
      {
        size: {x: 60, y: 120, z: 80},
        position: new THREE.Vector3(120, 0, 0),
        rotation: new THREE.Vector3(0, Math.PI * 0.215, 0),
      },
      {
        size: {x: 50, y: 160, z: 50},
        position: new THREE.Vector3(135, 0, -80),
        rotation: new THREE.Vector3(0, Math.PI * 0.215, 0),
      },
      {
        size: {x: 40, y: 160, z: 40},
        position: new THREE.Vector3(100, 0, -190),
        rotation: new THREE.Vector3(0, Math.PI * 0.215, 0),
      },
      {
        size: {x: 40, y: 160, z: 40},
        position: new THREE.Vector3(20, 0, -200),
        rotation: new THREE.Vector3(0, Math.PI * 0.215, 0),
      }
    ]
    
    var uniforms = {
			fogColor:    { type: "c", value: this.scene.fog.color },
    	fogNear:     { type: "f", value: this.scene.fog.near },
    	fogFar:      { type: "f", value: this.scene.fog.far },
      uColor:      { type: "v3", value: new THREE.Color(0x1d1d1d) },
    };
    var vertexShader = document.getElementById('vertexShader').text;
    var fragmentShader = document.getElementById('fragmentShader').text;
    var lematerial = new THREE.ShaderMaterial(
      {
        uniforms : uniforms,
        vertexShader : vertexShader,
        fragmentShader : fragmentShader,
        fog: true
      });
    buildings.forEach(({size, position, rotation}) => {
      const {x, y, z} = position
      var geometry = new THREE.BoxBufferGeometry( size.x, size.y, size.z );
      geometry.rotateY(rotation.y)
      geometry.translate(x, size.y * 0.5, z)
      var material = new THREE.MeshBasicMaterial( {color: 0x1d1d1d} );
      var cube = new THREE.Mesh( geometry, lematerial.clone() );
      this.scene.add( cube );
    })
  }
  handleMousemove(e) {
    e.preventDefault();
    
    const {
      pageX: x,
      pageY: y
    } = event.touches && event.touches[0] ? event.touches[0] : event
    this.mouse.x = x / this.sizes.width * 2 - 1;
    this.mouse.y = -(y / this.sizes.height) * 2 + 1;
    cursor.style.transform = `translateX(${x}px) translateY(${y}px)`
    this.target.x = 0.125 * this.mouse.x;
    this.target.y = 0.09765625 * this.mouse.y;
  }
  getScreenXY(position) {
    const { halfWidth, halfHeight } = this.sizes;
    var vector = position.clone();
    this.camera.updateProjectionMatrix();
    vector.project(this.camera);
    return vector;
  }
  render() {
    requestAnimationFrame(() => this.render());

    this.focusOffset = Math.max(Math.min(this.focusOffset + (this.zoom ? 0.005 : -0.025), 1), 0)
    this.camera.fov = 100 + this.focusOffset * 30;
    this.wagon.rotation.y += (this.target.x - this.wagon.rotation.y) * 0.04;
    thumb.style.strokeDashoffset = 82 - this.focusOffset * -82
    // this.camera.position.y = this.focusOffset * 12.5;
    this.camera.updateProjectionMatrix();
    this.renderer.render(this.scene, this.camera);
  }
  setMain() {
    this.renderer.setSize(this.sizes.width, this.sizes.height);
    this.camera.aspect = this.sizes.width / this.sizes.height;
    this.camera.updateProjectionMatrix();
  }
  handleResize() {
    this.sizes.width = document.body.offsetWidth;
    this.sizes.height = document.body.offsetHeight;
    this.sizes.halfWidth = this.sizes.width * 0.5;
    this.sizes.halfHeight = this.sizes.height * 0.5;
    this.setMain();
  }
}
const app = new App({
  container: document.body.querySelector("#scene")
});
window.addEventListener('mousedown', function(e) {
  e.preventDefault()
  app.zoom = true
  cursor.classList.add('visible')
}, false)
window.addEventListener('touchstart', function(e) {
  e.preventDefault()
  app.zoom = true
  cursor.classList.add('visible')
}, false)
window.addEventListener('mouseup', function(e) {
  e.preventDefault()
  app.zoom = false
  cursor.classList.remove('visible')
}, false)
window.addEventListener('mouseout', function(e) {
  e.preventDefault()
  app.zoom = false
  cursor.classList.remove('visible')
}, false)
window.addEventListener('touchend', function(e) {
  e.preventDefault()
  app.zoom = false
  cursor.classList.remove('visible')
}, false)
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

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