Source code of plot #061 back to plot
Download full working sketch as 061.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.
import * as E from "./lib/env.js"
import {caption} from "./lib/own/caption.js"
import {mulberry32, setRandomGenerator, rand, randn_bm, rand_select} from "./lib/own/random.js"
import * as G from "./lib/own/geo2.js"
import {kdTree} from "./lib/thirdparty/kdTree.js"
// Declarations below instruct build plugin to copy static files to runtime dir
// STATIC lib/texture.png
const pw = 1480; // Paper width
const ph = 2100; // Paper height
const w = 1050; // Drawing width
const h = 1480; // Drawing height
let seed = Math.round(Math.random() * 65535);
seed = 13878;
void setup();
async function setup() {
E.initEnv(w, h, pw, ph);
console.log(`Seed: ${seed}`);
E.info("Seed: " + seed);
setRandomGenerator(mulberry32(seed));
const startTime = performance.now();
await E.spin();
await draw();
const elapsed = performance.now() - startTime;
console.log(`Drawn in ${elapsed} msec`);
}
async function draw() {
caption((w+pw)/2, (h+ph)/2, "spiraline", "116a", seed);
const kdt = new kdTree([], (a, b) => Math.sqrt((a.x-b.x)**2 + (a.y-b.y)**2), ["x", "y"]);
const proj = new PerspectiveProjector(Math.PI * 0.2, new G.Vec3(0, 0, 12), w, h);
// Technical params for generation/rendering
const nSpiralSegs = 500;
const minSelfOcclusionDist = 50;
const kdtNearestMaxNodes = 500;
const occlusionBatchSize = 5;
const sparseSegLen = 2;
const minPathLength = 8;
// Composition params
const nSpirals = 200;
const spiralHeight = 5;
const avgRadius = 2.5;
const radVar = 5;
const climbs = [0.5, 1];
const occlusionRadius = 6;
// Generate spirals
const spirals = [];
for (let i = 0; i < nSpirals; ++i) {
const angle = 2 * Math.PI * rand();
const btmRad = randn_bm(avgRadius - radVar, avgRadius + radVar);
const climb = rand_select(climbs);
const pts3D = getSpiralPoints(angle, btmRad, spiralHeight, climb, nSpiralSegs);
const ptsProj = pts3D.map(pt => proj.project(pt));
const spiral = {pts3D, ptsProj};
spirals.push(spiral);
for (let j = 0; j < ptsProj.length; ++j) {
const pt = ptsProj[j];
pt.spiral = spiral;
pt.ixInSpiral = j;
kdt.insert(pt);
}
}
// Render with occlusion
for (let i = 0; i < spirals.length; ++i) {
if ((i%occlusionBatchSize) == 0) await E.spin();
const spiral = spirals[i];
let visiblePath = [];
let currLen = 0;
for (const pt of spiral.ptsProj) {
let hasOccluder = false;
const neighbors = kdt.nearest(pt, kdtNearestMaxNodes, occlusionRadius);
for (const nb of neighbors) {
const npt = nb[0];
if (npt.spiral === spiral && Math.abs(npt.ixInSpiral-pt.ixInSpiral) < minSelfOcclusionDist)
continue;
if (nb[0].z < pt.z) {
hasOccluder = true;
break;
}
}
if (hasOccluder) {
// TODO: integrate actual path length
if (currLen > minPathLength) makeSparseAndAdd(visiblePath);
visiblePath = [];
currLen = 0;
}
else {
if (visiblePath.length > 0) currLen += G.dist2(visiblePath[visiblePath.length-1], pt);
visiblePath.push(pt);
}
}
if (currLen > minPathLength) makeSparseAndAdd(visiblePath);
}
function makeSparseAndAdd(pts) {
const pts2 = sparser(pts, sparseSegLen);
E.addPath(pts2);
}
}
function getSpiralPoints(btmAngle, btmRadius, height, climb, nSegs) {
const points = [];
for (let i = 0; i <= nSegs; ++i) {
const t = i / nSegs; // [0, 1]
// If climb is 1, two full twists. More climb means less twists.
const twistAngle = t * 2 * Math.PI / climb;
const x = btmRadius * Math.sin(btmAngle + twistAngle);
const z = btmRadius * Math.cos(btmAngle + twistAngle);
const y = (t - 0.5) * height;
points.push(new G.Vec3(x, y, z));
}
return points;
}
function sparser(pts, minLen) {
const res = [pts[0]];
let travel = 0;
for (let i = 1; i < pts.length; ++i) {
travel += G.dist2(pts[i], pts[i-1]);
if (travel >= minLen || i == pts.length - 1) {
res.push(pts[i]);
travel = 0;
}
}
return res;
}
class PerspectiveProjector {
/**
* Initializes a new projector.
* @param {Number} fov Field of view
* @param {G.Vec3} camPos The camera's position. Camera always points at origin.
* @param {Number} canvasWidth Projection canvas width.
* @param {Number} canvasHeight Projection canvas height.
*/
constructor(fov, camPos, canvasWidth, canvasHeight) {
this.fov = fov;
this.fovFactor = Math.tan(fov / 2);
this.camPos = camPos;
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
this.aspect = canvasWidth / canvasHeight;
}
/**
* Projects a 3D point onto the canvas.
* @param {G.Vec3} pt The point to project.
* @param {G.Vec3} res The vector to receive the result. X and Y are canvas coordinates; Z is pt's distance from the camera.
* @returns {G.Vec3} The vector that was passed in as 'res', updated with the projection.
*/
project(pt, res = new G.Vec3()) {
const distance = Math.sqrt(
(pt.x - this.camPos.x) ** 2 +
(pt.y - this.camPos.y) ** 2 +
(pt.z - this.camPos.z) ** 2
);
const x = (pt.x - this.camPos.x) / (pt.z - this.camPos.z) / this.fovFactor * this.aspect;
const y = (pt.y - this.camPos.y) / (pt.z - this.camPos.z) / this.fovFactor;
res.set((x + 1) * this.canvasWidth / 2, (1 - y) * this.canvasHeight / 2, distance);
return res;
}
}