Created
July 10, 2018 05:19
-
-
Save michaschwab/24c24634198a888abb3a3c8ce685616a to your computer and use it in GitHub Desktop.
custom size and shape voronoi 2
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.polyBoundaries = []; | |
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); | |
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 true; | |
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 (targetValues) { | |
var n = targetValues.length; | |
var r = 170; | |
var points = []; | |
var i = 0; | |
var totalValue = targetValues.reduce(function (a, b) { return a + b; }); | |
var anglePerValue = 360 / totalValue; | |
var angle = 0; | |
while (points.length < n) { | |
var currentValue = targetValues[i]; | |
angle += anglePerValue * currentValue / 2; | |
var point = { | |
x: 200 + Math.round(Math.cos(angle * 2 * Math.PI / 360) * r), | |
y: 200 + Math.round(Math.sin(angle * 2 * Math.PI / 360) * r) | |
}; | |
if (this.pointWithinBoundary(point)) { | |
points.push(point); | |
} | |
i++; | |
angle += anglePerValue * currentValue / 2; | |
} | |
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; | |
//const normalSpeed = 300 * speed / 100; | |
var localSpeed = normalSpeed; //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.setPolyBoundariesFromSeeds = function (seeds, targetAreas) { | |
for (var i = 0; i < seeds.length; i++) { | |
var seed1 = seeds[i]; | |
for (var j = i + 1; j < seeds.length; j++) { | |
var seed2 = seeds[j]; | |
var diffVector = { | |
x: seed2.x - seed1.x, | |
y: seed2.x - seed1.x | |
}; | |
var middlePoint = { | |
x: (seed2.x + seed1.x) / 2, | |
y: (seed2.y + seed1.y) / 2, | |
}; | |
var perpendicularVector = { | |
x: diffVector.y * -1 * -1, | |
y: diffVector.x * -1 | |
}; | |
var perpendicularVectorLength = Math.sqrt(Math.pow(perpendicularVector.x, 2) + | |
Math.pow(perpendicularVector.y, 2)); | |
var perpendicularVectorNormal = { | |
x: perpendicularVector.x / perpendicularVectorLength, | |
y: perpendicularVector.y / perpendicularVectorLength | |
}; | |
var boundary = { | |
middle: middlePoint, | |
direction: perpendicularVectorNormal, | |
start: middlePoint, | |
end: middlePoint, | |
seed1: seed1, | |
seed2: seed2 | |
}; | |
this.polyBoundaries.push(boundary); | |
} | |
} | |
}; | |
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, 5, 6, 4]); | |
})(); | |
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; | |
private polyBoundaries: { | |
middle: { x: number, y: number }, | |
direction: { x: number, y: number }, | |
start: { x: number, y: number }, | |
end: { x: number, y: number }, | |
seed1: { x: number, y: number }, | |
seed2: { x: number, y: 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); | |
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 true; | |
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(targetValues: number[]) { | |
const n = targetValues.length; | |
const r = 170; | |
const points: {x: number, y: number}[] = []; | |
let i = 0; | |
const totalValue = targetValues.reduce((a, b) => a+b); | |
const anglePerValue = 360 / totalValue; | |
let angle = 0; | |
while(points.length < n) { | |
const currentValue = targetValues[i]; | |
angle += anglePerValue * currentValue / 2; | |
const point = { | |
x: 200 + Math.round(Math.cos(angle * 2 * Math.PI / 360) * r), | |
y: 200 + Math.round(Math.sin(angle * 2 * Math.PI / 360) * r) | |
}; | |
if(this.pointWithinBoundary(point)) { | |
points.push(point); | |
} | |
i++; | |
angle += anglePerValue * currentValue / 2; | |
} | |
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 normalSpeed = 300 * speed / 100; | |
const localSpeed = normalSpeed; //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; | |
} | |
} | |
} | |
setPolyBoundariesFromSeeds(seeds: {x: number, y: number}[], targetAreas: number[]) { | |
for(let i = 0; i < seeds.length; i++) { | |
const seed1 = seeds[i]; | |
for(let j = i+1; j < seeds.length; j++) { | |
const seed2 = seeds[j]; | |
const diffVector = { | |
x: seed2.x - seed1.x, | |
y: seed2.x - seed1.x | |
}; | |
const middlePoint = { | |
x: (seed2.x + seed1.x) / 2, | |
y: (seed2.y + seed1.y) / 2, | |
}; | |
const perpendicularVector = { | |
x: diffVector.y * -1 * -1, // 2x -1 to disable warning about assigning y to x | |
y: diffVector.x * -1 | |
}; | |
const perpendicularVectorLength = Math.sqrt( | |
Math.pow(perpendicularVector.x, 2) + | |
Math.pow(perpendicularVector.y, 2) | |
); | |
const perpendicularVectorNormal = { | |
x: perpendicularVector.x / perpendicularVectorLength, | |
y: perpendicularVector.y / perpendicularVectorLength | |
}; | |
const boundary = { | |
middle: middlePoint, | |
direction: perpendicularVectorNormal, | |
start: middlePoint, | |
end: middlePoint, | |
seed1, | |
seed2 | |
}; | |
this.polyBoundaries.push(boundary); | |
} | |
} | |
} | |
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, 5, 6, 4]); | |
})(); | |
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