Source code of plot #038 back to plot
Download full working sketch as 038.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: 30,
//"position":{"x":0,"y":3213.95,"z":-6304.47},"target":{"x":0,"y":0,"z":655.71}
// "position":{"x":-650.49,"y":1717.12,"z":-2349.29},"target":{"x":105.62,"y":-359.09,"z":431.49}
"position":{"x":-1993.42,"y":1605.49,"z":-3165.82},"target":{"x":22.42,"y":-679.6,"z":267}
};
let sceneBgColor = "black";
// Toggle: draw squiggly lines (slow, so we're turning it off while tuning the geometry)
let squiggly = false;
// Canvas masker & Three JS canvas/machinery
let segLen = 2;
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 = 4054;
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(23, 23, 23);
let boxes = [];
const matColor = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors, side: THREE.DoubleSide});
const matWF = new THREE.MeshBasicMaterial({color: 0x806040});
const cityX = w * 3;
const cityZ = w * 3;
const nBlocksX = 8;
const nBlocksZ = 8;
const avenueW = 40;
const streetW = 30;
const minH = h * 0.03;
const maxH = h * 0.6;
const keepProb = 0.99;
const nBuildingsPerBlock = [6, 20];
// let geo = new THREE.BoxGeometry(cityX, h * 0.05, cityZ).toNonIndexed();
// let mesh = new THREE.Mesh(geo, mat);
// if (!colorizer) addWF(mesh, geo);
// mesh.position.y = -h * 0.05;
// scene.add(mesh);
for (let ix = 0; ix < nBlocksX; ++ix) {
for (let iz = 0; iz < nBlocksZ; ++iz) {
let blockW = (cityX - avenueW * (nBlocksX - 1)) / nBlocksX;
let blockD = (cityZ - avenueW * (nBlocksZ - 1)) / nBlocksZ;
let blockCenterX = -cityX / 2 + (ix + 0.5) * blockW + ix * avenueW;
let blockCenterZ = -cityZ / 2 + (iz + 0.5) * blockD + iz * avenueW;
let zRel = iz / nBlocksZ;
let xRel = ix / nBlocksX;
let blockDistSq = Math.sqrt(zRel ** 2 + xRel ** 2);
// let ptBlockCenter = new THREE.Vector3(blockCenterX, 0, blockCenterZ);
// let ptCam = cam.matrixWorld.getPosition().clone();
// let blockDist = ptCam.distanceTo(ptBlockCenter);
// console.log(blockDist);
// let nBuildings = Math.floor(rand_range(...nBuildingsPerBlock));
// let nBuildings = 6 + 8 * Math.round(blockDistSq);
let nBuildings = shuffle([4,4,6,15,16,17])[0];
let buildings = subDivBlock(blockW, blockD, nBuildings, minH, maxH, streetW, keepProb);
for (const bdg of buildings) {
let geo = new THREE.BoxGeometry(bdg.width, bdg.height, bdg.depth).toNonIndexed();
let mesh;
// Colorize faces...
if (colorizer) {
colorizer.colorFaces(geo);
mesh = new THREE.Mesh(geo, matColor);
}
// ...or add wireframe
else {
mesh = new THREE.Mesh(geo, matWF);
addWF(mesh, geo);
}
mesh.position.x = blockCenterX + bdg.centerX;
mesh.position.z = blockCenterZ + bdg.centerZ;
mesh.position.y = bdg.height / 2;
scene.add(mesh);
boxes.push(mesh);
}
}
}
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);
// Project edges and face decor
const allEdges = [];
const allFaceLines = [];
for (let i = boxes.length - 1; i >= 0; --i) {
const box = boxes[i];
const boxEdges = [];
const faceLines = [];
const gapW = rand_range(5, 15);
const gapD = rand_range(5, 30);
const gapH = rand_range(5, 15);
getBoxLines(box, boxEdges, gapW, gapD, gapH, faceLines);
allEdges.push(...boxEdges);
allFaceLines.push(...faceLines);
}
// Line hiding based on pixel data
let visibleEdges = [], visibleFaceLines = [];
let maskFrame = [margin, margin, w - 2 * margin, h - 2 * margin];
const wcm = new WebGLCanvasMasker(pixels, w, h, rf, true);
visibleEdges = wcm.mask(allEdges, maskFrame, segLen);
visibleFaceLines = wcm.mask(allFaceLines, maskFrame, segLen);
// Render lines in Paper
for (const vl of visibleEdges) {
let ln = Path.Line(vl[0], vl[1]);
project.activeLayer.addChild(ln);
}
// Render lines in Paper
for (const vl of visibleFaceLines) {
let ln = Path.Line(vl[0], vl[1]);
project.activeLayer.addChild(ln);
}
}
function subDivBlock(blockW, blockD, nBuildings, minH, maxH, streetW, keepProb) {
// Start with 1 building occupying whole block
let buildings = [new Building(0, 0, blockW, blockD, 0)];
// Divide until we reach desired # of buildings
for (let nTries = 0; buildings.length < nBuildings && nTries < 200; ++nTries) {
// Divide building with most skewed proportions
let toDivIx = 0;
let toDiv = buildings[toDivIx];
let toDivMeasure = divMeasure(toDiv);
for (let i = 1; i < buildings.length; ++i) {
let bdg = buildings[i];
let measure = divMeasure(bdg);
if (measure > toDivMeasure) {
toDivIx = i;
toDiv = bdg;
toDivMeasure = measure;
}
}
// Divide longer side
let ratio = rand_range(0.2, 0.8);
let newW1 = toDiv.width, newW2 = toDiv.width;
let newD1 = toDiv.depth, newD2 = toDiv.depth;
let newCX1 = toDiv.centerX, newCX2 = toDiv.centerX;
let newCZ1 = toDiv.centerZ, newCZ2 = toDiv.centerZ;
if (toDiv.width > toDiv.depth) {
newW1 = newW1 * ratio - streetW / 2;
newW2 = toDiv.width - newW1 - streetW / 2;
newCX1 = toDiv.centerX - toDiv.width / 2 + newW1 / 2 - streetW / 4;
newCX2 = toDiv.centerX + toDiv.width / 2 - newW2 / 2 + streetW / 4;
}
else {
newD1 = newD1 * ratio - streetW / 2;
newD2 = toDiv.width - newD1 - streetW / 2;
newCZ1 = toDiv.centerZ - toDiv.depth / 2 + newD1 / 2 - streetW / 4;
newCZ2 = toDiv.centerZ + toDiv.depth / 2 - newD2 / 2 + streetW / 4;
}
if (newW1 <= 0 || newW2 <= 0 || newD1 <= 0 || newD2 <= 0) continue;
if (outOfProp(newW1, newD1) || outOfProp(newW2, newD2)) continue;
let bdg1 = new Building(newCX1, newCZ1, newW1, newD1, 0);
let bdg2 = new Building(newCX2, newCZ2, newW2, newD2, 0);
// TODO: Elegantly this
let newBuildings = [];
for (let i = 0; i < buildings.length; ++i) {
if (i != toDivIx) newBuildings.push(buildings[i]);
else {
newBuildings.push(bdg1);
newBuildings.push(bdg2);
}
}
buildings = newBuildings;
}
// Cull some buildings; set random height for those kept
const res = [];
for (const bdg of buildings) {
if (rand() >= keepProb) continue;
bdg.height = rand_range(minH, maxH);
res.push(bdg);
}
return res;
function divMeasure(bdg) {
// return Math.max(bdg.width / bdg.depth, bdg.depth / bdg.width);
return bdg.width * bdg.depth;
}
function outOfProp(width, depth) {
let ratio = Math.max(width / depth, depth / width);
return ratio < 0 || ratio > 4;
}
}
class Building {
constructor(centerX, centerZ, width, depth, height) {
this.centerX = centerX;
this.centerZ = centerZ;
this.width = width;
this.depth = depth;
this.height = height;
}
}
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;
}
class FilterableLine {
constructor(pt1, pt2, clr1, clr2) {
this.pt1 = pt1;
this.pt2 = pt2;
this.clr1 = clr1;
this.clr2 = clr2;
}
}
function getBoxLines(mesh, edges, gapW, gapD, gapH, faceLines) {
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 = to8bit(clrFront);
clrBack = to8bit(clrBack);
clrLeft = to8bit(clrLeft);
clrRight = to8bit(clrRight);
clrTop = to8bit(clrTop);
clrBottom = to8bit(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);
// Face lines
// Front
fillFace(tlf, blf, trf, brf, clrFront, gapH, gapW);
// Back
fillFace(trb, brb, tlb, blb, clrBack, gapH, gapW);
// Right
fillFace(trf, brf, trb, brb, clrRight, gapH, gapD);
// Left
fillFace(tlb, blb, tlf, blf, clrLeft, gapH, gapD);
// Top
fillFace(tlb, tlf, trb, trf, clrTop, gapD, gapW);
function fillFace(p1, p2, p3, p4, clr, gap1, gap2) {
// Project into view
let [pr1, z1] = pr(p1);
let [pr2, z2] = pr(p2);
let [pr3, z3] = pr(p3);
let [pr4, z4] = pr(p4);
// Whole face not in view? Skip.
let [l, r, t, b] = getBounds([pr1, pr2, pr3, pr4]);
if (r < 0 || l > w || b < 0 || t > h) return;
// When looking at front, pr1-pr2 is TL -> BL
// pr3-pr4 is TR -> BR
// gap1 is for horizontal, gap2 is for vertical lines on front
let len1 = (pr1.subtract(pr2).length + pr3.subtract(pr4).length) / 2;
let len2 = (pr1.subtract(pr3).length + pr2.subtract(pr4).length) / 2;
let n1 = Math.round(len1 / gap1);
let n2 = Math.round(len2 / gap2);
// Horizontal lines
for (let i = 0; i <= n1; ++i) {
let r = i / n1;
faceLines.push(new FilterableLine(
pr1.add(pr2.subtract(pr1).multiply(r)),
pr3.add(pr4.subtract(pr3).multiply(r)),
clr, clr));
}
// Vertical lines
for (let i = 0; i <= n2; ++i) {
let r = i / n2;
faceLines.push(new FilterableLine(
pr1.add(pr3.subtract(pr1).multiply(r)),
pr2.add(pr4.subtract(pr2).multiply(r)),
clr, clr));
}
}
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 to8bit(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 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);
}