import { SVG } from 'https://cdn.skypack.dev/@svgdotjs/svg.js' import { Vec2 } from 'https://cdn.skypack.dev/wtc-math'; import fitCurve from 'https://cdn.skypack.dev/fit-curves'; console.clear(); const config = { seed: 1337, drawingType: 1, dimensions: new Vec2(700, 700), nscale: .00125, sscale: 20, stepSize: 25, num: 1, r: 5, rigidity: 0, k: 8, testGridSize: 1, offset: new Vec2(10,-200), sneks: 200 }; const vars = { noise: null, grid: null, drawing: null } const setup = () => { const container = document.querySelector('#container'); config.offset.x = floatRandomBetween(-1000, 1000); config.offset.y = floatRandomBetween(-1000, 1000); config.nscale = floatRandomBetween(.000005, .002); container.innerHTML = ''; vars.drawing = new Drawing(config.drawingType).addTo('#container').size(config.dimensions); const t = document.createElement('div') t.className = 'tracer'; container.appendChild(t) vars.bluenoise = new BlueNoise({ size: config.dimensions.addNew(new Vec2(config.r*-1, config.r*-1)), offset: new Vec2(config.r*1, config.r*1), r: config.r, k: config.k, rigidity: config.rigidity }); vars.noise = new SimplexNoise(config.seed); vars.grid = new Grid({ cellSize: new Vec2(config.testGridSize,config.testGridSize), fill: -1 }); /// Create the download button // const dl = document.createElement('button'); // dl.innerText = 'download'; // dl.addEventListener('click', () => { // vars.drawing.download(); // }); // container.appendChild(dl); document.body.querySelector('#container>:first-child').addEventListener('click', () => { setup(); }); draw(); drawStep(); } let sneki = 0; const drawStep = () => { const sneks = []; if(vars.bluenoise.news.length > 0) { requestAnimationFrame(drawStep) for(let i = 0; i < config.sneks; i++) { const [p] = vars.bluenoise.news.splice(Math.floor(Math.random() * vars.bluenoise.news.length), 1.); if(!p) continue; if(p.subtractNew(config.dimensions.scaleNew(.5)).length > 200) continue; const pos = p; const dir = new Vec2(1,0); const f = field(pos, 1., sneki+1); dir.angle = f.noise; // vars.drawing.stroke = '#ff0000AA'; // vars.drawing.circle(pos, 1); const distancebreak = 3; let cont = false; for(let x = -distancebreak*.5; x <= distancebreak*.5; x++) { for(let y = -distancebreak*.5; y <= distancebreak*.5; y++) { const offset = new Vec2(x, y); const t = vars.grid.getChildAtPosition(pos.addNew(offset)); if(t !== -1) cont = true; } } if(cont) continue; // vars.drawing.circle(pos, .5); const s = new Snek(pos, { direction: dir, maxLength: 1000, id: sneki++, distanceProjection: 5, distanceBreak: distancebreak, projectionBreakMultiplier: 0 }); sneks.push(s); // projectionBreakMultiplier: 2 // } } // for(let j = 0; j < config.lineLength; j++) { for(let i = 0; i < sneks.length; i++) { sneks[i].walkOut(); } // } // document.querySelector('.tracer').innerHTML = vars.bluenoise.news.length; for(let i = 0; i < sneks.length; i++) { vars.drawing.path(sneks[i].bezier); } } let interval; const draw = () => { vars.drawing.stroke = '#AAA'; vars.drawing.c.fillStyle = "#111122"; vars.drawing.c.fillRect(0,0, config.dimensions.x, config.dimensions.y); vars.drawing.rect(new Vec2(0,0), config.dimensions); vars.drawing.c.lineWidth = 1.5; while(vars.bluenoise.active.length > 0) { vars.bluenoise.step(); } } setTimeout(() => { setup(); }, 500); const ngon = (pos, r, nsides) => { const a = Math.atan2(pos.y, pos.x) + Math.PI*.5; const split = (Math.PI*2) / nsides; return pos.length * Math.cos(split * Math.floor(.5 + a / split) - a) - r; } const field = (pos, id = 1, instanceID = 0)=> { const normpos = pos.subtractNew(config.dimensions.scaleNew(.5)); const mask1 = ngon(normpos, 200, 40) < Math.random()*10; const mask2 = ngon(normpos.addNew(new Vec2(0, 50)), 250, 3) < 0.; let mask; if(!id) { if(mask2) { id = 2.; } else if(mask1) { id = 1.; } } let n; if(id == 1.) { n = vars.noise.noise2D(pos.addNew(config.offset).scale(config.nscale).array)*Math.PI; // const a = Math.atan2(normpos.y, normpos.x); // n = a - .5; } else if(id == 2.) { // const a = Math.atan2(normpos.y, normpos.x) + Math.PI * .5; // n = a + .8; const a = Math.atan2(normpos.y, normpos.x); n = a + .5; // n = noise.noise2D(pos.addNew(config.offset).scale(config.nscale).array)*Math.PI; } // let apos = pos.subtractNew(dimensions.scaleNew(.5)); // let a = Math.atan2(apos.y, apos.x) + Math.PI * .5; // const n = noise.noise2D(pos.addNew(config.offset).scale(config.nscale).array)*Math.PI; // const n = a + .8; // const mask = pos.subtractNew(dimensions.scaleNew(.5)).length < 350; // const mask = ngon(normpos, 150, 3) < 0.; return {noise: n, mask: instanceID % 3 == 0 ? true : mask1, id: id}; } class Snek { static #defaults = { grid: vars.grid, head: true, tail: true, direction: new Vec2(1,0), maxLength: 1000, stepSize: 1, fieldID: 1, distanceBreak: 5, distanceProjection: 5, minLength: 10, id: 0, bailout: 10000, projectionBreakMultiplier: .5 }; #alive = [true, true]; /* Two alive directions per snek */ #directions = [0,0]; /* The direction of the head and tail */ #positions = [0,0]; /* The position of the head and tail */ #maxLength = 1000; /* The maximum length of the whole snake */ #stepSize = 1; #fieldID = 1; #distanceBreak = 5; #distanceProjection = 5; #minLength = 5; #points = []; #id = 0; #bailout = 10000; #projectionBreakMultiplier = .5; constructor(pos, settings) { settings = Object.assign({}, Snek.#defaults, settings); this.#alive[0] = settings.head === true; this.#alive[1] = settings.tail === true; this.directionHead = settings.direction.clone(); this.directionTail = settings.direction.rotateNew(Math.PI); this.#positions[0] = pos.clone(); this.#positions[1] = pos.clone(); this.#fieldID = settings.fieldID; this.#distanceBreak = settings.distanceBreak; this.#distanceProjection = settings.distanceProjection; this.#maxLength = settings.maxLength; this.#minLength = settings.minLength; this.#id = settings.id; this.#bailout = settings.bailout; this.#projectionBreakMultiplier = settings.projectionBreakMultiplier; } walkOut() { // console.log(this.length, this.#maxLength) let i = 0; while(this.length < this.#maxLength) { if(i++ > this.#bailout) break; if(this.dead) break; this.walk(this.#stepSize); } this.dead = true; } walk(distance, topTail = 0) { if(this.#alive[topTail]) { const dir = this.#directions[topTail]; const pos = this.#positions[topTail].addNew(dir.scaleNew(distance)); const f = field(pos, this.#fieldID, this.#id); const t = vars.grid.getChildAtPosition(pos); let draw = true; let i = -this.#distanceBreak; let a = dir.clone(); a.angle -= Math.PI * .5; while(i <= this.#distanceBreak) { const p = pos.addNew(a.scaleNew(i)); const t = vars.grid.getChildAtPosition(p); if(t === -1 || t === this) { } else { draw = false; break; } i += config.testGridSize; } const dp = this.#distanceProjection; let np = pos.clone(); const ndir = dir.clone(); for(let i = 0; i < dp; i++) { if(draw === false) break; np.add(ndir.scaleNew(distance)); const nf = field(np, this.#fieldID, this.#id); ndir.angle = nf.noise; let j = -this.#distanceBreak*this.#projectionBreakMultiplier; let a = ndir.clone(); a.angle -= Math.PI * .5; while(j <= this.#distanceBreak*this.#projectionBreakMultiplier) { const p = np.addNew(a.scaleNew(j)); const t = vars.grid.getChildAtPosition(p); if(t === -1 || t === this) { } else { draw = false; break; } j += config.testGridSize; } } if(!f.mask) draw = false; if(draw) { if(topTail === 0) { this.#points.push(pos.clone()); } else { this.#points.splice(0, 0, pos.clone()); } vars.grid.addChildAtPosition(this, pos); this.#positions[topTail] = pos.clone(); } else { this.#alive[topTail] = false; } dir.angle = f.noise; if(topTail === 1) dir.rotate(Math.PI); } if(topTail === 0 && this.#alive[1]) { this.walk(distance, 1) } } get length() { return this.#points.length * this.#stepSize; } set dead(v) { if(v === false) { this.#alive[0] = false; this.#alive[1] = false; } else if(v === true) { this.#alive[0] = true; } } get dead() { return !this.#alive[0] && !this.#alive[1]; } get points() { let points = this.#points; if(points.length * this.#stepSize < this.#minLength) { points.forEach((p) => { vars.grid.addChildAtPosition(-1, p); }); return null; } return points.map(p => p.array); } get bezier() { const points = this.points; if(!points?.length || points.length <= 1) return; const bezierCurves = fitCurve(this.points, 1); let str = ""; bezierCurves.map(function(bezier, i) { if (i == 0) { str += "M " + bezier[0][0] + " " + bezier[0][1]; } str += "C " + bezier[1][0] + " " + bezier[1][1] + ", " + bezier[2][0] + " " + bezier[2][1] + ", " + bezier[3][0] + " " + bezier[3][1] + " "; }); return str; } get id() { return this.#fieldID; } set directionHead(v) { if(v instanceof Vec2) this.#directions[0] = v; } get directionHead() { return this.#directions[0]; } set directionTail(v) { if(v instanceof Vec2) this.#directions[1] = v; } get directionTail() { return this.#directions[1]; } } class Drawing { static DT_CANVAS = 1; static DT_SVG = 2; #drawing; #ctx; #mode; #instructions = []; constructor(mode = Drawing.DT_CANVAS) { this.mode = mode; if(this.mode & Drawing.DT_CANVAS) { this.#drawing = document.createElement('canvas'); } else if(this.mode & Drawing.DT_SVG) { this.#drawing = SVG(); } this.stroke = '#333'; } clear() { if(this.mode & Drawing.DT_CANVAS) { this.c.clearRect(0,0,...this.dimensions.array); } else if(this.mode & Drawing.DT_SVG) { this.drawing.clear(); } } rect(position, dimensions) { if(this.saving) { this.#instructions.push({ f: 'rect', args: [position, dimensions] }); } if(this.mode & Drawing.DT_CANVAS) { this.c.beginPath(); this.c.rect(...position.array, ...dimensions.array); this.c.stroke(); } else if(this.mode & Drawing.DT_SVG) { this.drawing.rect(dimensions.width, dimensions.height).move(...position.array).fill("none").stroke('#f06'); } } circle(position, radius) { if(this.saving) { this.#instructions.push({ f: 'circle', args: [position, radius] }); } if(this.mode & Drawing.DT_CANVAS) { this.c.beginPath(); this.c.arc(position.x, position.y, radius, 0, 2 * Math.PI); if(this.stroke) this.c.stroke(); } else if(this.mode & Drawing.DT_SVG) { this.drawing.circle(radius*2).fill("none").stroke(this.stroke).move(...position.subtractScalarNew(radius).array); } } line(a, b) { if(this.saving) { this.#instructions.push({ f: 'line', args: [a, b] }); } if(this.mode & Drawing.DT_CANVAS) { this.c.beginPath(); this.c.moveTo(a.x, a.y); this.c.lineTo(b.x, b.y); if(this.stroke) this.c.stroke(); } else if(this.mode & Drawing.DT_SVG) { this.drawing.line(...a.array, ...b.array).stroke(this.stroke); } } polyline(points) { if(this.saving) { this.#instructions.push({ f: 'polyline', args: points }); } if(this.mode & Drawing.DT_CANVAS) { this.c.beginPath(); points.forEach((p, i) => { if(i === 0) this.c.moveTo(p.x, p.y); else this.c.lineTo(p.x, p.y); }) if(this.stroke) this.c.stroke(); } else if(this.mode & Drawing.DT_SVG) { this.drawing.polyline(points.map(p => p.array)).fill('none').stroke(this.stroke); } } path(path) { if(this.mode & Drawing.DT_CANVAS) { this.c.beginPath(); const p = new Path2D(path); if(this.stroke) this.c.stroke(p); } else if(this.mode & Drawing.DT_SVG) { this.drawing.path(path).fill('none').stroke(this.stroke); } } polygon(points) { if(this.saving) { this.#instructions.push({ f: 'polygon', args: [points] }); } if(this.mode & Drawing.DT_CANVAS) { this.c.beginPath(); points.forEach((p, i) => { if(i === 0) this.c.moveTo(p.x, p.y); else this.c.lineTo(p.x, p.y); }) if(this.stroke) this.c.stroke(); } else if(this.mode & Drawing.DT_SVG) { this.drawing.polygon(points.map(p => p.array)).fill('none').stroke(this.stroke); } } download() { let d; if(this.mode & Drawing.DT_CANVAS) { d = new Drawing(Drawing.DT_SVG).size(this.dimensions); this.#instructions.forEach((i) => { d[i.f](...i.args); }); } else if(this.mode & Drawing.DT_SVG) { d = this; } const fileName = "untitled.svg" const url = "data:image/svg+xml;utf8," + encodeURIComponent(d.drawing.svg()); const link = document.createElement("a"); link.download = fileName; link.href = url; link.click(); } addTo(element) { if(typeof(element) === 'string') { if(this.mode & Drawing.DT_CANVAS) { document.body.querySelector(element).appendChild(this.drawing); } else if(this.mode & Drawing.DT_SVG) { this.drawing.addTo('#container'); } } return this; } size(d) { if(this.mode & Drawing.DT_CANVAS) { this.drawing.width = d.width; this.drawing.height = d.height; } else if(this.mode & Drawing.DT_SVG) { this.drawing.size(...d.array); } this.#dimensions = d; return this; } #dimensions set dimensions(v) { if(v instanceof Vec2) { this.#dimensions = v; this.size(v); } } get dimensions() { return this.#dimensions; } get drawing() { return this.#drawing; } get c() { if(this.mode & Drawing.DT_CANVAS) { if(this.#ctx) return this.#ctx; this.#ctx = this.drawing.getContext('2d'); return this.#ctx; } } #stroke; set stroke(v) { this.#stroke = v; if(this.mode & Drawing.DT_CANVAS) { this.c.strokeStyle = v; } } get stroke() { return this.#stroke; } set mode(v) { if(v & (Drawing.DT_CANVAS | Drawing.DT_SVG)) { this.#mode = v; } } get mode() { return this.#mode || Drawing.DT_CANVAS; } #saving = false set saving(v) { this.#saving = v === true; } get saving() { return this.#saving; } } // vars.drawing = SVG().addTo('#container').size(config.dimensions.x, config.dimensions.y); class Grid { static #defaults = { size: config.dimensions.clone(), cellSize: new Vec2(50,50), fill: null }; #grid = []; #size; #cellSize; constructor(settings) { settings = Object.assign({}, Grid.#defaults, settings); this.#size = Vec2.interpolate(settings.size) || Grid.#defaults.size; this.#cellSize = Vec2.interpolate(settings.cellSize) || Grid.#defaults.cellSize; this.#grid.length = this.gridSize.area; this.#grid.fill(settings.fill || Grid.#defaults.fill); } addChild(child, i) { this.#grid[i] = child; } addChildAtPosition(child, pos) { this.#grid[this.getArrayPosition(pos)] = child; } addChildAtGridPosition(child, gridPos) { this.#grid[this.getArrayPositionFromGridPos(gridPos)] = child; } getChild(i) { return this.#grid[i]; } getChildAtPosition(pos) { return this.#grid[this.getArrayPosition(pos)]; } getChildAtGridPosition(gridPos) { return this.#grid[this.getArrayPositionFromGridPos(gridPos)]; } getGridPositionForIndex(i) { const gsize = this.gridSize; return new Vec2(i%gsize.x, Math.floor(i/gsize.x)); } getArrayPositionFromGridPos(gpos) { const gsize = this.gridSize; if( gpos.x < 0 || gpos.x >= gsize.x || gpos.y < 0 || gpos.y >= gsize.y ) return null; gpos.x = gpos.x % gsize.x; const arraypos = (gpos.x) + (gpos.y*gsize.x); return arraypos; } getArrayPosition(realPos) { const gpos = this.getGridPos(realPos); return this.getArrayPositionFromGridPos(gpos); } getGridPos(realPos) { if(realPos instanceof Vec2) { return realPos.divideNew(this.#cellSize).floor(); } // Throw an error } getSubPos(realPos) { if(realPos instanceof Vec2) { return realPos.modNew(this.#cellSize); } // Throw an error } getRealPos(gridPos) { if(gridPos instanceof Vec2) { return gridPos.multiplyNew(this.#cellSize); } // Throw an error } get size() { return this.#size; } get gridSize() { return this.#size.divideNew(this.#cellSize).floor(); } get cellSize() { return this.#cellSize; } get length() { return this.#grid.length; } } class BlueNoise { static #defaults = { size: config.dimensions.clone(), offset: new Vec2(0,0), r: 100, k: 32, d: 5, initialList: [new Vec2(375,250)], rigidity: 0 }; #activeList = []; #newPositions = []; #grid; #r; #k; #d; #size; #offset; #rigidity; constructor(settings) { settings = Object.assign({}, BlueNoise.#defaults, settings); this.#r = settings.r; this.#k = settings.k; this.#d = settings.d; this.#size = settings.size; this.#offset = settings.offset; this.#rigidity = settings.rigidity; this.#grid = new Grid({ size: this.#size, cellSize: this.#r / Math.SQRT2, // cellSize: 1. / Math.SQRT2 / this.#r, fill: -1 }); this.addElementAtPosition(...settings.initialList); } addElementAtPosition(...positions) { positions.forEach((pos) => { this.#grid.addChildAtPosition(pos, pos); this.#activeList.push(pos); this.#newPositions.push(pos); }); } draw(purge = true) { this.news.forEach((newPos, i) => { const r = 3; const pos = newPos.addNew(this.#offset); vars.drawing.circle(pos, r); if(purge) this.news[i] = null; }); this.#newPositions = this.news.filter(v => v !== null); // this.news.length = 0; } step() { const loopL = Math.min(this.active.length, this.#d); for(let l = 0; l < loopL; l++) { const ri = Math.floor(0, floatRandomBetween(this.active.length)); const c = this.active[ri]; let numfound = 0; for(var i = 0; i < this.#k; i++) { const a = floatRandomBetween(0, Math.PI*2); const l = floatRandomBetween(this.#r, this.#r*2); let pos = new Vec2(Math.cos(a)*l, Math.sin(a)*l).add(c); // console.log(this.grid.getChildAtPosition(pos)); if(this.grid.getChildAtPosition(pos) === -1) { const gridPos = this.grid.getGridPos(pos); let tooClose = false; for (var i = -1; i <= 1; i++) { for (var j = -1; j <= 1; j++) { if(i == 0 && j == 0) continue; const p = this.grid.getChildAtGridPosition(gridPos.addNew(new Vec2(i, j))); if(p !== -1 && p instanceof Vec2) { const d = pos.distance(p); if(d < this.#r*.5) tooClose = true; } } } if(!tooClose) { const gridPosition = this.grid.getRealPos(this.grid.getGridPos(pos)).addNew(this.grid.cellSize.scaleNew(.5)); pos = Vec2.lerp(pos, gridPosition, this.#rigidity); this.grid.addChildAtPosition(pos, pos); this.active.push(pos); this.news.push(pos); numfound++; break; } } } if(numfound === 0) { this.active.splice(ri, 1); } } } get active() { return this.#activeList; } get news() { return this.#newPositions; } get grid() { return this.#grid; } get offset() { return this.#offset; } } const floatRandomBetween = (min, max) => { return Math.random() * (max - min) + min; }; const clamp = function(a, b, v) { return Math.min(b, Math.max(a, v)); } const lerp = function(a, b, progress) { return a + progress * (b - a); } const hash21 = (p) => { const o = p.dot(new Vec2(27.609, 57.583)); return fract(Math.sin(o)*config.seed); } const precisionRound = (n, p) => { const ip = Math.pow(10, p); return Math.round(n*ip)/ip; } const fract = (n, p = 6) => { const ip = Math.pow(10, p); const _n = Math.abs(Math.floor(n*ip)/ip); if(_n < 1) return _n; return Math.floor(_n % Math.floor(_n)*ip)/ip; } const pal = ( t, a, b, c, d ) => { const mp = c.scaleNew(t).add(d).scale(6.28318); mp.x = Math.cos(mp.x); mp.y = Math.cos(mp.y); mp.z = Math.cos(mp.z); return a.addNew(b.multiplyNew(mp)); } const getColour = (d) => { const col = pal( d/70+.65, new Vec3(0.5,0.5,0.5), new Vec3(0.5,0.5,0.5), new Vec3(1.0,1.0,1.0), new Vec3(0.0,0.1,0.2) ); const colour = Math.floor(col.x * 255).toString(16) + Math.floor(col.y * 255).toString(16) + Math.floor(col.z * 255).toString(16); const a = Math.floor((1.-d/30)*255).toString(16); return colour+a; } //////////////////////////////////////////////////////////////// // Simplex Noise utility code. Created by Reinder Nijhoff 2020 // https://turtletoy.net/turtle/6e4e06d42e // Based on: http://webstaff.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf //////////////////////////////////////////////////////////////// function SimplexNoise(seed = 1) { const grad = [ [1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1] ]; const perm = new Uint8Array(512); const F2 = (Math.sqrt(3) - 1) / 2, F3 = 1/3; const G2 = (3 - Math.sqrt(3)) / 6, G3 = 1/6; const dot2 = (a, b) => a[0] * b[0] + a[1] * b[1]; const sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]]; const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; const sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; class SimplexNoise { constructor(seed = 1) { for (let i = 0; i < 512; i++) { perm[i] = i & 255; } for (let i = 0; i < 255; i++) { const r = (seed = this.hash(i+seed)) % (256 - i) + i; const swp = perm[i]; perm[i + 256] = perm[i] = perm[r]; perm[r + 256] = perm[r] = swp; } } noise2D(p) { const s = dot2(p, [F2, F2]); const c = [Math.floor(p[0] + s), Math.floor(p[1] + s)]; const i = c[0] & 255, j = c[1] & 255; const t = dot2(c, [G2, G2]); const p0 = sub2(p, sub2(c, [t, t])); const o = p0[0] > p0[1] ? [1, 0] : [0, 1]; const p1 = sub2(sub2(p0, o), [-G2, -G2]); const p2 = sub2(p0, [1-2*G2, 1-2*G2]); let n = Math.max(0, 0.5-dot2(p0, p0))**4 * dot2(grad[perm[i+perm[j]] % 12], p0); n += Math.max(0, 0.5-dot2(p1, p1))**4 * dot2(grad[perm[i+o[0]+perm[j+o[1]]] % 12], p1); n += Math.max(0, 0.5-dot2(p2, p2))**4 * dot2(grad[perm[i+1+perm[j+1]] % 12], p2); return 70 * n; } hash(i) { i = 1103515245 * ((i >> 1) ^ i); const h32 = 1103515245 * (i ^ (i>>3)); return h32 ^ (h32 >> 16); } } return new SimplexNoise(seed); } // https://github.com/mapbox/delaunator // // ISC License // // Copyright (c) 2017, Mapbox // // Permission to use, copy, modify, and/or distribute this software for any purpose // with or without fee is hereby granted, provided that the above copyright notice // and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS // OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER // TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF // THIS SOFTWARE. // const EPSILON = Math.pow(2, -52); const EDGE_STACK = new Uint32Array(512); class Delaunator { static from(points) { const n = points.length; const coords = new Float64Array(n * 2); for (let i = 0; i < n; i++) { const p = points[i]; coords[2 * i] = p[0]; coords[2 * i + 1] = p[1]; } return new Delaunator(coords); } constructor(coords) { const n = coords.length >> 1; if (n > 0 && typeof coords[0] !== 'number') throw new Error('Expected coords to contain numbers.'); this.coords = coords; // arrays that will store the triangulation graph const maxTriangles = 2 * n - 5; const triangles = this.triangles = new Uint32Array(maxTriangles * 3); const halfedges = this.halfedges = new Int32Array(maxTriangles * 3); // temporary arrays for tracking the edges of the advancing convex hull this._hashSize = Math.ceil(Math.sqrt(n)); const hullPrev = this.hullPrev = new Uint32Array(n); // edge to prev edge const hullNext = this.hullNext = new Uint32Array(n); // edge to next edge const hullTri = this.hullTri = new Uint32Array(n); // edge to adjacent triangle const hullHash = new Int32Array(this._hashSize).fill(-1); // angular edge hash // populate an array of point indices; calculate input data bbox const ids = new Uint32Array(n); let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (let i = 0; i < n; i++) { const x = coords[2 * i]; const y = coords[2 * i + 1]; if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; ids[i] = i; } const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2; let minDist = Infinity; let i0, i1, i2; // pick a seed point close to the center for (let i = 0; i < n; i++) { const d = dist(cx, cy, coords[2 * i], coords[2 * i + 1]); if (d < minDist) { i0 = i; minDist = d; } } const i0x = coords[2 * i0]; const i0y = coords[2 * i0 + 1]; minDist = Infinity; // find the point closest to the seed for (let i = 0; i < n; i++) { if (i === i0) continue; const d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1]); if (d < minDist && d > 0) { i1 = i; minDist = d; } } let i1x = coords[2 * i1]; let i1y = coords[2 * i1 + 1]; let minRadius = Infinity; // find the third point which forms the smallest circumcircle with the first two for (let i = 0; i < n; i++) { if (i === i0 || i === i1) continue; const r = circumradius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1]); if (r < minRadius) { i2 = i; minRadius = r; } } let i2x = coords[2 * i2]; let i2y = coords[2 * i2 + 1]; if (minRadius === Infinity) { throw new Error('No Delaunay triangulation exists for this input.'); } // swap the order of the seed points for counter-clockwise orientation if (orient(i0x, i0y, i1x, i1y, i2x, i2y)) { const i = i1; const x = i1x; const y = i1y; i1 = i2; i1x = i2x; i1y = i2y; i2 = i; i2x = x; i2y = y; } const center = circumcenter(i0x, i0y, i1x, i1y, i2x, i2y); this._cx = center.x; this._cy = center.y; const dists = new Float64Array(n); for (let i = 0; i < n; i++) { dists[i] = dist(coords[2 * i], coords[2 * i + 1], center.x, center.y); } // sort the points by distance from the seed triangle circumcenter quicksort(ids, dists, 0, n - 1); // set up the seed triangle as the starting hull this.hullStart = i0; let hullSize = 3; hullNext[i0] = hullPrev[i2] = i1; hullNext[i1] = hullPrev[i0] = i2; hullNext[i2] = hullPrev[i1] = i0; hullTri[i0] = 0; hullTri[i1] = 1; hullTri[i2] = 2; hullHash[this._hashKey(i0x, i0y)] = i0; hullHash[this._hashKey(i1x, i1y)] = i1; hullHash[this._hashKey(i2x, i2y)] = i2; this.trianglesLen = 0; this._addTriangle(i0, i1, i2, -1, -1, -1); for (let k = 0, xp, yp; k < ids.length; k++) { const i = ids[k]; const x = coords[2 * i]; const y = coords[2 * i + 1]; // skip near-duplicate points if (k > 0 && Math.abs(x - xp) <= EPSILON && Math.abs(y - yp) <= EPSILON) continue; xp = x; yp = y; // skip seed triangle points if (i === i0 || i === i1 || i === i2) continue; // find a visible edge on the convex hull using edge hash let start = 0; for (let j = 0, key = this._hashKey(x, y); j < this._hashSize; j++) { start = hullHash[(key + j) % this._hashSize]; if (start !== -1 && start !== hullNext[start]) break; } start = hullPrev[start]; let e = start, q; while (q = hullNext[e], !orient(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1])) { e = q; if (e === start) { e = -1; break; } } if (e === -1) continue; // likely a near-duplicate point; skip it // add the first triangle from the point let t = this._addTriangle(e, i, hullNext[e], -1, -1, hullTri[e]); // recursively flip triangles from the point until they satisfy the Delaunay condition hullTri[i] = this._legalize(t + 2); hullTri[e] = t; // keep track of boundary triangles on the hull hullSize++; // walk forward through the hull, adding more triangles and flipping recursively let n = hullNext[e]; while (q = hullNext[n], orient(x, y, coords[2 * n], coords[2 * n + 1], coords[2 * q], coords[2 * q + 1])) { t = this._addTriangle(n, i, q, hullTri[i], -1, hullTri[n]); hullTri[i] = this._legalize(t + 2); hullNext[n] = n; // mark as removed hullSize--; n = q; } // walk backward from the other side, adding more triangles and flipping if (e === start) { while (q = hullPrev[e], orient(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1])) { t = this._addTriangle(q, i, e, -1, hullTri[e], hullTri[q]); this._legalize(t + 2); hullTri[q] = t; hullNext[e] = e; // mark as removed hullSize--; e = q; } } // update the hull indices this.hullStart = hullPrev[i] = e; hullNext[e] = hullPrev[n] = i; hullNext[i] = n; // save the two new edges in the hash table hullHash[this._hashKey(x, y)] = i; hullHash[this._hashKey(coords[2 * e], coords[2 * e + 1])] = e; } this.hull = new Uint32Array(hullSize); for (let i = 0, e = this.hullStart; i < hullSize; i++) { this.hull[i] = e; e = hullNext[e]; } this.hullPrev = this.hullNext = this.hullTri = null; // get rid of temporary arrays // trim typed triangle mesh arrays this.triangles = triangles.subarray(0, this.trianglesLen); this.halfedges = halfedges.subarray(0, this.trianglesLen); } _hashKey(x, y) { return Math.floor(pseudoAngle(x - this._cx, y - this._cy) * this._hashSize) % this._hashSize; } _legalize(a) { const {triangles, coords, halfedges} = this; let i = 0; let ar = 0; // recursion eliminated with a fixed-size stack while (true) { const b = halfedges[a]; /* if the pair of triangles doesn't satisfy the Delaunay condition * (p1 is inside the circumcircle of [p0, pl, pr]), flip them, * then do the same check/flip recursively for the new pair of triangles * * pl pl * /||\ / \ * al/ || \bl al/ \a * / || \ / \ * / a||b \ flip /___ar___\ * p0\ || /p1 => p0\---bl---/p1 * \ || / \ / * ar\ || /br b\ /br * \||/ \ / * pr pr */ const a0 = a - a % 3; ar = a0 + (a + 2) % 3; if (b === -1) { // convex hull edge if (i === 0) break; a = EDGE_STACK[--i]; continue; } const b0 = b - b % 3; const al = a0 + (a + 1) % 3; const bl = b0 + (b + 2) % 3; const p0 = triangles[ar]; const pr = triangles[a]; const pl = triangles[al]; const p1 = triangles[bl]; const illegal = inCircle( coords[2 * p0], coords[2 * p0 + 1], coords[2 * pr], coords[2 * pr + 1], coords[2 * pl], coords[2 * pl + 1], coords[2 * p1], coords[2 * p1 + 1]); if (illegal) { triangles[a] = p1; triangles[b] = p0; const hbl = halfedges[bl]; // edge swapped on the other side of the hull (rare); fix the halfedge reference if (hbl === -1) { let e = this.hullStart; do { if (this.hullTri[e] === bl) { this.hullTri[e] = a; break; } e = this.hullNext[e]; } while (e !== this.hullStart); } this._link(a, hbl); this._link(b, halfedges[ar]); this._link(ar, bl); const br = b0 + (b + 1) % 3; // don't worry about hitting the cap: it can only happen on extremely degenerate input if (i < EDGE_STACK.length) { EDGE_STACK[i++] = br; } } else { if (i === 0) break; a = EDGE_STACK[--i]; } } return ar; } _link(a, b) { this.halfedges[a] = b; if (b !== -1) this.halfedges[b] = a; } // add a new triangle given vertex indices and adjacent half-edge ids _addTriangle(i0, i1, i2, a, b, c) { const t = this.trianglesLen; this.triangles[t] = i0; this.triangles[t + 1] = i1; this.triangles[t + 2] = i2; this._link(t, a); this._link(t + 1, b); this._link(t + 2, c); this.trianglesLen += 3; return t; } } // monotonically increases with real angle, but doesn't need expensive trigonometry function pseudoAngle(dx, dy) { const p = dx / (Math.abs(dx) + Math.abs(dy)); return (dy > 0 ? 3 - p : 1 + p) / 4; // [0..1] } function dist(ax, ay, bx, by) { const dx = ax - bx; const dy = ay - by; return dx * dx + dy * dy; } function orient(px, py, qx, qy, rx, ry) { return (qy - py) * (rx - qx) - (qx - px) * (ry - qy) < 0; } function inCircle(ax, ay, bx, by, cx, cy, px, py) { const dx = ax - px; const dy = ay - py; const ex = bx - px; const ey = by - py; const fx = cx - px; const fy = cy - py; const ap = dx * dx + dy * dy; const bp = ex * ex + ey * ey; const cp = fx * fx + fy * fy; return dx * (ey * cp - bp * fy) - dy * (ex * cp - bp * fx) + ap * (ex * fy - ey * fx) < 0; } function circumradius(ax, ay, bx, by, cx, cy) { const dx = bx - ax; const dy = by - ay; const ex = cx - ax; const ey = cy - ay; const bl = dx * dx + dy * dy; const cl = ex * ex + ey * ey; const d = 0.5 / (dx * ey - dy * ex); const x = (ey * bl - dy * cl) * d; const y = (dx * cl - ex * bl) * d; return x * x + y * y; } function circumcenter(ax, ay, bx, by, cx, cy) { const dx = bx - ax; const dy = by - ay; const ex = cx - ax; const ey = cy - ay; const bl = dx * dx + dy * dy; const cl = ex * ex + ey * ey; const d = 0.5 / (dx * ey - dy * ex); const x = ax + (ey * bl - dy * cl) * d; const y = ay + (dx * cl - ex * bl) * d; return {x, y}; } function quicksort(ids, dists, left, right) { if (right - left <= 20) { for (let i = left + 1; i <= right; i++) { const temp = ids[i]; const tempDist = dists[temp]; let j = i - 1; while (j >= left && dists[ids[j]] > tempDist) ids[j + 1] = ids[j--]; ids[j + 1] = temp; } } else { const median = (left + right) >> 1; let i = left + 1; let j = right; swap(ids, median, i); if (dists[ids[left]] > dists[ids[right]]) swap(ids, left, right); if (dists[ids[i]] > dists[ids[right]]) swap(ids, i, right); if (dists[ids[left]] > dists[ids[i]]) swap(ids, left, i); const temp = ids[i]; const tempDist = dists[temp]; while (true) { do i++; while (dists[ids[i]] < tempDist); do j--; while (dists[ids[j]] > tempDist); if (j < i) break; swap(ids, i, j); } ids[left + 1] = ids[j]; ids[j] = temp; if (right - i + 1 >= j - left) { quicksort(ids, dists, i, right); quicksort(ids, dists, left, j - 1); } else { quicksort(ids, dists, left, j - 1); quicksort(ids, dists, i, right); } } } function swap(arr, i, j) { const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }