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.
Unless otherwise noted, code published here is © Gábor L Ugray, shared under the Creative Commons
BY-NC-SA license (Attribution, Non-Commercial, Share-Alike). Files in lib/thirdparty
, and additional
libraries in the downloadable archive, are shared under their respective open-source licenses, attributed
to their authors.
///<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);
}