readme: <style>
  @import url('https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@900&family=Rokkitt:wght@900&display=swap');
  .text1 {
    position: absolute;
    top: 3vh;
    left: calc(50% - 50vh);
  }
  .text2 {
    position: absolute;
    bottom: 4vh;
    left: 50%;
  }
  .retro {
    font-family: "Roboto Slab";
    font-size: 13vh;
    display: block;
    color: #000;
    text-shadow: -0.5vh 0 #8800aa, 0 0.5vh #8800aa, 0.5vh 0 #aa0088, 0 -0.5vh #aa0088;
  }
  </style>
  
  <div class="text1">
    <span class="retro">RETRO</span>
  </div>
  <div class="text2">
    <span class="retro">WAVE</span>
  </div>
  
  <script type="module">
  import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.123.0/build/three.module.js";
  import { OrbitControls } from "https://cdn.jsdelivr.net/npm/three@0.123.0/examples/jsm/controls/OrbitControls.js";
  import { TWEEN } from "https://cdn.jsdelivr.net/npm/three@0.123.0/examples/jsm/libs/tween.module.min.js";
  
  let scene = new THREE.Scene();
  let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 100);
  camera.position.set(-5, 10, 20);
  let renderer = new THREE.WebGLRenderer({antialias: true});
  renderer.setSize(innerWidth, innerHeight);
  document.querySelector("div.prose").appendChild(renderer.domElement);
  
  const textureCube = generateCubeMap();
  
  let controls = new OrbitControls(camera, renderer.domElement);
  controls.enableZoom = false;
  controls.enablePan = false;
  controls.enableKeys = false;
  
  let square = new THREE.GridHelper(20, 1, 0xaaaaff, 0xaaaff);
  square.position.y = 0.01;
  scene.add(square);
  
  let grid = new THREE.GridHelper(20, 10, "magenta", "magenta");
  console.log(grid.geometry.attributes.position.count);
  let moveable = [];
  for(let i = 0; i < grid.geometry.attributes.position.count / 4; i++){
    moveable.push(1, 1, 0, 0);
  }
  console.log(moveable.length)
  grid.geometry.setAttribute("moveable", new THREE.Float32BufferAttribute(moveable, 1));
  let uniforms = {
    time: {value: 0},
    speed: {value: 1},
    size: {value: 20}
  }
  grid.material.onBeforeCompile = shader => {
    shader.uniforms.time = uniforms.time;
    shader.uniforms.speed = uniforms.speed;
    shader.uniforms.size = uniforms.size;
    shader.vertexShader = `
      uniform float time;
      uniform float speed;
      uniform float size;
      attribute float moveable;
      ${shader.vertexShader}
    `.replace(
      `#include <begin_vertex>`,
      `#include <begin_vertex>
          
          if (floor(moveable + 0.1) > 0.5){
            float start = size * -0.5;
            float zPos = mod( (position.z - start) + (time * speed), size) + start;
            transformed.z = zPos;
          }
      `
    );
    console.log(shader.vertexShader)
  }
  scene.add(grid);
  
  // palm
  let base = new THREE.Object3D();
  
  let baseSpline = new THREE.CatmullRomCurve3([
    new THREE.Vector2(),
    new THREE.Vector2(3, 0),
    new THREE.Vector2(2.5, -7),
    new THREE.Vector2(-4, -6),
    new THREE.Vector2(-4.8, 0)
  ], true, "catmullrom", 0.1);
  let baseG = new THREE.ExtrudeBufferGeometry(new THREE.Shape(baseSpline.getPoints(50)), {depth: 0.2, bevelEnabled: true, bevelThickness: 0.8, bevelSize: 0.2});
  let baseObject = new THREE.Mesh(baseG, new THREE.MeshBasicMaterial({color: "magenta", wireframe: false, envMap: textureCube}));
  base.add(baseObject);
  scene.add(base);
  let phalanxes = [];
  let f1 = createFinger(new THREE.Object3D(), 0.8, false);   // pinky
  let f2 = createFinger(new THREE.Object3D(), 0.95, false);  // ring
  let f3 = createFinger(new THREE.Object3D(), 1, false);     // middle
  let f4 = createFinger(new THREE.Object3D(), 0.95, false);  // index
  let f5Base = new THREE.Object3D();
  let f5 = createFinger(new THREE.Object3D(), 0.75, true);  // thumb
  f5Base.add(f5);
  base.add(f1, f2, f3, f4, f5Base);
  
  f1.position.set( -4, 0.2, 0);
  f2.position.set( -2, 0.2, 0);
  f3.position.set(  0, 0.2, 0);
  f4.position.set(  2, 0.2, 0);
  f5Base.position.set(  3, -3, 0);
  f5Base.rotation.set( 0, 0, THREE.MathUtils.degToRad(-60));
  f5Base.updateMatrixWorld();
  
  let g = createPhalanxGeom(1, 3);
  let m = new THREE.MeshBasicMaterial({color: "aqua", wireframe: false, envMap: textureCube});
  let o = new THREE.InstancedMesh(g, m, phalanxes.length);
  phalanxes.forEach( (ph, i) => {
    ph.updateMatrixWorld();
    o.setMatrixAt(i, ph.matrixWorld);
  })
  scene.add(o);
  
  window.addEventListener( 'resize', onWindowResize, false );
  
  let t = new TWEEN.Tween({value: Math.PI * 0.075})
    .to({value: Math.PI * 0.45}, 4000)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .repeat(Infinity)
    .yoyo(true)
    .onUpdate(val => {
      phalanxes.forEach((ph, i) => {
        ph.rotation.x = val.value;
        ph.updateMatrixWorld();
        o.setMatrixAt(i, ph.matrixWorld)
      });
      o.instanceMatrix.needsUpdate = true;
    });
  t.start();
  
  let clock = new THREE.Clock();
  
  renderer.setAnimationLoop(() => {
    let t = clock.getElapsedTime();
    TWEEN.update();
    uniforms.time.value = t;
    base.rotation.x = (Math.sin(t * 0.125) * 0.5 + 0.5) * -Math.PI * 0.5;
    base.rotation.y = -t * 0.125;
    renderer.render(scene, camera);
  });
  
  function onWindowResize() {
  
    camera.aspect = innerWidth / innerHeight;
    camer