Created
July 9, 2018 00:21
-
-
Save michaschwab/601525565084c5f18d4804a2b586ec8c to your computer and use it in GitHub Desktop.
custom shape voronoi
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<title>Custom Shape Voronoi</title> | |
</head> | |
<body> | |
<div class="container" id="container"> | |
<button id="reset">Reset</button> | |
</div> | |
<script src="index.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var CustomShapeVoronoi = /** @class */ (function () { | |
function CustomShapeVoronoi(container, targetValues) { | |
var _this = this; | |
this.targetValues = targetValues; | |
this.polygons = []; | |
this.setupVis(container); | |
this.drawBoundary(); | |
this.init(targetValues); | |
var raf = function () { | |
_this.propagatePolygons(); | |
_this.drawPolygons(); | |
requestAnimationFrame(raf); | |
}; | |
raf(); | |
document.getElementById('reset').onclick = function () { | |
_this.polygons = []; | |
_this.init(targetValues); | |
}; | |
} | |
CustomShapeVoronoi.prototype.init = function (targetValues) { | |
var _this = this; | |
var totalTargetValue = targetValues.reduce(function (a, b) { return a + b; }, 0); | |
var totalArea = 2 * Math.PI * 200; | |
this.areaToValueRatio = totalArea / totalTargetValue; | |
var targetAreas = targetValues.map(function (t) { return t * _this.areaToValueRatio; }); | |
var seeds = this.getSeedPoints(targetAreas.length); | |
this.setPolygonsFromSeeds(seeds, targetAreas); | |
this.drawPolygons(); | |
this.drawSeedPoints(seeds, targetValues); | |
}; | |
CustomShapeVoronoi.prototype.setupVis = function (container) { | |
this.vis = this.createSvgEl('svg'); | |
container.appendChild(this.vis); | |
this.vis.setAttribute('width', '450'); | |
this.vis.setAttribute('height', '450'); | |
}; | |
CustomShapeVoronoi.prototype.drawBoundary = function () { | |
var circle = this.createSvgEl('circle'); | |
this.vis.appendChild(circle); | |
this.setAttributes(circle, { | |
r: '200', | |
stroke: '#a00', | |
cx: '200', | |
cy: '200', | |
fill: 'none' | |
}); | |
}; | |
CustomShapeVoronoi.prototype.pointWithinBoundary = function (p) { | |
return Math.sqrt(Math.pow(p.x - 200, 2) + Math.pow(p.y - 200, 2)) < 200; | |
}; | |
CustomShapeVoronoi.prototype.pointWithinPolygons = function (p, polys) { | |
var point = [p.x, p.y]; | |
for (var _i = 0, polys_1 = polys; _i < polys_1.length; _i++) { | |
var poly = polys_1[_i]; | |
if (inside(point, poly.vertices)) { | |
return true; | |
} | |
} | |
return false; | |
}; | |
CustomShapeVoronoi.prototype.getSeedPoints = function (n) { | |
var points = []; | |
while (points.length < n) { | |
var point = { x: Math.round(Math.random() * 400), y: Math.round(Math.random() * 400) }; | |
if (this.pointWithinBoundary(point)) { | |
points.push(point); | |
} | |
} | |
return points; | |
}; | |
CustomShapeVoronoi.prototype.propagatePolygons = function (speed) { | |
if (speed === void 0) { speed = 1; } | |
var unfinishedPolies = this.polygons.filter(function (p) { return !p.finished; }); | |
var _loop_1 = function (polygon) { | |
var otherPolygons = this_1.polygons.filter(function (p) { return p !== polygon; }); | |
var numberOfReached = polygon.finishedVertices; | |
var numberNotReached = polygon.vertices.length - numberOfReached; | |
var missingProgress = numberOfReached * speed; | |
var normalSpeed = polygon.targetArea * speed / 100; | |
var localSpeed = Math.min(normalSpeed + missingProgress / numberNotReached, 3 * normalSpeed); | |
for (var _i = 0, _a = polygon.vertices; _i < _a.length; _i++) { | |
var vertex = _a[_i]; | |
var angle = vertex.angle / 360 * 2 * Math.PI; | |
if (!vertex.reached) { | |
vertex.x += Math.cos(angle) * localSpeed; | |
vertex.y += Math.sin(angle) * localSpeed; | |
if (!this_1.pointWithinBoundary(vertex) || | |
this_1.pointWithinPolygons(vertex, otherPolygons)) { | |
vertex.reached = true; | |
polygon.finishedVertices++; | |
} | |
} | |
} | |
if (numberOfReached === polygon.vertices.length) { | |
polygon.finished = true; | |
} | |
}; | |
var this_1 = this; | |
for (var _i = 0, unfinishedPolies_1 = unfinishedPolies; _i < unfinishedPolies_1.length; _i++) { | |
var polygon = unfinishedPolies_1[_i]; | |
_loop_1(polygon); | |
} | |
}; | |
CustomShapeVoronoi.prototype.setPolygonsFromSeeds = function (seeds, targetAreas) { | |
var i = 0; | |
for (var _i = 0, seeds_1 = seeds; _i < seeds_1.length; _i++) { | |
var seed = seeds_1[_i]; | |
var polygon = { | |
seed: seed, | |
vertices: [], | |
color: this.getRandomColor(), | |
area: 0, | |
el: null, | |
areaTextEl: null, | |
targetArea: targetAreas[i], | |
finishedVertices: 0, | |
finished: false | |
}; | |
var numberPoints = 100; | |
for (var i_1 = 0; i_1 < numberPoints; i_1++) { | |
polygon.vertices.push({ | |
x: seed.x, | |
y: seed.y, | |
angle: 360 / numberPoints * i_1, | |
reached: false | |
}); | |
} | |
this.polygons.push(polygon); | |
i++; | |
} | |
}; | |
CustomShapeVoronoi.prototype.drawSeedPoints = function (points, targetValues) { | |
var i = 0; | |
for (var _i = 0, points_1 = points; _i < points_1.length; _i++) { | |
var point = points_1[_i]; | |
var circle = this.createSvgEl('circle'); | |
this.setAttributes(circle, { | |
r: '5', | |
cx: point.x.toString(), | |
cy: point.y.toString(), | |
fill: '#f00' | |
}); | |
this.vis.appendChild(circle); | |
var text = this.createSvgEl('text'); | |
this.setAttributes(text, { | |
x: point.x.toString(), | |
y: point.y.toString(), | |
fill: '#fff' | |
}); | |
text.innerHTML = targetValues[i].toString(); | |
this.vis.appendChild(text); | |
i++; | |
} | |
}; | |
CustomShapeVoronoi.prototype.drawPolygons = function () { | |
for (var _i = 0, _a = this.polygons; _i < _a.length; _i++) { | |
var polygon = _a[_i]; | |
if (!polygon.el) { | |
polygon.el = this.createSvgEl('polygon'); | |
this.vis.appendChild(polygon.el); | |
polygon.areaTextEl = this.createSvgEl('text'); | |
this.setAttributes(polygon.areaTextEl, { | |
x: polygon.seed.x.toString(), | |
y: (polygon.seed.y + 15).toString(), | |
fill: '#fff' | |
}); | |
this.vis.appendChild(polygon.areaTextEl); | |
} | |
var points = polygon.vertices | |
.map(function (v) { return v.x.toString() + ',' + v.y.toString(); }) | |
.reduce(function (a, b) { return a + ' ' + b; }); | |
this.setAttributes(polygon.el, { | |
fill: '#' + polygon.color, | |
points: points | |
}); | |
polygon.areaTextEl.innerHTML = | |
(Math.round(calcPolygonArea(polygon.vertices) / this.areaToValueRatio / 10) / 10).toString(); | |
} | |
}; | |
CustomShapeVoronoi.prototype.setAttributes = function (el, attributes) { | |
for (var attrName in attributes) { | |
el.setAttribute(attrName, attributes[attrName]); | |
} | |
}; | |
CustomShapeVoronoi.prototype.createSvgEl = function (elType) { | |
return document.createElementNS("http://www.w3.org/2000/svg", elType); | |
}; | |
CustomShapeVoronoi.prototype.getRandomColor = function () { | |
var alphabet = '0123456789abcdef'; | |
return this.getRandomString(6, alphabet); | |
}; | |
CustomShapeVoronoi.prototype.getRandomString = function (length, alphabet) { | |
if (alphabet === void 0) { alphabet = 'abcdefghijklmnopqrstuvwxzy'; } | |
var s = ''; | |
for (var i = 0; i < length; i++) { | |
s += alphabet[Math.round(Math.random() * (alphabet.length - 1))]; | |
} | |
return s; | |
}; | |
return CustomShapeVoronoi; | |
}()); | |
(function () { | |
var container = document.getElementById('container'); | |
new CustomShapeVoronoi(container, [1, 2, 3, 4, 5, 6]); | |
})(); | |
function inside(point, vs) { | |
// ray-casting algorithm based on | |
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html | |
var x = point[0], y = point[1]; | |
var inside = false; | |
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) { | |
var xi = vs[i].x, yi = vs[i].y; | |
var xj = vs[j].x, yj = vs[j].y; | |
var intersect = ((yi > y) != (yj > y)) | |
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi); | |
if (intersect) | |
inside = !inside; | |
} | |
//console.log(point, vs, inside); | |
return inside; | |
} | |
function calcPolygonArea(vertices) { | |
var total = 0; | |
for (var i = 0, l = vertices.length; i < l; i++) { | |
var addX = vertices[i].x; | |
var addY = vertices[i == vertices.length - 1 ? 0 : i + 1].y; | |
var subX = vertices[i == vertices.length - 1 ? 0 : i + 1].x; | |
var subY = vertices[i].y; | |
total += (addX * addY * 0.5); | |
total -= (subX * subY * 0.5); | |
} | |
return Math.abs(total); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type VoronoiPolygon = { | |
seed: {x: number, y: number}, | |
color: string, | |
targetArea: number, | |
area: number, | |
el: SVGElement, | |
areaTextEl: SVGTextElement, | |
vertices: { | |
x: number, | |
y: number, | |
angle: number, | |
reached: boolean | |
}[], | |
finishedVertices: number, | |
finished: boolean | |
} | |
class CustomShapeVoronoi { | |
private vis: SVGElement; | |
private polygons: VoronoiPolygon[] = []; | |
private areaToValueRatio: number; | |
constructor(container: HTMLElement, private targetValues: number[]) { | |
this.setupVis(container); | |
this.drawBoundary(); | |
this.init(targetValues); | |
const raf = () => { | |
this.propagatePolygons(); | |
this.drawPolygons(); | |
requestAnimationFrame(raf); | |
}; | |
raf(); | |
document.getElementById('reset').onclick = () => { | |
this.polygons = []; | |
this.init(targetValues); | |
}; | |
} | |
init(targetValues) { | |
const totalTargetValue = targetValues.reduce((a, b) => a+b, 0); | |
const totalArea = 2 * Math.PI * 200; | |
this.areaToValueRatio = totalArea / totalTargetValue; | |
const targetAreas = targetValues.map(t => t * this.areaToValueRatio); | |
const seeds = this.getSeedPoints(targetAreas.length); | |
this.setPolygonsFromSeeds(seeds, targetAreas); | |
this.drawPolygons(); | |
this.drawSeedPoints(seeds, targetValues); | |
} | |
setupVis(container: HTMLElement) { | |
this.vis = this.createSvgEl('svg'); | |
container.appendChild(this.vis); | |
this.vis.setAttribute('width', '450'); | |
this.vis.setAttribute('height', '450'); | |
} | |
drawBoundary() { | |
const circle = this.createSvgEl('circle'); | |
this.vis.appendChild(circle); | |
this.setAttributes(circle, { | |
r: '200', | |
stroke: '#a00', | |
cx: '200', | |
cy: '200', | |
fill: 'none' | |
}); | |
} | |
pointWithinBoundary(p: {x: number, y: number}): boolean { | |
return Math.sqrt(Math.pow(p.x - 200, 2) + Math.pow(p.y - 200, 2)) < 200; | |
} | |
pointWithinPolygons(p: {x: number, y: number}, polys: VoronoiPolygon[]): boolean { | |
const point = [p.x, p.y]; | |
for(const poly of polys) { | |
if(inside(point, poly.vertices)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
getSeedPoints(n: number) { | |
const points: {x: number, y: number}[] = []; | |
while(points.length < n) { | |
const point = {x: Math.round(Math.random() * 400), y: Math.round(Math.random() * 400)}; | |
if(this.pointWithinBoundary(point)) { | |
points.push(point); | |
} | |
} | |
return points; | |
} | |
propagatePolygons(speed: number = 1) { | |
const unfinishedPolies = this.polygons.filter(p => !p.finished); | |
for(const polygon of unfinishedPolies) { | |
const otherPolygons = this.polygons.filter(p => p !== polygon); | |
const numberOfReached = polygon.finishedVertices; | |
const numberNotReached = polygon.vertices.length - numberOfReached; | |
const missingProgress = numberOfReached * speed; | |
const normalSpeed = polygon.targetArea * speed / 100; | |
const localSpeed = Math.min(normalSpeed + missingProgress / numberNotReached, | |
3 * normalSpeed); | |
for(const vertex of polygon.vertices) { | |
const angle = vertex.angle / 360 * 2 * Math.PI; | |
if(!vertex.reached) { | |
vertex.x += Math.cos(angle) * localSpeed; | |
vertex.y += Math.sin(angle) * localSpeed; | |
if(!this.pointWithinBoundary(vertex) || | |
this.pointWithinPolygons(vertex, otherPolygons)) { | |
vertex.reached = true; | |
polygon.finishedVertices++; | |
} | |
} | |
} | |
if(numberOfReached === polygon.vertices.length) { | |
polygon.finished = true; | |
} | |
} | |
} | |
setPolygonsFromSeeds(seeds: {x: number, y: number}[], targetAreas: number[]) { | |
let i = 0; | |
for(const seed of seeds) { | |
const polygon = { | |
seed: seed, | |
vertices: [], | |
color: this.getRandomColor(), | |
area: 0, | |
el: null, | |
areaTextEl: null, | |
targetArea: targetAreas[i], | |
finishedVertices: 0, | |
finished: false | |
}; | |
const numberPoints = 100; | |
for(let i = 0; i < numberPoints; i++) { | |
polygon.vertices.push({ | |
x: seed.x, | |
y: seed.y, | |
angle: 360 / numberPoints * i, | |
reached: false | |
}); | |
} | |
this.polygons.push(polygon); | |
i++; | |
} | |
} | |
drawSeedPoints(points: {x: number, y: number}[], targetValues: number[]) { | |
let i = 0; | |
for(const point of points) { | |
const circle = this.createSvgEl('circle'); | |
this.setAttributes(circle, { | |
r: '5', | |
cx: point.x.toString(), | |
cy: point.y.toString(), | |
fill: '#f00' | |
}); | |
this.vis.appendChild(circle); | |
const text = this.createSvgEl('text'); | |
this.setAttributes(text, { | |
x: point.x.toString(), | |
y: point.y.toString(), | |
fill: '#fff' | |
}); | |
text.innerHTML = targetValues[i].toString(); | |
this.vis.appendChild(text); | |
i++; | |
} | |
} | |
drawPolygons() { | |
for(const polygon of this.polygons) { | |
if(!polygon.el) { | |
polygon.el = this.createSvgEl('polygon'); | |
this.vis.appendChild(polygon.el); | |
polygon.areaTextEl = this.createSvgEl('text') as SVGTextElement; | |
this.setAttributes(polygon.areaTextEl, { | |
x: polygon.seed.x.toString(), | |
y: (polygon.seed.y + 15).toString(), | |
fill: '#fff' | |
}); | |
this.vis.appendChild(polygon.areaTextEl); | |
} | |
const points = polygon.vertices | |
.map(v => v.x.toString() + ',' + v.y.toString()) | |
.reduce((a, b) => a + ' ' + b); | |
this.setAttributes(polygon.el, { | |
fill: '#' + polygon.color, | |
points: points | |
}); | |
polygon.areaTextEl.innerHTML = | |
(Math.round(calcPolygonArea(polygon.vertices) / this.areaToValueRatio / 10) / 10).toString(); | |
} | |
} | |
setAttributes(el: SVGElement, attributes: {[attrName: string]: string}) { | |
for(const attrName in attributes) { | |
el.setAttribute(attrName, attributes[attrName]); | |
} | |
} | |
createSvgEl(elType: string) { | |
return document.createElementNS("http://www.w3.org/2000/svg", elType); | |
} | |
getRandomColor() { | |
const alphabet = '0123456789abcdef'; | |
return this.getRandomString(6, alphabet); | |
} | |
getRandomString(length: number, alphabet = 'abcdefghijklmnopqrstuvwxzy') { | |
let s = ''; | |
for(let i = 0; i < length; i++) { | |
s += alphabet[Math.round(Math.random() * (alphabet.length - 1))]; | |
} | |
return s; | |
} | |
} | |
(() => { | |
const container = document.getElementById('container'); | |
new CustomShapeVoronoi(container, [1, 2, 3, 4, 5, 6]); | |
})(); | |
function inside(point, vs) { | |
// ray-casting algorithm based on | |
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html | |
const x = point[0], y = point[1]; | |
let inside = false; | |
for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { | |
const xi = vs[i].x, yi = vs[i].y; | |
const xj = vs[j].x, yj = vs[j].y; | |
const intersect = ((yi > y) != (yj > y)) | |
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi); | |
if (intersect) inside = !inside; | |
} | |
//console.log(point, vs, inside); | |
return inside; | |
} | |
function calcPolygonArea(vertices) { | |
let total = 0; | |
for (let i = 0, l = vertices.length; i < l; i++) { | |
const addX = vertices[i].x; | |
const addY = vertices[i == vertices.length - 1 ? 0 : i + 1].y; | |
const subX = vertices[i == vertices.length - 1 ? 0 : i + 1].x; | |
const subY = vertices[i].y; | |
total += (addX * addY * 0.5); | |
total -= (subX * subY * 0.5); | |
} | |
return Math.abs(total); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment