Source code of plot #037 back to plot
Download full working sketch as 037.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 = 1480; // Paper width
const ph = 1050; // Paper height
const w = 1480; // Drawing width
const h = 1050; // Drawing height
const margin = 50;
let camFOV = 60; // Cam field of view in degrees
let camAltitude = -20; // Cam altitude in degrees
let camAzimuth = 18; // Cam azimuth in degrees; front (from Z) is 0, goes CCW from Y top
let camDistance = w * 0.6; // Cam distance from origin
let camOverride = {
// position: { x: 0.0003603302135864198, y: 360.31643911669613, z: 0.0000012709263311965863 },
// target: { x: 0, y: 0, z: 0 },
position: { x: -163.43070919990362, y: 350.5107840366473, z: -565.6917517236838 },
target: { x: -78.00868657771106, y: 106.26965243350297, z: -314.9383190807513 },
}
let sceneBgColor = "black";
let controls = false;
// 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;
let colorer;
let seed; // Random seed
if (window.fxhash) seed = Math.round(fxrand() * 65535);
else seed = Math.round(Math.random() * 65535);
seed = 13094;
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);
colorer = new Colorer(17, 17, 27);
let boxes = [];
const nHoriz = 13;
const jointDist = w * 0.1;
const jointSz = w * 0.02;
const barSz = w * 0.01;
const wf = false;
let jointGeo = new THREE.BoxGeometry(jointSz, jointSz, jointSz).toNonIndexed();
let barXGeo = new THREE.BoxGeometry(jointDist - jointSz - 2, barSz, barSz).toNonIndexed();
let barYGeo = new THREE.BoxGeometry(barSz, jointDist - jointSz - 2, barSz).toNonIndexed();
let barZGeo = new THREE.BoxGeometry(barSz, barSz, jointDist - jointSz - 2).toNonIndexed();
let mat = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors, side: THREE.DoubleSide});
for (let xIx = 0; xIx < nHoriz; ++xIx) {
const xPos = -(nHoriz / 2) * jointDist + xIx * jointDist;
for (let yIx = 0; yIx < nHoriz; ++yIx) {
const yPos = -(nHoriz / 2) * jointDist + yIx * jointDist;
for (let zIx = 0; zIx < nHoriz; ++zIx) {
const zPos = -(nHoriz / 2) * jointDist + zIx * jointDist;
let geo = jointGeo.clone();
colorer.colorFaces(geo);
let jointMesh = new THREE.Mesh(geo, mat);
if (wf) addWF(jointMesh, jointGeo);
jointMesh.position.set(xPos, yPos, zPos);
scene.add(jointMesh);
boxes.push(jointMesh);
geo = barXGeo.clone();
colorer.colorFaces(geo);
let barXMesh = new THREE.Mesh(geo, mat);
if (wf) addWF(barXMesh, barXGeo);
barXMesh.position.set(xPos - jointDist / 2, yPos, zPos);
scene.add(barXMesh);
boxes.push(barXMesh);
geo = barYGeo.clone();
colorer.colorFaces(geo);
let barYMesh = new THREE.Mesh(geo, mat);
if (wf) addWF(barYMesh, barYGeo);
barYMesh.position.set(xPos, yPos - jointDist / 2, zPos);
scene.add(barYMesh);
boxes.push(barYMesh);
geo = barZGeo.clone();
colorer.colorFaces(geo);
let barZMesh = new THREE.Mesh(geo, mat);
if (wf) addWF(barZMesh, barZGeo);
barZMesh.position.set(xPos, yPos, zPos - jointDist / 2);
scene.add(barZMesh);
boxes.push(barZMesh);
}
}
}
renderer.render(scene, cam);
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);
let allEdges = [];
for (let i = boxes.length - 1; i >= 0; --i) {
const box = boxes[i];
let edges = getBoxEdges(box);
allEdges.push(...edges);
}
// dload();
// return;
// Line hiding for 36819 edges, testing 154,933,630 points
// Keep 3375 lines.
// With allocations in get3JSMaskedLine: 50 sec
// Eliminated allocations get3JSMaskedLineNoAlloc: 16 sec
// Same thing in Go: 2.1 sec
console.log("Calculating line hiding for " + allEdges.length + " edges");
let calcStart = new Date();
let visibleLines = [];
// Get visible lines by probing pixels from JS
// ============================================
// for (let i = 0; i < allEdges.length; ++i) {
// const edge = allEdges[i];
// let viss = get3JSMaskedLineNoAlloc(edge.pt1, edge.pt2, pixels, edge.clr1, edge.clr2, true);
// visibleLines.push(...viss);
// }
// ============================================
// Get visible lines through shader approach
// ============================================
const wcm = new WebGLCanvasMasker(pixels, w, h, rf);
visibleLines = wcm.mask(allEdges, null, segLen);
// ============================================
let elapsed = new Date() - calcStart;
console.log("Tested " + testedPoints + " points");
console.log("Kept " + visibleLines.length + " lines");
printTime(elapsed);
for (const vl of visibleLines) {
let ln = Path.Line(vl[0], vl[1]);
project.activeLayer.addChild(ln);
}
function dload() {
let elmInfo = document.getElementById("info");
let edgesTxt = "";
for (const edge of allEdges) {
let txt = edge.pt1.x + " " + edge.pt1.y + " " + edge.pt2.x + " " + edge.pt2.y;
txt += " " + edge.clr1.r + " " + edge.clr1.g + " " + edge.clr1.b;
txt += " " + edge.clr2.r + " " + edge.clr2.g + " " + edge.clr2.b;
edgesTxt += txt + "\n";
}
const elmDlLines = document.createElement("a");
elmDlLines.text = "lines";
elmDlLines.style.marginRight = "100px";
elmInfo.appendChild(elmDlLines);
const linesFile = new File([edgesTxt], "lines.txt", {type: 'text/plain'});
elmDlLines.href = URL.createObjectURL(linesFile);
elmDlLines.download = "lines.txt";
let pixelsTxt = "";
let count = 0;
for (const px of pixels) {
if (count % (w * rf * 4) == 0) {
if (count != 0) pixelsTxt += "\n";
}
else pixelsTxt += " ";
pixelsTxt += px;
++count;
}
const elmDlPixels = document.createElement("a");
elmDlPixels.text = "pixels";
elmDlPixels.style.marginRight = "150px";
elmInfo.appendChild(elmDlPixels);
const pixelsFile = new File([pixelsTxt], "pixels.txt", {type: 'text/plain'});
elmDlPixels.href = URL.createObjectURL(pixelsFile);
elmDlPixels.download = "pixels.txt";
}
dload();
if (controls) {
controls = new OrbitControls(cam, renderer.domElement);
requestAnimationFrame(animate);
}
}
function animate() {
controls.update();
renderer.render(scene, cam);
requestAnimationFrame(animate);
}
function printTime(msec) {
let sec = Math.floor(msec / 1000);
let ms = msec - sec * 1000;
console.log("Elapsed: " + sec.toString() + "." + ms.toString());
}
class BoxEdge {
constructor(pt1, pt2, clr1, clr2) {
this.pt1 = pt1;
this.pt2 = pt2;
this.clr1 = clr1;
this.clr2 = clr2;
}
}
function getBoxEdges(mesh) {
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
let edges = [];
// Front top
addEdgeIfInView(tlf, trf, clrFront, clrTop);
// Front bottom
addEdgeIfInView(blf, brf, clrFront, clrBottom);
// Front left
addEdgeIfInView(tlf, blf, clrFront, clrLeft);
// Front right
addEdgeIfInView(trf, brf, clrFront, clrRight);
// Back top
addEdgeIfInView(tlb, trb, clrBack, clrTop);
// Back bottom
addEdgeIfInView(blb, brb, clrBack, clrBottom);
// Back left
addEdgeIfInView(tlb, blb, clrBack, clrLeft);
// Back right
addEdgeIfInView(trb, brb, clrBack, clrRight);
// Top left dept
addEdgeIfInView(tlf, tlb, clrTop, clrLeft);
// Top right dep
addEdgeIfInView(trf, trb, clrTop, clrRight);
// Bottom left dh
addEdgeIfInView(blf, blb, clrBottom, clrLeft);
// Bottom right th
addEdgeIfInView(brf, brb, clrBottom, clrRight);
return edges;
function addEdgeIfInView(vert1, vert2, clr1, clr2) {
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;
edges.push(new BoxEdge(pt1, pt2, clr1, clr2));
}
function getBounds(pt1, pt2) {
let left = Math.min(pt1.x, pt2.x);
let right = Math.max(pt1.x, pt2.x);
let top = Math.min(pt1.y, pt2.y);
let bottom = Math.max(pt1.y, pt2.y);
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 and masking
// ===========================================================================
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(camFOV, asprat, 1, 40000);
if (!camOverride) {
let camPos = new Vector3(
Math.sin(camAzimuth * Math.PI / 180) * Math.cos(camAltitude * Math.PI / 180),
Math.sin(camAltitude * Math.PI / 180),
Math.cos(camAzimuth * Math.PI / 180) * Math.cos(camAltitude * Math.PI / 180),
);
camPos.multiplyScalar(camDistance);
cam.position.set(camPos.x, camPos.y, camPos.z);
cam.lookAt(0, 0, 0);
// cam.setViewOffset(w, h, 0, camViewOfsY, w, h);
cam.updateProjectionMatrix();
}
else {
cam.position.set(camOverride.position.x, camOverride.position.y, camOverride.position.z);
cam.lookAt(camOverride.target.x, camOverride.target.y, camOverride.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 traceEdge(pt1, pt2, mesh1, mesh2) {
let visibles = [], hiddens = [];
// Get all meshes in scene: we'll be testing for intersection with them
const meshes = [];
scene.traverse(obj => {
if (obj.isMesh) meshes.push(obj);
});
// Build points: short segments of the requested length
const lineVect = pt2.subtract(pt1);
const lineLength = lineVect.length;
const nSegs = Math.max(2, Math.round(lineLength / segLen));
const segVect = lineVect.divide(nSegs);
const pts = [];
for (let i = 0; i <= nSegs; ++i) {
pts.push(pt1.add(segVect.multiply(i)));
}
let orto = pt2.subtract(pt1).rotate(90);
orto.length = 1;
let firstVisible = null, firstHidden = null;
for (let i = 0; i < pts.length; ++i) {
let isVisible = false;
let pt = pts[i];
let ptl = pt.add(orto);
let ptr = pt.subtract(orto);
// Trace sample point itself
let [nearerFaceIsect, nearestDist] = trace(pt);
// No intersection with ANYTHING in scene: visible
if (nearestDist == Number.MAX_VALUE) {
isVisible = true;
}
// Intersects with one of the sides
else if (nearerFaceIsect != null) {
isVisible = nearerFaceIsect.distance == nearestDist;
}
// Does not intersect with either side: test points off to left and right
else {
[nearerFaceIsect, nearestDist] = trace(ptl);
if (nearerFaceIsect != null) {
isVisible = nearerFaceIsect.distance == nearestDist;
} else {
[nearerFaceIsect, nearestDist] = trace(ptr);
if (nearerFaceIsect != null) {
isVisible = nearerFaceIsect.distance == nearestDist;
} else isVisible = false;
}
}
if (isVisible && firstVisible == null) firstVisible = pts[i];
else if (!isVisible && firstVisible != null) {
if (firstVisible != pts[i - 1]) visibles.push([firstVisible, pts[i - 1]]);
firstVisible = null;
}
if (!isVisible && firstHidden == null) firstHidden = pts[i];
else if (isVisible && firstHidden != null) {
if (firstHidden != pts[i - 1]) hiddens.push([firstHidden, pts[i - 1]]);
firstHidden = null;
}
}
if (firstVisible != null && firstVisible != pts[pts.length - 1]) visibles.push([firstVisible, pts[pts.length - 1]]);
if (firstHidden != null && firstHidden != pts[pts.length - 1]) hiddens.push([firstHidden, pts[pts.length - 1]]);
return [visibles, hiddens];
function trace(pt) {
let vec2 = new THREE.Vector2(pt.x / w * 2 - 1, -pt.y / h * 2 + 1);
ray.setFromCamera(vec2, cam);
let isects = ray.intersectObjects(meshes, false);
let nearerMeshIsect = null;
let nearestDist = Number.MAX_VALUE;
isects.forEach(i => {
if (i.distance < nearestDist) nearestDist = i.distance;
if (i.object != mesh1 && i.object != mesh2) return;
if (nearerMeshIsect == null || i.distance < nearerMeshIsect.distance) nearerMeshIsect = i;
});
return [nearerMeshIsect, nearestDist];
}
}
let testedPoints = 0;
function get3JSMaskedLineNoAlloc(pt1, pt2, pixels, clr1, clr2, lookAside) {
const deltaX = pt2.x - pt1.x;
const deltaY = pt2.y - pt1.y;
const lineLength = Math.sqrt(deltaX ** 2 + deltaY ** 2);
const nSegs = Math.max(2, Math.round(lineLength / segLen));
testedPoints += nSegs + 1;
let orto = pt2.subtract(pt1).rotate(90);
orto.length = 1;
let visibleLines = [];
let firstVisible = null;
let pt = new Point();
let prevPt = new Point();
for (let i = 0; i <= nSegs; ++i) {
pt.x = pt1.x + i / nSegs * deltaX;
pt.y = pt1.y + i / nSegs * deltaY;
let clrHere = getPixel(pixels, pt.x, pt.y);
let isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
if (!isVisible && lookAside) {
clrHere = getPixel(pixels, pt.x + orto.x, pt.y + orto.y);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (!isVisible && lookAside) {
clrHere = getPixel(pixels, pt.x - orto.x, pt.y - orto.y);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (isVisible && firstVisible == null) {
firstVisible = pt.clone();
}
else if (!isVisible && firstVisible != null) {
if (!firstVisible.equals(prevPt)) visibleLines.push([firstVisible, prevPt.clone()]);
firstVisible = null;
}
prevPt.x = pt.x;
prevPt.y = pt.y;
}
if (firstVisible != null && !firstVisible.equals(pt2)) visibleLines.push([firstVisible, pt2.clone()]);
return visibleLines;
}
function get3JSMaskedLine(pt1, pt2, pixels, clr1, clr2, lookAside) {
// Build points: short segments of the requested length
const lineVect = pt2.subtract(pt1);
const lineLength = lineVect.length;
const nSegs = Math.max(2, Math.round(lineLength / segLen));
const segVect = lineVect.divide(nSegs);
const pts = [];
for (let i = 0; i <= nSegs; ++i) {
pts.push(pt1.add(segVect.multiply(i)));
}
let orto = pt2.subtract(pt1).rotate(90);
orto.length = 1;
let visibleLines = [];
let firstVisible = null
for (let i = 0; i < pts.length; ++i) {
let pt = pts[i];
let clrHere = getPixel(pixels, pt.x, pt.y);
let isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
if (!isVisible && lookAside) {
clrHere = getPixel(pixels, pt.x + orto.x, pt.y + orto.y);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (!isVisible && lookAside) {
clrHere = getPixel(pixels, pt.x - orto.x, pt.y - orto.y);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (isVisible && firstVisible == null) firstVisible = pts[i];
else if (!isVisible && firstVisible != null) {
if (firstVisible != pts[i - 1]) visibleLines.push([firstVisible, pts[i - 1]]);
firstVisible = null;
}
}
if (firstVisible != null && firstVisible != pts[pts.length - 1]) visibleLines.push([firstVisible, pts[pts.length - 1]]);
return visibleLines;
}
function getMaskedEdge(pt1, pt2, pixels, clr1, clr2) {
// Build points: short segments of the requested length
const lineVect = pt2.subtract(pt1);
const lineLength = lineVect.length;
const nSegs = Math.max(2, Math.round(lineLength / segLen));
const segVect = lineVect.divide(nSegs);
const pts = [];
for (let i = 0; i <= nSegs; ++i) {
pts.push(pt1.add(segVect.multiply(i)));
}
let orto = pt2.subtract(pt1).rotate(90);
orto.length = 1;
let visibles = [], hiddens = [];
let firstVisible = null, firstHidden = null;
for (let i = 0; i < pts.length; ++i) {
let sidePts = [];
for (let j = 1; j <= 2; ++j) {
sidePts.push(pts[i].add(orto.multiply(j)));
sidePts.push(pts[i].subtract(orto.multiply(j)));
}
let clrs = [];
sidePts.forEach(pt => clrs.push(getPixel(pixels, pt.x, pt.y)));
let isVisible = false;
clrs.forEach(clr => {
if (clrEq(clr, clr1)) isVisible = true;
if (clrEq(clr, clr2)) isVisible = true;
})
if (isVisible && firstVisible == null) firstVisible = pts[i];
else if (!isVisible && firstVisible != null) {
if (firstVisible != pts[i - 1]) visibles.push([firstVisible, pts[i - 1]]);
firstVisible = null;
}
if (!isVisible && firstHidden == null) firstHidden = pts[i];
else if (isVisible && firstHidden != null) {
if (firstHidden != pts[i - 1]) hiddens.push([firstHidden, pts[i - 1]]);
firstHidden = null;
}
}
if (firstVisible != null && firstVisible != pts[pts.length - 1]) visibles.push([firstVisible, pts[pts.length - 1]]);
if (firstHidden != null && firstHidden != pts[pts.length - 1]) hiddens.push([firstHidden, pts[pts.length - 1]]);
return [visibles, hiddens];
}
function clrEq(a, b) {
// return a.r == b.r && a.g == b.g && a.b == b.b;
let eq = Math.abs(a.r - b.r) <= 1;
eq &= Math.abs(a.g - b.g) <= 1;
eq &= Math.abs(a.b - b.b) <= 1;
return eq;
}
function getPixel(pixels, x, y) {
x = Math.round(x * rf);
y = h * rf - Math.round(y * rf);
if (x < 0 || x >= w * rf || y < 0 || y >= h * rf)
return { r: 0, g: 0, b: 0 };
let ix = (y * w * rf + x) * 4;
let clr = {
r: pixels[ix],
g: pixels[ix + 1],
b: pixels[ix + 2],
}
return clr;
}
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);
}