Given an array of points with an x, y, and preferred next points, draw a path around all without closing the path until the end. Kinda turned into an optimization thing too.
A Pen by HARUN PEHLİVAN on CodePen.
| <header> | |
| <input type="range" min="3" max="200" step="1" value="12"> | |
| </header> | |
| <div> | |
| <canvas id="cvs1" height="800" width="800"></canvas> | |
| <canvas id="cvs2" height="800" width="800"></canvas> | |
| </div> |
| console.clear(); | |
| const PI = Math.PI; | |
| const PI2 = PI * 2; | |
| var staticId = 0; | |
| class Point { | |
| constructor({ x, y }) { | |
| this.id = generateId(); | |
| this.x = x; | |
| this.y = y; | |
| this.connection1 = null; | |
| this.connection2 = null; | |
| } | |
| static prox(x1, y1, x2, y2) { | |
| return Math.hypot(x2 - x1, y2 - y1); | |
| } | |
| open() { | |
| if (this.used()) return false; | |
| return this.connection1 ? 'connection2' : 'connection1'; | |
| } | |
| connect(id) { | |
| let open = this.open(); | |
| if (open) | |
| this[open] = id; | |
| else | |
| console.warn('NOT OPEN YET ASSUMING IT IS'); | |
| } | |
| used() { | |
| return this.connection1 !== null && this.connection2 !== null; | |
| } | |
| relate(points, centerX, centerY) { | |
| this.proxC = Point.prox(this.x, this.y, centerX, centerY); | |
| this.preference = []; | |
| this.preferences = []; | |
| // set distance to this point | |
| points.forEach(point => { | |
| if (point.id !== this.id) { | |
| let prox = Point.prox(this.x, this.y, point.x, point.y); | |
| // rounding proximity to a vector | |
| let vectorSize = 0.0625; | |
| let relProx = Math.round(prox / vectorSize) * vectorSize; | |
| // radial influence, normalized | |
| let rad = Math.atan2((this.y - point.y), (this.x - point.x)) * 180 / PI; | |
| let relRad = (rad + 180) / 360; | |
| relRad = Math.round(relRad / vectorSize) * vectorSize; | |
| let yDist = Math.round((this.y - point.y) / vectorSize) * vectorSize; | |
| let xDist = Math.round((this.x - point.x) / vectorSize) * vectorSize; | |
| this.preference.push([point.id, relProx, prox, relRad, yDist, xDist]); | |
| } | |
| }); | |
| // sort by distance to this, radial preference, SE emphasis | |
| this.preferences[0] = this.preference.sort((a, b) => { | |
| // relative prox | |
| if (a[1] > b[1]) return 1; | |
| if (a[1] < b[1]) return -1; | |
| // relative radial | |
| if (a[3] > b[3]) return 1; | |
| if (a[3] < b[3]) return -1; | |
| // y (negative == down) | |
| if (a[4] > b[4]) return 1; | |
| if (a[4] < b[4]) return -1; | |
| // x (negative == right) | |
| if (a[5] > b[5]) return 1; | |
| if (a[5] > b[5]) return -1; | |
| // real prox | |
| if (a[2] > b[2]) return 1; | |
| if (a[2] < b[2]) return -1; | |
| return 0; | |
| }); | |
| // sort by distance to this, radial ignorance, NW emphasis | |
| this.preferences[1] = this.preference.sort((a, b) => { | |
| // relative prox | |
| if (a[1] > b[1]) return 1; | |
| if (a[1] < b[1]) return -1; | |
| // relative radial | |
| if (a[3] > b[3]) return -1; | |
| if (a[3] < b[3]) return 1; | |
| // y (positive == up) | |
| if (a[4] > b[4]) return -1; | |
| if (a[4] < b[4]) return 1; | |
| // x (positive == left) | |
| if (a[5] > b[5]) return -1; | |
| if (a[5] > b[5]) return 1; | |
| // real prox | |
| if (a[2] > b[2]) return -1; | |
| if (a[2] < b[2]) return 1; | |
| return 0; | |
| }); | |
| // just ids | |
| this.preference = this.preference.map(p => { | |
| return p[0]; | |
| }); | |
| this.preferences[0] = this.preferences[0].map(p => { | |
| return p[0]; | |
| }); | |
| this.preferences[1] = this.preferences[1].map(p => { | |
| return p[0]; | |
| }); | |
| } | |
| } | |
| class Group { | |
| constructor(count) { | |
| this.generatePoints(count); | |
| } | |
| generatePoints(count) { | |
| this.points = {}; | |
| this.pointIds = []; | |
| let xs = 0; | |
| let ys = 0; | |
| for (let i = 0; i < count; i++) { | |
| let point = new Point({ x: Math.random(), y: Math.random() }); | |
| this.points[point.id] = point; | |
| this.pointIds.push(point.id); | |
| xs += point.x; | |
| ys += point.y; | |
| } | |
| this.x = xs / count; | |
| this.y = ys / count; | |
| this.relatePoints(); | |
| this.sortPoints(); | |
| } | |
| sortPoints() { | |
| this.pointIds = this.pointIds.sort((a, b) => { | |
| let apt = this.points[a]; | |
| let bpt = this.points[b]; | |
| // prefer real prox | |
| if (apt.proxC > bpt.proxC) return -1; | |
| if (apt.proxC < bpt.proxC) return 1; | |
| return 0; | |
| }); | |
| } | |
| relatePoints() { | |
| let arr = this.pointIds.map(id => { | |
| return this.points[id]; | |
| }); | |
| arr.forEach(point => { | |
| point.relate(arr, this.x, this.y); | |
| }); | |
| } | |
| } | |
| class Connector { | |
| constructor() {} | |
| run({ points, pointIds }) { | |
| this.points = points; | |
| this.pointIds = pointIds; | |
| this.path1 = [pointIds[0]]; | |
| this.path2 = [pointIds[0]]; | |
| return this.loop(); | |
| } | |
| loop() { | |
| let last1 = this.path1[this.path1.length - 1]; | |
| let last2 = this.path2[this.path2.length - 1]; | |
| let conn1 = this.processPoint(last1, 0); | |
| if (conn1) this.path1.push(conn1); | |
| let conn2 = this.processPoint(last2, 1); | |
| if (conn2) this.path2.push(conn2); | |
| if ((conn1 && conn2) || conn1 !== conn2) { | |
| return this.loop(); | |
| } else { | |
| this.path1.push(this.path2[this.path2.length - 1]); | |
| this.path2.push(this.path1[this.path1.length - 2]); | |
| return { path1: this.path1, path2: this.path2 }; | |
| } | |
| } | |
| processPoint(pointId, pref) { | |
| let point = this.points[pointId]; | |
| let prefs = point.preferences[pref]; | |
| let foundId = null; | |
| let prefIdx = 0; | |
| while (!foundId && prefIdx < prefs.length) { | |
| let prefId = prefs[prefIdx]; | |
| if (!this.path1.includes(prefId) && !this.path2.includes(prefId)) { | |
| foundId = prefId; | |
| } | |
| prefIdx++; | |
| } | |
| if (foundId) { | |
| point.connect(foundId); | |
| this.points[foundId].connect(point.id); | |
| } | |
| return foundId; | |
| } | |
| } | |
| class Canvas { | |
| constructor() { | |
| this.element1 = document.querySelector('#cvs1'); | |
| this.context1 = this.element1.getContext('2d'); | |
| this.element2 = document.querySelector('#cvs2'); | |
| this.context2 = this.element2.getContext('2d'); | |
| this.gutter = 30; | |
| this.diameter = this.element1.width; | |
| this.switchContext(0); | |
| } | |
| clear() { | |
| this.context1.clearRect(0, 0, this.diameter, this.diameter); | |
| this.context2.clearRect(0, 0, this.diameter, this.diameter); | |
| } | |
| switchContext(idx) { | |
| this.context = [this.context1, this.context2][idx]; | |
| } | |
| text(x, y, text, offset = 1) { | |
| this.context.textAlign = 'center'; | |
| this.context.textBaseline = 'middle'; | |
| let fontSize = 14; | |
| let offsetY = (y > 0.5) ? fontSize * (offset * 1.5) : fontSize * (offset * -1.5); | |
| this.context.fillStyle = 'white'; | |
| this.context.font = `${fontSize}px Helvetica`; | |
| this.context.fillText(text, this.relative(x), this.relative(y) + offsetY); | |
| } | |
| point(x, y, fill = 'red') { | |
| this.context.fillStyle = fill; | |
| this.context.beginPath(); | |
| this.context.arc(this.relative(x), this.relative(y), 4, 0, PI2); | |
| this.context.fill(); | |
| } | |
| path(x1, y1, x2, y2, stroke = 'red') { | |
| this.context.strokeStyle = stroke; | |
| this.context.lineWidth = 2; | |
| this.context.beginPath(); | |
| this.context.moveTo(this.relative(x1), this.relative(y1)); | |
| this.context.lineTo(this.relative(x2), this.relative(y2)); | |
| this.context.stroke(); | |
| } | |
| relative(plot) { | |
| return plot * (this.diameter - this.gutter * 2) + this.gutter; | |
| } | |
| } | |
| class Drawer { | |
| constructor() { | |
| this.canvas = new Canvas(); | |
| this.drawStraight(); | |
| } | |
| update(points, pointIds, paths, x, y) { | |
| this.x = x; | |
| this.y = y; | |
| this.speed = 8; | |
| this.pointIdx = 0; | |
| this.progress = 0; | |
| this.points = points; | |
| this.pointIds = pointIds; | |
| this.pointIdCount = pointIds.length; | |
| this.path1 = paths.path1; | |
| this.path2 = paths.path2; | |
| this.pointCount = paths.path1.length; | |
| } | |
| drawEach() { | |
| let first = this.pointIdx === 0; | |
| let idx = this.pointIdx % this.pointIdCount; | |
| let pointEnd = (this.progress % this.speed) === 0; | |
| // clearing the canvases | |
| if (idx === 0 && pointEnd) { | |
| this.drawn = []; | |
| this.canvas.switchContext(1); | |
| this.canvas.clear(); | |
| this.canvas.point(this.x, this.y, 'white'); | |
| this.pointIds.forEach((pointId) => { | |
| let point = this.points[pointId]; | |
| this.canvas.point(point.x, point.y); | |
| // let text = point.id + ' ' + Math.round(point.proxC * 100) / 100; | |
| let text = Math.round(point.proxC * 100) / 100; | |
| this.canvas.text(point.x, point.y, text); | |
| }); | |
| } | |
| this.canvas.switchContext(0); | |
| let ratio = (this.progress % this.speed) / (this.speed - 1); | |
| let point = this.points[this.pointIds[this.pointIdx % this.pointIdCount]]; | |
| let drawn = [point.id]; | |
| let skipped = 0; | |
| [point.connection1, point.connection2].forEach((pointId) => { | |
| if (pointId) { | |
| if (this.drawn.includes(pointId)) { | |
| skipped++; | |
| } else { | |
| let conn = this.points[pointId]; | |
| // drawn.push(pointId); | |
| let distX = (point.x - conn.x) * ratio; | |
| let distY = (point.y - conn.y) * ratio; | |
| let dx = point.x - distX; | |
| let dy = point.y - distY; | |
| this.canvas.path(point.x, point.y, dx, dy, 'red'); | |
| } | |
| } else if(!point.connection1) {console.log('here')} | |
| }); | |
| if (skipped === 2 && idx+1 !== 0) { | |
| this.pointIdx++; | |
| } else { | |
| this.progress++; | |
| } | |
| if (this.progress % this.speed === 0) { | |
| drawn.forEach((id) => { this.drawn.push(id) }); | |
| this.pointIdx++; | |
| } | |
| window.requestAnimationFrame(() => { | |
| this.drawEach(); | |
| }); | |
| } | |
| drawStraight() { | |
| if (this.points) { | |
| let first = this.pointIdx === 0; | |
| let idx1 = this.pointIdx % this.pointCount; | |
| let pointEnd = (this.progress % this.speed) === 0; | |
| // clearing the canvases | |
| if (idx1 === 0 && pointEnd) { | |
| this.canvas.switchContext(1); | |
| this.canvas.clear(); | |
| this.path1.forEach((pointId) => { | |
| let point = this.points[pointId]; | |
| this.canvas.point(point.x, point.y, 'red'); | |
| }); | |
| this.path2.forEach((pointId) => { | |
| let point = this.points[pointId]; | |
| this.canvas.point(point.x, point.y, 'blue'); | |
| // this.canvas.text(point.x, point.y, Math.round(point.proxC * 100) / 100); | |
| // this.canvas.text(point.x, point.y, point.id); | |
| }); | |
| this.canvas.point(this.x, this.y, 'yellow'); | |
| } | |
| this.canvas.switchContext(0); | |
| let ratio = (this.progress % this.speed) / (this.speed - 1); | |
| [this.path1, this.path2].forEach((path, i) => { | |
| let idx = this.pointIdx % this.pointCount; | |
| let point = this.points[path[idx]]; | |
| let conn = this.points[path[idx + 1]]; | |
| if (conn) { | |
| let distX = (point.x - conn.x) * ratio; | |
| let distY = (point.y - conn.y) * ratio; | |
| let dx = point.x - distX; | |
| let dy = point.y - distY; | |
| let color = ['red', 'blue'][i]; | |
| this.canvas.path(point.x, point.y, dx, dy, color); | |
| } | |
| }); | |
| this.progress++; | |
| if (this.progress % this.speed === 0) { | |
| this.pointIdx++; | |
| } | |
| } | |
| window.requestAnimationFrame(() => { | |
| this.drawStraight(); | |
| }); | |
| } | |
| } | |
| let $input = document.querySelector('input'); | |
| let group = new Group(); | |
| let connector = new Connector(); | |
| let drawer = new Drawer(); | |
| function run() { | |
| let count = parseInt($input.value); | |
| group.generatePoints(count); | |
| let paths = connector.run(group); | |
| drawer.update(group.points, group.pointIds, paths, group.x, group.y); | |
| } | |
| run(); | |
| document.body.addEventListener('click', run); | |
| $input.addEventListener('change', run); | |
| function generateId() { | |
| let id = staticId++; | |
| if (id < 10) return `000${id}`; | |
| if (id < 100) return `00${id}`; | |
| if (id < 1000) return `0${id}`; | |
| return id.toString(); | |
| } |
| body { | |
| background: #121212; | |
| } | |
| header { | |
| text-align: center; | |
| margin: 1rem 0; | |
| } | |
| div { | |
| position: relative; | |
| width: 95%; | |
| max-width: 600px; | |
| margin: 1rem auto; | |
| &:after { | |
| content: ''; | |
| padding-bottom: 100%; | |
| display: block; | |
| } | |
| canvas { | |
| &:first-child { | |
| background: black; | |
| } | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; | |
| height: auto; | |
| } | |
| } |
Given an array of points with an x, y, and preferred next points, draw a path around all without closing the path until the end. Kinda turned into an optimization thing too.
A Pen by HARUN PEHLİVAN on CodePen.