Drawing all the permutations of the ordering of a group of nodes.
The permutation count could be cut in half to eliminate reverse order permutations. eg 01230 === 03210.
A Pen by HARUN PEHLİVAN on CodePen.
| <header> | |
| <input type="range" list="numbers" value="5"> | |
| <datalist id="numbers"> | |
| <option>3</option> | |
| <option>5</option> | |
| <option>10</option> | |
| <option>25</option> | |
| <option>50</option> | |
| <option>100</option> | |
| </datalist> | |
| </header> | |
| <div> | |
| <canvas id="cvs1" height="800" width="800"></canvas> | |
| <canvas id="cvs2" height="800" width="800"></canvas> | |
| </div> | |
| <footer> | |
| <p>Permutation</p> | |
| <p id="perm"></p> | |
| </footer> |
| console.clear(); | |
| const PI = Math.PI; | |
| const PI2 = PI * 2; | |
| class Point { | |
| constructor() { | |
| this.x = Math.random(); | |
| this.y = Math.random(); | |
| this.prox = Math.hypot(0.5 - this.x, 0.5 - this.y); | |
| } | |
| } | |
| class Permutations { | |
| constructor(pointCount) { | |
| this.pointCount = pointCount; | |
| this.data = []; | |
| // initial array ignores 0. we start permutations at 1. | |
| for (let i = 1; i <= this.pointCount - 1; i++) this.data.push(i); | |
| this.originalData = Array.from(this.data); | |
| } | |
| next() { | |
| let more = this.nextPermutation(this.data); | |
| if (!more) this.data = Array.from(this.originalData); | |
| return this.current(); | |
| } | |
| load(idx) { | |
| return this.current()[idx]; | |
| } | |
| current() { | |
| return [0].concat(this.data).concat([0]); | |
| } | |
| // https://stackoverflow.com/a/34014930 | |
| nextPermutation(array) { | |
| // Find non-increasing suffix | |
| let i = array.length - 1; | |
| while (i > 0 && array[i - 1] >= array[i]) i--; | |
| if (i <= 0) return false; | |
| // Find successor to pivot | |
| let j = array.length - 1; | |
| while (array[j] <= array[i - 1]) j--; | |
| let temp = array[i - 1]; | |
| array[i - 1] = array[j]; | |
| array[j] = temp; | |
| // Reverse suffix | |
| j = array.length - 1; | |
| while (i < j) { | |
| temp = array[i]; | |
| array[i] = array[j]; | |
| array[j] = temp; | |
| i++; j--; | |
| } | |
| return true; | |
| } | |
| } | |
| 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(which) { | |
| if (which !== undefined) { | |
| [this.context1, this.context2][which].clearRect(0, 0, this.diameter, this.diameter); | |
| } else { | |
| 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); | |
| } | |
| rect(x, y, w, h, color = 'red') { | |
| this.context.strokeStyle = color; | |
| this.context.strokeRect( | |
| this.relative(x), this.relative(y), | |
| this.relativeSize(w), this.relativeSize(h) | |
| ); | |
| } | |
| 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(); | |
| } | |
| relativeSize(size) { | |
| return size * (this.diameter - this.gutter * 2); | |
| } | |
| relative(plot) { | |
| return this.relativeSize(plot) + this.gutter; | |
| } | |
| } | |
| class Drawer { | |
| constructor() { | |
| this.canvas = new Canvas(); | |
| this.$perm = document.querySelector('#perm'); | |
| this.draw(); | |
| } | |
| run(points) { | |
| this.speed = 12; | |
| this.points = points; | |
| this.points = this.points.sort((a, b) => { | |
| if (a.prox > b.prox) return -1; | |
| if (a.prox < b.prox) return 1; | |
| return 0; | |
| }); | |
| this.permutations = new Permutations(points.length); | |
| this.pointCount = this.permutations.data.length + 2; | |
| this.write(); | |
| this.pointIdx = 0; | |
| this.progress = 0; | |
| } | |
| write() { | |
| this.$perm.innerHTML = this.permutations.current().join(', '); | |
| } | |
| draw() { | |
| if (this.points) { | |
| this.canvas.clear(1); | |
| if (this.pointIdx % this.pointCount === 0) { | |
| this.canvas.clear(); | |
| this.canvas.switchContext(0); | |
| this.points.forEach((point) => { | |
| this.canvas.point(point.x, point.y, '#222'); | |
| }); | |
| this.canvas.switchContext(1); | |
| this.write(); | |
| } | |
| let idx1 = this.permutations.load(this.pointIdx % this.pointCount); | |
| let point1 = this.points[idx1]; | |
| let idx2 = this.permutations.load(((this.pointIdx % this.pointCount) + 1) % this.pointCount); | |
| let point2 = this.points[idx2]; | |
| let x1 = point1.x, | |
| y1 = point1.y; | |
| let x2 = point2.x, | |
| y2 = point2.y; | |
| if ((this.pointIdx + 1) % this.pointCount > 0) { | |
| let ratio = (this.progress % this.speed) / (this.speed - 1); | |
| let distX = (x1 - x2) * ratio; | |
| let distY = (y1 - y2) * ratio; | |
| let dx = x1 - distX; | |
| let dy = y1 - distY; | |
| this.canvas.switchContext(1); | |
| this.canvas.path(x1, y1, dx, dy, 'red', 2); | |
| this.canvas.point(x1, y1, 'red'); | |
| this.canvas.switchContext(1); | |
| } | |
| this.progress++; | |
| if (this.progress % this.speed === 0) { | |
| this.canvas.switchContext(0); | |
| this.canvas.path(x1, y1, x2, y2, '#222', 2); | |
| this.canvas.point(x1, y1, '#222'); // previous | |
| this.pointIdx++; | |
| if (this.pointIdx % this.pointCount === 0) { | |
| this.permutations.next(); | |
| } | |
| } | |
| } | |
| window.requestAnimationFrame(this.draw.bind(this)); | |
| } | |
| } | |
| class AwesomePossum { | |
| constructor() { | |
| this.drawer = new Drawer(); | |
| } | |
| run(pointCount) { | |
| this.points = []; | |
| this.pointCount = pointCount; | |
| for (let i = 0; i < this.pointCount; i++) { | |
| this.points.push(new Point()); | |
| } | |
| this.drawer.run(this.points); | |
| } | |
| } | |
| let app = new AwesomePossum(); | |
| let $range = document.querySelector('input'); | |
| document.body.addEventListener('click', run); | |
| $range.addEventListener('change', run); | |
| run(); | |
| function run() { | |
| app.run(parseInt($range.value)); | |
| } |
| body { | |
| background: #121212; | |
| } | |
| header, | |
| footer { | |
| text-align: center; | |
| margin: 1rem 0; | |
| color: #F00; | |
| font-weight: 300; | |
| font-size: 0.8rem; | |
| p#perm { | |
| color: #999; | |
| width: 95%; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| } | |
| ul { | |
| list-style: none; | |
| padding: 0; | |
| } | |
| } | |
| div { | |
| position: relative; | |
| width: 95%; | |
| max-width: 600px; | |
| margin: 1rem auto; | |
| cursor: pointer; | |
| &:after { | |
| content: ''; | |
| padding-bottom: 100%; | |
| display: block; | |
| } | |
| canvas { | |
| &:first-child { | |
| background: black; | |
| } | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; | |
| height: auto; | |
| } | |
| } |
Drawing all the permutations of the ordering of a group of nodes.
The permutation count could be cut in half to eliminate reverse order permutations. eg 01230 === 03210.
A Pen by HARUN PEHLİVAN on CodePen.