voronoi.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. console.log('hi voronoi');
  2. const canvas = document.getElementById('canvas'),
  3. canvwidth = canvas.width,
  4. canvheight = canvas.height,
  5. ctx = canvas.getContext('2d');
  6. const minDist = 20,
  7. edgeBounds = 20;
  8. function drawSite(x, y, colour = 'black') {
  9. ctx.save();
  10. ctx.beginPath();
  11. ctx.arc(x, y, 2, 0, Math.PI * 2, false);
  12. ctx.closePath();
  13. ctx.fillStyle = colour;
  14. ctx.fill();
  15. ctx.restore();
  16. }
  17. function drawLine(x1, y1, x2, y2) {
  18. ctx.save();
  19. ctx.beginPath();
  20. ctx.strokeStyle = 'black';
  21. ctx.lineWidth = 1;
  22. ctx.moveTo(x1, y1);
  23. ctx.lineTo(x2, y2);
  24. ctx.stroke();
  25. ctx.restore();
  26. }
  27. function drawLineO(p1, p2) {
  28. drawLine(p1.x, p1.y, p2.x, p2.y);
  29. }
  30. function distance(p1, p2) {
  31. return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
  32. }
  33. function generateSites(n = 10) {
  34. const sites = [],
  35. safety = n * 2,
  36. randMultWidth = canvwidth - (edgeBounds * 2),
  37. randMultHeight = canvheight - (edgeBounds * 2),
  38. origin = {x:0, y:0};
  39. let count = 0;
  40. while( sites.length < n && count < safety ) {
  41. count += 1;
  42. const x = Math.floor(Math.random() * randMultWidth) + edgeBounds,
  43. y = Math.floor(Math.random() * randMultHeight) + edgeBounds,
  44. min = sites.reduce(function checkDist(minSoFar, existing) {
  45. const d = distance(existing, {x, y});
  46. if( d < minSoFar ) {
  47. return d;
  48. }
  49. else {
  50. return minSoFar;
  51. }
  52. }, canvwidth * canvheight);
  53. if( min > minDist ) {
  54. sites.push( {x, y} );
  55. }
  56. }
  57. sites.sort(function (a, b) {
  58. return a.y - b.y;
  59. });
  60. return sites;
  61. }
  62. function randIndivC(a = 0, b = 255) {
  63. return Math.floor(Math.random() * (b-a) + a);
  64. }
  65. function cts(c) {
  66. const r = c.r < 16 ? '0' + c.r.toString(16) : c.r.toString(16),
  67. g = c.g < 16 ? '0' + c.g.toString(16) : c.g.toString(16),
  68. b = c.b < 16 ? '0' + c.b.toString(16) : c.b.toString(16);
  69. return '#' + r + g + b;
  70. }
  71. function primaryOrSecondary() {
  72. const t = Math.random(),
  73. w = Math.floor(Math.random() * 3);
  74. if( t < 0.45 ) {
  75. const r = w === 0 ? 255 : 0,
  76. g = w === 1 ? 255 : 0,
  77. b = w === 2 ? 255 : 0;
  78. return {r, g, b};
  79. }
  80. else if( t < 0.9 ) {
  81. const r = w !== 0 ? 255 : 0,
  82. g = w !== 1 ? 255 : 0,
  83. b = w !== 2 ? 255 : 0;
  84. return {r, g, b};
  85. }
  86. else {
  87. //return {r: 255, g:255, b:255};
  88. const r = randIndivC(100, 255),
  89. g = randIndivC(100, 255),
  90. b = randIndivC(100, 255);
  91. return {r, g, b};
  92. }
  93. }
  94. function randomColor() {
  95. const t = Math.random(),
  96. w = Math.floor(Math.random() * 3);
  97. if( t < 0.45 ) {
  98. const r = w === 0 ? randIndivC(200, 255) : randIndivC(10, 100),
  99. g = w === 1 ? randIndivC(200, 255) : randIndivC(10, 100),
  100. b = w === 2 ? randIndivC(200, 255) : randIndivC(10, 100);
  101. return {r, g, b};
  102. }
  103. else if( t < 0.9 ) {
  104. const r = w !== 0 ? randIndivC(200, 255) : randIndivC(10, 100),
  105. g = w !== 1 ? randIndivC(200, 255) : randIndivC(10, 100),
  106. b = w !== 2 ? randIndivC(200, 255) : randIndivC(10, 100);
  107. return {r, g, b};
  108. }
  109. else {
  110. const r = randIndivC(100, 200),
  111. g = randIndivC(100, 200),
  112. b = randIndivC(100, 200);
  113. return {r, g, b};
  114. }
  115. }
  116. function mixedColor(base) {
  117. let randr = Math.floor(Math.random() * 200 + 55),
  118. randg = Math.floor(Math.random() * 200 + 55),
  119. randb = Math.floor(Math.random() * 200 + 55);
  120. if( base ) {
  121. randr = Math.floor((base.r + randr) / 2);
  122. randg = Math.floor((base.g + randg) / 2);
  123. randb = Math.floor((base.b + randb) / 2);
  124. }
  125. return cts({r:randr, g:randg, b:randb});
  126. }
  127. const sites = generateSites(50).map(function assignRandomColour(site) {
  128. //site.color = cts(randomColor());
  129. //site.color = cts(primaryOrSecondary());
  130. site.color = mixedColor(primaryOrSecondary());
  131. site.sectors = [];
  132. return site;
  133. });
  134. //sites.forEach(function renderSites(site) {
  135. // drawSite(site.x, site.y);
  136. //});
  137. function bruteForceRow(row, sites) {
  138. sites = sites.map(function addNewRow(site) {
  139. site.sectors[row] = [];
  140. return site;
  141. })
  142. for( let i = 0; i < canvwidth; i += 1 ) {
  143. const point = {x:i, y:row},
  144. cind = sites.slice(1).reduce(function check(closest, site, ind) {
  145. const d = distance(site, point);
  146. if( d < closest.d ) {
  147. return {c:(ind + 1), d};
  148. }
  149. return closest;
  150. }, {c:0, d:distance(sites[0], point)});
  151. sites[cind.c].sectors[row].push(point);
  152. }
  153. return sites;
  154. }
  155. function spot(x, y, colour) {
  156. ctx.save();
  157. ctx.fillStyle = colour;
  158. ctx.fillRect(x, y, 1, 1);
  159. ctx.restore();
  160. }
  161. function goRow(i, sites) {
  162. const newSites = bruteForceRow(i, sites);
  163. newSites.filter(x => x.sectors[i].length)
  164. .forEach(function parse(leader) {
  165. leader.sectors[i].forEach(function draw(place) {
  166. spot(place.x, place.y, leader.color);
  167. })
  168. });
  169. if( i < canvheight ) {
  170. setTimeout(goRow, 10, i + 1, newSites);
  171. }
  172. else {
  173. setTimeout(function action() {
  174. // temp redraw sites
  175. sites.forEach(function renderSites(site) {
  176. drawSite(site.x, site.y);
  177. });
  178. }, 100);
  179. }
  180. }
  181. //goRow(0, sites);
  182. function parabola(focus, directrixY) {
  183. // http://jtauber.com/blog/2008/11/08/from_focus_and_directrix_to_bezier_curve_parameters/
  184. const a = Math.sqrt(Math.pow(directrixY, 2) - Math.pow(focus.y, 2)),
  185. start = {x: (focus.x - a), y: 0},
  186. control = {x: focus.x, y: (focus.y + directrixY)},
  187. end = {x: (focus.x + a), y: 0};
  188. ctx.save();
  189. ctx.lineWidth = 1;
  190. ctx.strokeStyle = '#aaa';
  191. //ctx.fillStyle = 'rgba(255,0,0,0.5)';
  192. //ctx.fillStyle = 'white';
  193. ctx.beginPath();
  194. ctx.moveTo(start.x, start.y);
  195. ctx.quadraticCurveTo(control.x, control.y, end.x, end.y);
  196. ctx.stroke();
  197. //ctx.fill();
  198. ctx.closePath()
  199. ctx.restore();
  200. }
  201. function demo(dirY) {
  202. ctx.clearRect(0,0,canvwidth,canvheight);
  203. //ctx.save();
  204. //ctx.fillStyle = 'rgba(255,0,0,0.5)';
  205. //ctx.fillRect(0,0,canvwidth,canvheight);
  206. //ctx.restore();
  207. sites.forEach(function parabolas(site) {
  208. if( site.y < dirY ) {
  209. parabola(site, dirY);
  210. }
  211. })
  212. sites.forEach(function renderSites(site) {
  213. drawSite(site.x, site.y);
  214. });
  215. drawLine(0,dirY,canvwidth,dirY);
  216. if( dirY < canvheight + 1000 ) {
  217. setTimeout(demo, 10, dirY + 1)
  218. }
  219. else {
  220. console.log('done!');
  221. }
  222. }
  223. //demo(0);
  224. //TODO
  225. // http://hotmath.com/hotmath_help/topics/finding-the-equation-of-a-parabola-given-focus-and-directrix.html
  226. // http://www.ams.org/samplings/feature-column/fcarc-voronoi
  227. // interactive: https://rosettacode.org/wiki/Voronoi_diagram#JavaScript
  228. // cell growth: http://www.raymondhill.net/voronoi/rhill-voronoi-demo5.html
  229. // modified from: http://stackoverflow.com/a/32863775
  230. // TODO: handle the situation where the two chosen points have the same Y
  231. function calculateCircleCenter(a, b, c) {
  232. const yDelta_a = b.y - a.y,
  233. xDelta_a = b.x - a.x,
  234. yDelta_b = c.y - b.y,
  235. xDelta_b = c.x - b.x;
  236. const center = {};
  237. const aSlope = yDelta_a / xDelta_a,
  238. bSlope = yDelta_b / xDelta_b;
  239. center.x = (aSlope*bSlope*(a.y - c.y) + bSlope*(a.x + b.x) - aSlope*(b.x+c.x) )/(2* (bSlope-aSlope) );
  240. center.y = -1*(center.x - (a.x+b.x)/2)/aSlope + (a.y+b.y)/2;
  241. return center;
  242. }
  243. function circleEvent(a, b, c) {
  244. const center = calculateCircleCenter(a, b, c),
  245. radius = distance(center, a);
  246. return {
  247. center,
  248. radius,
  249. event: {
  250. x: center.x,
  251. y: center.y + radius,
  252. },
  253. };
  254. }
  255. //const circle = circleEvent(...circlePoints);
  256. //console.log('circle center', circle.center);
  257. //console.log('circle radius', circle.radius);
  258. //console.log('circle event', circle.event);
  259. //
  260. //ctx.save();
  261. //ctx.beginPath();
  262. //ctx.arc(circle.center.x, circle.center.y, circle.radius, 0, Math.PI * 2, false);
  263. //ctx.closePath();
  264. //ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
  265. //ctx.fill();
  266. //ctx.restore();
  267. //
  268. //drawSite(circle.center.x, circle.center.y, 'black');
  269. //drawSite(circle.event.x, circle.event.y, 'blue');
  270. const ss = generateSites(2),
  271. directrix = Math.max(ss[0].y, ss[1].y) + Math.floor(Math.random() * 30) + 1;
  272. ss.forEach(function renderSites(site) {
  273. drawSite(site.x, site.y);
  274. });
  275. drawLine(0, directrix, canvwidth, directrix);
  276. parabola(ss[0], directrix);
  277. parabola(ss[1], directrix);
  278. function incorrectParabolaIntersections(site1, site2, directrix) {
  279. const a = directrix,
  280. b = directrix,
  281. X = site1.x - site2.x,
  282. Y = site1.y - site2.y,
  283. denom = 8 * a * (Math.pow(b, 2) - a);
  284. if( denom === 0 ) {
  285. console.log('zero!');
  286. return [];
  287. }
  288. const a2 = Math.pow(a, 2),
  289. a4 = Math.pow(a, 4),
  290. b2 = Math.pow(b, 2),
  291. Y2 = Math.pow(Y, 2),
  292. first = -4 * a2 * Y,
  293. second = Math.sqrt((16 * a4 * Y2) - (16 * a * (b2 - a) * ((4 * b2 * X) - (a * Y2))));
  294. const option1 = (first - second) / denom,
  295. option2 = (first + second) / denom;
  296. console.log('a (d)', a, b);
  297. console.log('X', X);
  298. console.log('Y', Y);
  299. console.log('1', (a * Y2));
  300. console.log('2', (16 * a4 * Y2));
  301. console.log('3', (16 * a * (b2 - a) * ((4 * b2 * X) - (a * Y2))));
  302. console.log('4', ((16 * a4 * Y2) - (16 * a * (b2 - a) * ((4 * b2 * X) - (a * Y2)))));
  303. console.log('first', first);
  304. console.log('second', second);
  305. console.log('denom', denom);
  306. console.log('option1', option1);
  307. console.log('option2', option2);
  308. }
  309. function parabolaIntersections(site1, site2, directrix) {
  310. const a = site1.x,
  311. b = site1.y,
  312. c = site2.x,
  313. e = site2.y,
  314. d = directrix;
  315. const denom = b - e;
  316. if( denom === 0 ) {
  317. console.log('zero.');
  318. return [];
  319. }
  320. const a2 = Math.pow(a, 2),
  321. b2 = Math.pow(b, 2),
  322. c2 = Math.pow(c, 2),
  323. e2 = Math.pow(e, 2),
  324. d2 = Math.pow(d, 2),
  325. first = (-b*d+b*e+d2-d*e),
  326. second = a2 - 2*a*c + b2 - 2*b*e + c2 + e2,
  327. fs = Math.sqrt(first * second),
  328. third = a*d - a*e + b*c - c*d;
  329. const option1 = (-fs + third) / denom,
  330. option2 = (fs + third) / denom;
  331. console.log('option1', option1);
  332. console.log('option2', option2);
  333. if( option1 === option2 ) {
  334. return [option1];
  335. }
  336. return [option1, option2];
  337. }
  338. const inter = parabolaIntersections(...ss, directrix);
  339. inter.forEach(function (x) {
  340. drawLine(x, 0, x, canvheight);
  341. });