<div class="headerContainer">
  <div id="container">
    <button onClick="init()">New Mesh</button>
    <button class="lights-button" onClick="toggleLight()">Toggle
      Global Lights</button>

    <div class="slidecontainer">
      <span>Point Variance</span>
      <input type="range" min="1" max="10" value="5" id="sliderRange">
    </div>
  </div>
</div>
body {
  margin: 0;
  background-color: #fff;
  font-family: "Oswald", sans-serif;
}

button {
  position: absolute;
  left: 10px;
  top: 40px;
  padding: 0.3rem;
  font-family: "Oswald", sans-serif;
  font-size: 1rem;
}

.lights-button {
  top: 80px;
}

.headerContainer {
  height: 100vh;
  min-height: 700px;
  width: 100%;
  overflow: hidden;
  position: relative;
}

.invitationOverlay {
  position: absolute;
  padding: 24px;
  //background-color: rgba(255, 255, 255, 0.5);
  left: 65%;
  bottom: 30%;
  color: #fff;
}

h1 {
  font-size: 85px;
  line-height: 87px;
  font-weight: 800;
  margin: 0 0 20px 0;
  text-shadow: 0 0 5px rgb(0 0 0 / 50%);
}

h2 {
  font-size: 50px;
  line-height: 52px;
  font-weight: 400;
  margin: 0 0 40px 0;
  text-shadow: 0 0 5px rgb(0 0 0 / 50%);
}

p {
  font-size: 2rem;
  color: rgba(255, 255, 255, 0.7);
  margin: 0;
}

.statsBarOuter {
  width: 100%;
  position: absolute;
  bottom: 0;
  left: 0;
  display: flex;
  background: rgba(12, 22, 45, 0.3);
}

.statsBarInner {
  padding: 20px;
  display: flex;
  justify-content: space-evenly;
  width: 100%;
}

.statItem {
  text-shadow: 0 0 5px rgb(0 0 0 / 50%);
}

.light {
  font-weight: 300;
  color: #a675b3;
  font-size: 1.5rem;
}

.button {
  display: inline;
}

.noselect {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.button {
  box-shadow: inset 0 1px 1px rgb(111 55 125 / 80%),
    inset 0 -1px 0px rgb(63 59 113 / 20%), 0 9px 16px 0 rgb(0 0 0 / 30%),
    0 4px 3px 0 rgb(0 0 0 / 30%), 0 0 0 1px #150a1e;
  background-image: linear-gradient(#3b2751, #271739);
  text-shadow: 0 0 21px rgb(223 206 228 / 50%), 0 -1px 0 #311d47;
  animation: bounceInDown 900ms 200ms ease-in-out both;
  width: 150px;
  height: 40px;
  text-decoration: none;
  outline-width: 0px;
  z-index: 990;
  color: #a675b3;
  text-align: center;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border: none;
  padding: 10px 40px;
  font-size: 1rem;
}

.button.active,
.button:active {
  box-shadow: 0 9px 16px 0 rgba(0, 0, 0, 0.1), 0 0 0 1px #170c22,
    0 2px 1px 0 rgba(121, 65, 135, 0.5), inset 0 0 4px 3px rgba(15, 8, 22, 0.2);
  background-image: linear-gradient(#1f132e, #311d47);
  text-shadow: 0 0 21px rgba(223, 206, 228, 0.5),
    0 0 10px rgba(223, 206, 228, 0.4), 0 0 2px #2a153c;
  color: #e4e3ce;
}

.blue {
  box-shadow: inset 0 1px 1px rgb(95 96 151 / 80%),
    inset 0 -1px 0px rgb(63 59 113 / 20%), 0 9px 16px 0 rgb(0 0 0 / 30%),
    0 4px 3px 0 rgb(0 0 0 / 30%), 0 0 0 1px #5f5f97;
  background-image: linear-gradient(#272951, #171c39);
  text-shadow: 0 0 21px rgb(223 206 228 / 50%), 0 -1px 0 #1d2047;
  color: #9293b7;
}

.blue.active,
.blue:active {
  box-shadow: 0 9px 16px 0 rgba(0, 0, 0, 0.1), 0 0 0 1px #0c0d22,
    0 2px 1px 0 rgba(160, 160, 195, 0.5), inset 0 0 4px 3px rgba(15, 8, 22, 0.2);
  background-image: linear-gradient(#13132e, #1e1d47);
  text-shadow: 0 0 21px rgba(223, 206, 228, 0.5),
    0 0 10px rgba(223, 206, 228, 0.4), 0 0 2px #16153c;
  color: #e4e3ce;
}

.slidecontainer {
  position: absolute;
  left: 10px;
  top: 10px;
  color: #fff;
}
let container, stats;

let camera, scene, renderer;

let windowHalfX = window.innerWidth / 2;
let windowHalfY = window.innerHeight / 2;

var mouseIsDown = true;

//Mouse variables for mouse light positioning
var mouse = {
  x: 0,
  y: 0
};

var mouseLightPos = {
  x: 0,
  y: 0
};

const mouseLight = new THREE.PointLight(0xcccccc, 2, 30);
var mouseLightHeight = 20;

init();
animate();

var headerContainer = document.querySelector(".headerContainer");

var light;

function init() {
  var headerCanvas = document.getElementById("headerCanvas");
  if (headerCanvas != null) {
    headerCanvas.remove();
  }
  // SETUP
  // ======================
  headerContainer = document.querySelector(".headerContainer");
  camera = new THREE.PerspectiveCamera(
    5,
    headerContainer.offsetWidth / headerContainer.offsetHeight,
    1,
    10000
  );
  camera.position.z = 1800 - camera.aspect * 200;

  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x000000);
  // LIGHTS
  // ======================

  light = new THREE.DirectionalLight(0xcccccc);
  light.position.set(0, 0, 1);
  light.intensity = 0;
  scene.add(light);

  mouseLight.position.set(mouseLightPos.x, mouseLightPos.y, mouseLightHeight);
  scene.add(mouseLight);

  // point light helper
  const sphereSize = 1;
  const pointLightHelper = new THREE.PointLightHelper(mouseLight, sphereSize);
  //scene.add(pointLightHelper);

  // TEMPORARY CANVAS
  // this is the canvas for mapping the gradient
  // ======================
  const tempCanvas = document.createElement("canvas");
  var ctx = tempCanvas.getContext("2d");

  const width = headerContainer.offsetWidth;
  const height = headerContainer.offsetWidth;

  const halfWidth = Math.floor(width * 0.5);
  const halfHeight = Math.floor(height * 0.5);

  const gradientRange = Math.sqrt(width ** 2 + height ** 2);

  // Size the canvas to match viewport
  tempCanvas.width = width;
  tempCanvas.height = height;

  // Create a gradient for the fill
  var grd = ctx.createRadialGradient(
    THREE.Math.randFloat(-halfWidth / 2, halfWidth / 2) + halfWidth,
    height,
    THREE.Math.randFloat(5, 100),
    halfWidth,
    height,
    gradientRange
  );
  var firstColorR = Math.floor(Math.random() * 255);
  var firstColorG = Math.floor(Math.random() * 255);
  var firstColorB = Math.floor(Math.random() * 255);

  var numOfGradientBands = Math.floor(THREE.Math.randFloat(2, 4));
  grd.addColorStop(
    0.25,
    "rgb(" + firstColorR + "," + firstColorG + ", " + firstColorB
  );
  if (numOfGradientBands == 3) {
    grd.addColorStop(
      0.35,
      "rgb(" +
        Math.floor(Math.random() * 255) +
        "," +
        Math.floor(Math.random() * 255) +
        ", " +
        Math.floor(Math.random() * 255)
    );
  }
  grd.addColorStop(
    0.45,
    "rgb(" +
      Math.floor(Math.random() * 255) +
      "," +
      Math.floor(Math.random() * 255) +
      ", " +
      Math.floor(Math.random() * 255)
  );

  // Render gradient across whole fill covering canvas
  ctx.fillStyle = grd;
  ctx.fillRect(0, 0, width, height);
  var pixels = [];

  // Map the pixel data (RGB) to an array
  for (var a = 1; a < 33; a++) {
    for (var b = 1; b < 33; b++) {
      var pixel = ctx.getImageData(
        (tempCanvas.width / 32) * a,
        (tempCanvas.height / 32) * b,
        1,
        1
      ).data;
      pixels.push(pixel);
    }
  }

  // * For debugging, render the gradient canvas to see the gradient
  //document.body.appendChild(tempCanvas);

  // GENERATE LOW-POLY MESH
  // ======================

  var pointsCount = 1000;
  var points3d = [];

  const visibleHeightAtZDepth = (depth, camera) => {
    // compensate for cameras not positioned at z=0
    const cameraOffset = camera.position.z;
    if (depth < cameraOffset) depth -= cameraOffset;
    else depth += cameraOffset;

    // vertical fov in radians
    const vFOV = (camera.fov * Math.PI) / 180;

    // Math.abs to ensure the result is always positive
    return 2 * Math.tan(vFOV / 2) * Math.abs(depth);
  };

  const visibleWidthAtZDepth = (depth, camera) => {
    const height = visibleHeightAtZDepth(depth, camera);
    return height * camera.aspect;
  };

  var visibleWidth = visibleWidthAtZDepth(camera.position.z, camera);
  if (visibleWidth < 800) {
    visibleWidth = 500;
  }

  var variance;
  var varianceSlider = document.getElementById("sliderRange");

  if (varianceSlider) {
    variance = varianceSlider.value;
  } else {
    variance = THREE.Math.randFloat(1, 10);
  }
  // generate 1024 verticies (32 * 32)
  for (let i = 1; i < 33; i++) {
    for (let j = 1; j < 33; j++) {
      // width/height of screen / 32 segents * index / 5 (used to scale the mesh. Larger values = smaller mesh)
      let x = (visibleWidth / 32) * i + THREE.Math.randFloatSpread(variance);
      let z = (visibleWidth / 32) * j + THREE.Math.randFloatSpread(variance);
      let y = THREE.Math.randFloatSpread(4);
      points3d.push(new THREE.Vector3(x, y, z));
    }
  }

  var geometry1 = new THREE.BufferGeometry().setFromPoints(points3d);

  // DELAUNAY / APPLY FACES TO MESH
  // ======================

  // triangulate x, z
  var indexDelaunay = Delaunator.from(
    points3d.map((v) => {
      return [v.x, v.z];
    })
  );

  // delaunay index => three.js index
  var meshIndex = [];
  for (let i = 0; i < indexDelaunay.triangles.length; i++) {
    meshIndex.push(indexDelaunay.triangles[i]);
  }

  // add three.js index to the existing geometry
  geometry1.setIndex(meshIndex);
  geometry1.computeVertexNormals();

  // get the geometry points using attributes.position.count
  const count = geometry1.attributes.position.count;
  // assign a color attribute to geometry points
  geometry1.setAttribute(
    "color",
    new THREE.BufferAttribute(new Float32Array(count * 3), 3)
  );

  const color = new THREE.Color();
  const positions1 = geometry1.attributes.position;
  const colors1 = geometry1.attributes.color;

  // Generate color
  color1 =
    "rgb(" +
    Math.floor(Math.random() * 255) +
    "," +
    Math.floor(Math.random() * 255) +
    "," +
    Math.floor(Math.random() * 255);

  for (let i = 0; i < count; i++) {
    color.setRGB(pixels[i][0] / 255, pixels[i][1] / 255, pixels[i][2] / 255);

    colors1.setXYZ(i, color.r, color.g, color.b);
  }

  var mesh = new THREE.Mesh(
    geometry1,
    new THREE.MeshPhongMaterial({
      color: 0xffffff,
      opacity: 1,
      vertexColors: true,
      flatShading: true,
      shininess: 30
    })
  );
  mesh.rotation.x = Math.PI / 2;
  mesh.position.y = 200;
  mesh.position.x -= visibleWidth / 2.5;
  mesh.scale.set(0.8, 0.8, 0.8);
  scene.add(mesh);

  const wireframeGeometry = new THREE.WireframeGeometry(geometry1);
  const wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0x111111 });
  const wireframe = new THREE.LineSegments(
    wireframeGeometry,
    wireframeMaterial
  );
  mesh.add(wireframe);
  wireframe.position.y = 2;

  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(headerContainer.offsetWidth, headerContainer.offsetHeight);
  var canvas = renderer.domElement;
  canvas.id = "headerCanvas";
  headerContainer.appendChild(canvas);

  // * DEBUG enable orbit controls
  //var controls = new THREE.OrbitControls(camera, canvas);

  document.addEventListener("mousemove", onDocumentMouseMove);
  document.addEventListener("mousedown", onDocumentMouseDown);
  document.addEventListener("mouseup", onDocumentMouseUp);

  window.addEventListener("resize", onWindowResize);
}

function onWindowResize() {
  camera.aspect = headerContainer.offsetWidth / headerContainer.offsetHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(headerContainer.offsetWidth, headerContainer.offsetHeight);
}

var distX;
var distY;
var pos;
console.log(mouse.x);
function onDocumentMouseMove(event) {
  // Update the mouse variable
  //event.preventDefault();
  mouse.x = (event.clientX / headerContainer.offsetWidth) * 2 - 1;
  mouse.y = -(event.clientY / headerContainer.offsetHeight) * 2 + 1;

  // Make the light follow the mouse
  var vector = new THREE.Vector3(mouse.x, mouse.y, 0.5);
  vector.unproject(camera);
  var dir = vector.sub(camera.position).normalize();
  var distance = -camera.position.z / dir.z;
  pos = camera.position.clone().add(dir.multiplyScalar(distance));

  //1. find distance X , distance Y
  distX = pos.x - mouseLight.position.x;
  distY = pos.y - mouseLight.position.y;
  //Easing motion
  //Progressive reduction of distance
  mouseLight.position.x += distX / 10;
  mouseLight.position.y += distY / 10;

  mouseLight.position.z = mouseLightHeight;
}

function GetMousePosition() {}

function onDocumentMouseDown(event) {
  mouseIsDown = true;
}

function onDocumentMouseUp(event) {
  mouseIsDown = false;
}

//

function animate() {
  if (mouseIsDown & (mouseLight.distance < 150)) {
    mouseLight.distance += 2;
  } else if (mouseLight.distance > 30) {
    mouseLight.distance -= 2;
  }
  if (pos) {
    if (mouseLight.position.x != pos.x) {
      distX = pos.x - mouseLight.position.x;
      mouseLight.position.x += distX / 50;
    }
    if (mouseLight.position.y != pos.y) {
      distY = pos.y - mouseLight.position.y;
      mouseLight.position.y += distY / 50;
    }
  }

  requestAnimationFrame(animate);

  render();
}

function render() {
  camera.lookAt(scene.position);

  renderer.render(scene, camera);
}

function toggleLight() {
  if (light.intensity == 0) {
    light.intensity = 1;
  } else light.intensity = 0;
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/[email protected]/delaunator.js
  2. https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js