Skip to content

Instantly share code, notes, and snippets.

@michaschwab
Created July 9, 2018 00:21
Show Gist options
  • Save michaschwab/601525565084c5f18d4804a2b586ec8c to your computer and use it in GitHub Desktop.
Save michaschwab/601525565084c5f18d4804a2b586ec8c to your computer and use it in GitHub Desktop.
custom shape voronoi
<!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>
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);
}
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