Source code of plot #039 back to plot

Download full working sketch as 039.tar.gz.
Unzip, then start a local web server and load the page in a browser.

///<reference path="../pub/lib/paper.d.ts" />
import {info, init, loadLib, setSketch, dbgRedraw} from "./utils/boilerplate.js";
import {mulberry32, rand, rand_range, rand_select, randn_bm, setRandomGenerator, shuffle} from "./utils/random.js"
import * as THREE from "../pub/lib/three.module.js";
import {Vector3} from "../pub/lib/three.module.js";
import {OrbitControls} from "../pub/lib/OrbitControls.js"
import {WebGLCanvasMasker} from "./utils/webgl-canvas-masker.js";

const pw = 2100;    // Paper width
const ph = 1480;    // Paper height
const w = 1480;     // Drawing width
const h = 1050;     // Drawing height
const margin = 50;

let camProps = {
  fov: 60,
  position: { x: -980, y: 1450, z: 2340}, target: { x: 0, y: 290, z: 21 },
};
let sceneBgColor = "black";

// Canvas masker & Three JS canvas/machinery
let segLen = 1;
let rf = 2;                 // Occlusion canvas & three canvas are this many times larger than our area
let elmThreeCanvas;
let renderer, scene, cam, ray;

// If true, we'll create colorizer and colorize faces to sample for vector graphics
// If false, we'll render wireframe-like 3D model
let colorizer = true;

// If true, orbit controls will be created for interactive exploration of 3D model
// If false, we'll render plot
let controls = false;

let seed;     // Random seed
if (window.fxhash) seed = Math.round(fxrand() * 65535);
else seed = Math.round(Math.random() * 65535);
// seed = 57737;

setRandomGenerator(mulberry32(seed));

setSketch(function () {

  info("Seed: " + seed, seed);
  init(w, h, pw, ph);

  // Three JS canvas
  initThree();

  setTimeout(draw, 10);
});

async function draw() {

  paper.project.addLayer(new paper.Layer({name: "1-cyan"}));
  paper.project.addLayer(new paper.Layer({name: "2-magenta"}));

  paper.project.currentStyle.strokeColor = "black";
  paper.project.currentStyle.strokeWidth = 2;

  let frame = Path.Rectangle(margin, margin, w - 2 * margin, h - 2 * margin);
  project.activeLayer.addChild(frame);

  // If colorizer is requested, create it
  if (colorizer) colorizer = new Colorer(31, 31, 31);

  const boxes = [];
  const boardLines = [];
  const darkTiles = [];

  const fieldSz = 500;
  const bRange = [-8, 13];
  const dRange = [-13, 4];
  const figureProb = 0.3;

  const clrDark = new THREE.Color(0x404040);
  const clrLight = new THREE.Color(0xe0e0e0);
  const fieldDarkMat = new THREE.MeshBasicMaterial({color: clrDark, side: THREE.DoubleSide});
  const fieldLightMat = new THREE.MeshBasicMaterial({color: clrLight, side: THREE.DoubleSide});
  const matColor = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors, side: THREE.DoubleSide});
  const matWF =  new THREE.MeshBasicMaterial({color: 0x806040});

  for (let iz = dRange[0]; iz <= dRange[1]; ++iz) {
    let bl1 = new Vector3((bRange[0] - 0.5) * fieldSz, 0, (iz - 0.5) * fieldSz);
    let bl2 = new Vector3((bRange[1] + 0.5) * fieldSz, 0, (iz - 0.5) * fieldSz);
    boardLines.push([bl1, bl2]);
  }

  for (let ix = bRange[0]; ix <= bRange[1]; ++ix) {
    let bl1 = new Vector3((ix - 0.5) * fieldSz, 0, (dRange[0] - 0.5) * fieldSz);
    let bl2 = new Vector3((ix - 0.5) * fieldSz, 0, (dRange[1] + 0.5) * fieldSz);
    boardLines.push([bl1, bl2]);
  }

  for (let iz = dRange[0]; iz < dRange[1]; ++ iz) {
    for (let ix = bRange[0]; ix < bRange[1]; ++ix) {

      // Field on the board
      const fieldGroup = new THREE.Group();
      scene.add(fieldGroup);
      fieldGroup.translateX(ix * fieldSz);
      fieldGroup.translateZ(iz * fieldSz);

      // Floor tile
      let isDark = (ix + iz) % 2 == 0;
      let mat = isDark ? fieldDarkMat : fieldLightMat;
      const fieldShape = new THREE.Shape();
      fieldShape.moveTo(-fieldSz / 2, fieldSz / 2);
      fieldShape.lineTo(-fieldSz / 2, -fieldSz / 2);
      fieldShape.lineTo(fieldSz / 2, -fieldSz / 2);
      fieldShape.lineTo(fieldSz / 2, fieldSz / 2);
      const fieldGeo = new THREE.ShapeGeometry(fieldShape);
      const fieldMesh = new THREE.Mesh(fieldGeo, mat);
      fieldMesh.rotateX(-Math.PI / 2);
      fieldGroup.add(fieldMesh);
      if (isDark) darkTiles.push(fieldMesh);

      // Figure
      if (rand() < figureProb) {

        let figure;

        if (rand() < 0.8) figure = makeFigure1(fieldSz, matWF, matColor);
        else figure = makeFigure2(fieldSz, matWF, matColor);
        // figure = makeFigure1(fieldSz, matWF, matColor);

        fieldGroup.add(figure);
        for (const box of figure.children) boxes.push(box);

      }
    }
  }

  renderer.render(scene, cam);

  // If orbit controls are requested, create them, and kick off animation
  if (controls) {
    controls = new OrbitControls(cam, renderer.domElement);
    controls.target.set(camProps.target.x, camProps.target.y, camProps.target.z);
    requestAnimationFrame(animate);
    return;
  }

  // No orbit controls: render plot
  // Get pixels of 3D canvas
  let pixels = new Uint8Array(w * rf * h * rf * 4);
  let ctx = elmThreeCanvas.getContext("webgl2", {preserveDrawingBuffer: true});
  ctx.readPixels(0, 0, ctx.drawingBufferWidth, ctx.drawingBufferHeight, ctx.RGBA, ctx.UNSIGNED_BYTE, pixels);


  // Lines to plot!
  const allLines = [];

  // Project board's lines
  for (const bl of boardLines) {
    let [pt1, z1] = proj(bl[0]);
    let [pt2, z2] = proj(bl[1]);
    allLines.push(new FilterableLine(pt1, pt2, clrTo8Bit(clrLight), clrTo8Bit(clrDark)));
  }

  // Get dark tile hatches
  for (const tile of darkTiles) {
    let hatches = getTileHatches(tile);
    for (const [pt1, pt2] of hatches) {
      allLines.push(new FilterableLine(pt1, pt2, clrTo8Bit(clrDark), clrTo8Bit(clrDark)));
    }
  }

  // Project edges of cubes
  for (let i = boxes.length - 1; i >= 0; --i) {
    const box = boxes[i];
    const boxEdges = [];
    getBoxLines(box, boxEdges);
    allLines.push(...boxEdges);
  }

  // Line hiding based on pixel data
  let visibleEdges = [];
  let maskFrame = [margin, margin, w - 2 * margin, h - 2 * margin];
  const wcm = new WebGLCanvasMasker(pixels, w, h, rf, true);
  visibleEdges = wcm.mask(allLines, maskFrame, segLen);

  // Render lines in Paper
  for (const vl of visibleEdges) {
    let ln = Path.Line(vl[0], vl[1]);
    project.activeLayer.addChild(ln);
  }
}

function makeFigure2(fieldSz, matWF, matColor) {

  const mat = colorizer ? matColor : matWF;
  const group = new THREE.Group();

  const sz = fieldSz * 0.5;
  const gap = fieldSz * 0.05;
  let elev = sz * 0.2;
  const nLayers = Math.floor(rand_range(6, 9));
  for (let i = 0; i < nLayers; ++i) {

    let val = (i - (nLayers - 1) / 2) / ((nLayers - 1) / 2); // -1 to +1
    val *= Math.PI / 2;
    let h = gap / 2 + Math.pow(Math.cos(val), 2) * fieldSz * 0.4;
    let geo = new THREE.BoxGeometry(sz, h, sz).toNonIndexed();
    let mesh = new THREE.Mesh(geo, mat);
    if (colorizer) colorizer.colorFaces(geo);
    else addWF(mesh, geo);
    mesh.translateY(h * 0.5 + elev);
    group.add(mesh);
    elev += h + gap;
  }

  return group;
}

function makeFigure1(fieldSz, matWF, matColor) {

  const mat = colorizer ? matColor : matWF;
  const group = new THREE.Group();
  const sz = fieldSz * 0.6;

  const nLevels = rand_select([5,7,9,11,13]);
  const levelH = sz * rand_range(0.1, 4 / nLevels);
  const levelGap = sz * rand_range(0.1, 0.2);
  const szFun = rand_select([szFunPeak, szFunBulb]);
  for (let i = 0; i < nLevels; ++i) {
    const szHere = szFun(i);
    let geo = new THREE.BoxGeometry(szHere, levelH, szHere).toNonIndexed();
    let mesh = new THREE.Mesh(geo, mat);
    if (colorizer) colorizer.colorFaces(geo);
    else addWF(mesh, geo);
    mesh.translateY(levelH / 2 + i * (levelH + levelGap));
    group.add(mesh);
  }

  function szFunPeak(i) {
    const prop = 1 - ((i / (nLevels + 1)) ** 0.6);
    return sz * prop;
  }

  function szFunBulb(i) {
    let prop = (i - nLevels / 2) / nLevels * 1.2;
    prop = 1 - Math.abs(prop);
    prop = prop ** 2;
    return sz * prop;
  }

  return group;
}

class FilterableLine {
  constructor(pt1, pt2, clr1, clr2) {
    this.pt1 = pt1;
    this.pt2 = pt2;
    this.clr1 = clr1;
    this.clr2 = clr2;
  }
}

function getTileHatches(mesh) {
  const res = [];
  const positionAttribute = mesh.geometry.getAttribute("position");
  const crns = [];
  for (let i = 0; i < positionAttribute.count; ++i) {
    const v = new THREE.Vector3();
    v.fromBufferAttribute(positionAttribute, i);
    mesh.localToWorld(v);
    crns.push(proj(v)[0]);
  }

  let dA = dist(crns[0], crns[1], crns[2]);
  let dB = dist(crns[3], crns[1], crns[2]);
  let d = (dA + dB) / 2;
  let nLines = Math.round(d / 7);
  if (nLines > 0) {
    for (let i = 1; i < nLines + 1; ++i) {
      let prop = i / (nLines + 1);
      let pt1 = crns[0].add(crns[1].subtract(crns[0]).multiply(prop));
      let pt2 = crns[3].add(crns[2].subtract(crns[3]).multiply(prop));
      res.push([pt1, pt2]);
    }
  }
  dA = dist(crns[0], crns[2], crns[3]);
  dB = dist(crns[1], crns[2], crns[3]);
  d = (dA + dB) / 2;
  nLines = Math.round(d / 7);
  for (let i = 1; i < nLines + 1; ++i) {
    let prop = i / (nLines + 1);
    let pt1 = crns[1].add(crns[2].subtract(crns[1]).multiply(prop));
    let pt2 = crns[0].add(crns[3].subtract(crns[0]).multiply(prop));
    res.push([pt1, pt2]);
  }

  return res;

  function dist(p, p1, p2) {

    let A = p.x - p1.x;
    let B = p.y - p1.y;
    let C = p2.x - p1.x;
    let D = p2.y - p1.y;

    let dot = A * C + B * D;
    let len_sq = C * C + D * D;
    let param = dot / len_sq;

    let xx, yy;

    xx = p1.x + param * C;
    yy = p1.y + param * D;

    let dx = p.x - xx;
    let dy = p.y - yy;
    return Math.sqrt(dx * dx + dy * dy);
  }

}

function getBoxLines(mesh, edges) {
  const positionAttribute = mesh.geometry.getAttribute("position");
  const colorAttribute = mesh.geometry.getAttribute("color");
  // Got 36 vertices (6 per side, 6 sides)
  //  0 -  5: Right
  //  5 - 11: Left
  // 12 - 17: Top
  // 18 - 23: Bottom
  // 24 - 29: Front
  // 30 - 35: Back
  let top = Number.MIN_VALUE, right = Number.MIN_VALUE, front = Number.MIN_VALUE;
  let bottom = Number.MAX_VALUE, left = Number.MAX_VALUE, back = Number.MAX_VALUE;
  for (let i = 0; i < positionAttribute.count; ++i) {
    const v = new THREE.Vector3();
    v.fromBufferAttribute(positionAttribute, i);
    if (v.x < left) left = v.x;
    if (v.x > right) right = v.x;
    if (v.y < bottom) bottom = v.y;
    if (v.y > top) top = v.y;
    if (v.z < back) back = v.z;
    if (v.z > front) front = v.z;
  }
  // Corners
  let tlf = new THREE.Vector3(left, top, front);
  let trf = new THREE.Vector3(right, top, front);
  let blf = new THREE.Vector3(left, bottom, front);
  let brf = new THREE.Vector3(right, bottom, front);
  let tlb = new THREE.Vector3(left, top, back);
  let trb = new THREE.Vector3(right, top, back);
  let blb = new THREE.Vector3(left, bottom, back);
  let brb = new THREE.Vector3(right, bottom, back);
  // Side colors
  let clrFront = new THREE.Color();
  let clrBack = new THREE.Color();
  let clrLeft = new THREE.Color();
  let clrRight = new THREE.Color();
  let clrTop = new THREE.Color();
  let clrBottom = new THREE.Color();
  clrFront.fromBufferAttribute(colorAttribute, 24);
  clrBack.fromBufferAttribute(colorAttribute, 30);
  clrLeft.fromBufferAttribute(colorAttribute, 6);
  clrRight.fromBufferAttribute(colorAttribute, 0);
  clrTop.fromBufferAttribute(colorAttribute, 12);
  clrBottom.fromBufferAttribute(colorAttribute, 18);
  clrFront = clrTo8Bit(clrFront);
  clrBack = clrTo8Bit(clrBack);
  clrLeft = clrTo8Bit(clrLeft);
  clrRight = clrTo8Bit(clrRight);
  clrTop = clrTo8Bit(clrTop);
  clrBottom = clrTo8Bit(clrBottom);

  // Edges - projected, with color
  // Front top
  addIfInView(tlf, trf, clrFront, clrTop, edges);
  // Front bottom
  addIfInView(blf, brf, clrFront, clrBottom, edges);
  // Front left
  addIfInView(tlf, blf, clrFront, clrLeft, edges);
  // Front right
  addIfInView(trf, brf, clrFront, clrRight, edges);
  // Back top
  addIfInView(tlb, trb, clrBack, clrTop, edges);
  // Back bottom
  addIfInView(blb, brb, clrBack, clrBottom, edges);
  // Back left
  addIfInView(tlb, blb, clrBack, clrLeft, edges);
  // Back right
  addIfInView(trb, brb, clrBack, clrRight, edges);
  // Top left depth
  addIfInView(tlf, tlb, clrTop, clrLeft, edges);
  // Top right depth
  addIfInView(trf, trb, clrTop, clrRight, edges);
  // Bottom left depth
  addIfInView(blf, blb, clrBottom, clrLeft, edges);
  // Bottom right depth
  addIfInView(brf, brb, clrBottom, clrRight, edges);


  function addIfInView(vert1, vert2, clr1, clr2, arr) {
    let [pt1, z1] = pr(vert1);
    let [pt2, z2] = pr(vert2);
    // If both points behind camera, or outside canvas: ignore
    if (z1 <=0 && z2 <= 0) return;
    let [l, r, t, b] = getBounds([pt1, pt2]);
    if (r < 0 || l > w || b < 0 || t > h) return;
    arr.push(new FilterableLine(pt1, pt2, clr1, clr2));
  }

  function getBounds(pts) {
    let left = Number.MAX_VALUE, top = Number.MAX_VALUE;
    let right = Number.MIN_VALUE, bottom = Number.MIN_VALUE;
    for (const pt of pts) {
      left = Math.min(pt.x, left);
      right = Math.max(pt.x, right);
      top = Math.min(pt.y, top);
      bottom = Math.max(pt.y, bottom);
    }
    return [left, right, top, bottom];
  }

  function pr(vert) {
    let w = vert.clone();
    mesh.localToWorld(w);
    return proj(w);
  }
}

function clrTo8Bit(clr) {
  return {
    r: Math.floor(clr.r >= 1 ? 255 : clr.r * 256),
    g: Math.floor(clr.g >= 1 ? 255 : clr.g * 256),
    b: Math.floor(clr.b >= 1 ? 255 : clr.b * 256),
  };
}

class Colorer {

  constructor(nHues, nSats, nLights) {
    this.currIx = 0;
    const minSat = 30;
    const maxSat = 80;
    const minLight = 30;
    const maxLight = 80;
    this.colors = [];
    for (let iHue = 0; iHue < nHues; ++iHue) {
      for (let iSat = 0; iSat < nSats; ++iSat) {
        for (let iLight = 0; iLight < nLights; ++iLight) {
          let hue = 360 * iHue / nHues;
          let sat = minSat + (maxSat - minSat) * iSat / nSats;
          let light = minLight + (maxLight - minLight) * iLight / nLights;
          hue = Math.round(hue);
          sat = Math.round(sat);
          light = Math.round(light);
          let str = "hsl(" + hue + ", " + sat + "%, " + light + "%)";
          this.colors.push(new THREE.Color(str));
        }
      }
    }
    shuffle(this.colors);
  }

  next() {
    let res = this.colors[this.currIx];
    this.currIx = (this.currIx + 1) % this.colors.length;
    return res;
  }

  colorFaces(geo) {
    const positionAttribute = geo.getAttribute('position');
    const colors = [];
    let color;
    for (let i = 0; i < positionAttribute.count; ++i) {
      if ((i % 6) == 0) color = this.next();
      colors.push(color.r, color.g, color.b);
    }
    // define the new attribute
    geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
  }
}

// ===========================================================================
// Three JS machinery
// ===========================================================================


function initThree() {

  const elmPaperCanvas = document.getElementById("paper-canvas");
  const elmCanvasHost = document.getElementById("canvasHost");
  const canvasWidth = elmPaperCanvas.clientWidth;
  const canvasHeight = canvasWidth * h / w;
  const asprat = w / h;

  renderer = new THREE.WebGLRenderer({preserveDrawingBuffer: true});
  elmCanvasHost.appendChild(renderer.domElement);
  elmThreeCanvas = renderer.domElement;
  elmThreeCanvas.id = "three-canvas";
  renderer.setSize(w * rf, h * rf);

  elmCanvasHost.style.width = (canvasWidth * 2) + "px";
  elmPaperCanvas.style.width = canvasWidth + "px";
  elmPaperCanvas.style.position = "relative";
  elmThreeCanvas.style.position = "relative";
  elmThreeCanvas.style.float = "right";
  elmThreeCanvas.style.width = canvasWidth + "px";
  elmThreeCanvas.style.height = canvasHeight + "px";

  let D = w;
  // cam = new THREE.OrthographicCamera(-D, D, D / asprat, -D / asprat, 1, 10000);
  cam = new THREE.PerspectiveCamera(camProps.fov, asprat, 1, 40000);

  cam.position.set(camProps.position.x, camProps.position.y, camProps.position.z);
  cam.lookAt(camProps.target.x, camProps.target.y, camProps.target.z);
  cam.updateProjectionMatrix();

  scene = new THREE.Scene();
  scene.background = new THREE.Color(sceneBgColor);

  ray = new THREE.Raycaster();
}

function animate() {

  controls.update();

  let newPos = cam.position;
  let newTarget = controls.target;
  let camMoved =
    !nearEq(newPos.x, camProps.position.x) || !nearEq(newPos.y, camProps.position.y) || !nearEq(newPos.z, camProps.position.z) ||
    !nearEq(newTarget.x, camProps.target.x) || !nearEq(newTarget.y, camProps.target.y) || !nearEq(newTarget.z, camProps.target.z);
  if (camMoved) {
    camProps.position.x = twoDecimals(newPos.x);
    camProps.position.y = twoDecimals(newPos.y);
    camProps.position.z = twoDecimals(newPos.z);
    camProps.target.x = twoDecimals(newTarget.x);
    camProps.target.y = twoDecimals(newTarget.y);
    camProps.target.z = twoDecimals(newTarget.z);
    console.log(JSON.stringify(camProps));
  }

  renderer.render(scene, cam);
  requestAnimationFrame(animate);

  function nearEq(f, g) {
    if (f == g) return true;
    let ratio = f / g;
    return ratio > 0.999999 && ratio < 1.000001;
  }

  function twoDecimals(x) {
    return Math.round(x * 100) / 100;
  }
}

function proj(vec) {
  let projected = vec.clone().project(cam);
  return [new Point((projected.x + 1) * w / rf, (1 - projected.y) * h / rf), projected.z];
}

function addWF(mesh, geo) {
  let wfg = new THREE.WireframeGeometry(geo);
  let wmat = new THREE.LineBasicMaterial({color: 0xeffffff});
  let wf = new THREE.LineSegments(wfg, wmat);
  mesh.add(wf);
}