Source: Arrow.js

  1. /**
  2. *
  3. * @param {number} posX - position coordinate
  4. * @param {number} posY - position coordinate
  5. * @param {number} velocity -
  6. * @param {VelocityField} velocityField - the velocity field where the arrow lives in
  7. * @param {DistanceMap} distanceMap - the related distance map
  8. * @constructor
  9. */
  10. var Arrow = function (posX, posY, velocity, velocityField, distanceMap) {
  11. var INTERPOLATION_STEP = 0.1;
  12. var velocityField = velocityField;
  13. this.occupiedFields = [];
  14. this.currX = posX;
  15. this.currY = posY;
  16. this.len = velocity;
  17. //TODO: length: dependent on velocity/magnitude of flowField, == integration length
  18. this.isAlive = true;
  19. this.opacity = 1e-6;//0;
  20. this.strokeWidth = 2;
  21. this.respawnTime = 0;
  22. this.recolorTime = 0;
  23. this.recolor = true;
  24. this.currp0x = posX - 20;
  25. this.currp0y = posY - 30;
  26. this.currp1x = posX - 10;
  27. this.currp1y = posY - 10;
  28. this.currp2x = posX + 10;
  29. this.currp2y = posY + 10;
  30. this.currp3x = posX + 40;
  31. this.currp3y = posY + 20;
  32. this.creationDatetime = new Date().getTime(); //creation datetime or respawn datetime
  33. //Offsets to render a circle with 5 control points
  34. //y-axis
  35. this.oy1 = 2;
  36. this.oy2 = 15;
  37. this.oy3 = 18;
  38. this.oy4 = 15;
  39. this.oy5 = 2;
  40. //x-axis is to short to make a proper circle if the integration length is too small.
  41. //so "stretch" the x-part by some offset
  42. this.ox1 = -18;
  43. this.ox2 = -14;
  44. this.ox3 = 0;
  45. this.ox4 = 14;
  46. this.ox5 = 18;
  47. this.RAD2DEG = (180/Math.PI);
  48. var parent = this; //to use 'this.' variables in child functions
  49. /**
  50. * integrates an arrow
  51. */
  52. this.integrateArrow = function () {
  53. //sample grid at arrows position:
  54. //map curr position to a grid-field
  55. var idx = Math.floor(parent.currX);
  56. var idy = Math.floor(parent.currY);
  57. if(idx >= 0 && idy >= 0) {
  58. //if (!(idx < 0 || idy < 0 || idx.isNan)) {
  59. //get direction and velocity at grid field
  60. /*
  61. var fx = fieldData[idy*WIDTH+idx].dirX;
  62. var fy = fieldData[idy*WIDTH+idx].dirY;
  63. var s = fieldData[idy*WIDTH+idx].velocity; //s = arclength == length of arrow, depends on magnitude of velocity
  64. var stepsize = s/INTEGRATION_POINTS; //idea: the longer s, the bigger the integration step, the bigger the arrow
  65. */
  66. var avgPoint = velocityField.getAverageFieldData(idx, idy);
  67. var stepsize = avgPoint.velocity / 4; //4 == Number of Integration points
  68. parent.isCirc = 0;
  69. if (avgPoint.velocity < 10) {
  70. parent.isCirc = 1;
  71. parent.yCirc = parent.currY; //all controlpoints in our circle need the same height before adding offset
  72. }
  73. //Our arrows are like: p0 - p1 - curr - p2 - p3 ->
  74. //Forward integration.
  75. var p2 = rk4avg(parent.currX, parent.currY, stepsize);
  76. var p3 = rk4avg(p2[0], p2[1], stepsize);
  77. //Backward integration
  78. var p1 = rk4avg(parent.currX, parent.currY, -stepsize);
  79. var p0 = rk4avg(p1[0], p1[1], -stepsize);
  80. //Update positions at handle points
  81. parent.currp0x = p0[0];
  82. parent.currp0y = p0[1];
  83. parent.currp1x = p1[0];
  84. parent.currp1y = p1[1];
  85. //conter = currX, currY, already done in advection step
  86. parent.currp2x = p2[0];
  87. parent.currp2y = p2[1];
  88. parent.currp3x = p3[0];
  89. parent.currp3y = p3[1];
  90. }
  91. }
  92. /**
  93. * advects an arrow and calls checkDomain
  94. * @param {number} dt - delta time
  95. */
  96. this.advectArrow = function (dt) {
  97. //map curr position to a grid-field
  98. var idx = Math.floor(parent.currX);
  99. var idy = Math.floor(parent.currY);
  100. var avgPoint = velocityField.getAverageFieldData(idx, idy);
  101. parent.currX = (parent.currX + avgPoint.dirX * dt);
  102. parent.currY = (parent.currY + avgPoint.dirY * dt);
  103. /* TODO: If lifetime expired, reduce opacity*/
  104. /* parent.opacity = parent.opacity - 0.1; */
  105. checkDomain();
  106. }
  107. /**
  108. * checks if arrow is colliding with another arrow
  109. * if no collision --> marks this area in the velocity field as occupied and returns true, so the arrowManager inserts the arrow
  110. * @returns {boolean} returns true if arrow is still alive, false if arrow is dead.
  111. */
  112. this.checkAndMarkOccupied = function() {
  113. //reset
  114. parent.occupiedFields = [];
  115. //get the entries at the handlepoints
  116. var distanceMapEntry;
  117. distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currp0x/distanceMap.getDistanceMapDivisor()),Math.floor(parent.currp0y/distanceMap.getDistanceMapDivisor()));
  118. if(distanceMapEntry != null)
  119. parent.occupiedFields.push(distanceMapEntry);
  120. distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currp1x/distanceMap.getDistanceMapDivisor()),Math.floor(parent.currp1y/distanceMap.getDistanceMapDivisor()));
  121. if(distanceMapEntry != null)
  122. parent.occupiedFields.push(distanceMapEntry);
  123. distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currX/distanceMap.getDistanceMapDivisor()), Math.floor(parent.currY/distanceMap.getDistanceMapDivisor()));
  124. if(distanceMapEntry != null)
  125. parent.occupiedFields.push(distanceMapEntry);
  126. distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currp2x/distanceMap.getDistanceMapDivisor()),Math.floor(parent.currp2y/distanceMap.getDistanceMapDivisor()));
  127. if(distanceMapEntry != null)
  128. parent.occupiedFields.push(distanceMapEntry);
  129. distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currp3x/distanceMap.getDistanceMapDivisor()),Math.floor(parent.currp3y/distanceMap.getDistanceMapDivisor()));
  130. if(distanceMapEntry != null)
  131. parent.occupiedFields.push(distanceMapEntry);
  132. //console.log("distanceMapEntry for curr3 x: " + distanceMapEntry.xPos + " y: " + distanceMapEntry.yPos);
  133. /*
  134. //neighbors above/ under handlepoints and sideways
  135. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp0x/4),Math.ceil((parent.currp0y-1)/4)));
  136. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp1x/4),Math.ceil((parent.currp1y-1)/4)));
  137. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currX/4), Math.ceil((parent.currY-1)/4)));
  138. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp2x/4),Math.ceil((parent.currp2y-1)/4)));
  139. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp3x/4),Math.ceil((parent.currp3y-1)/4)));
  140. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp0x/4),Math.ceil((parent.currp0y+1)/4)));
  141. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp1x/4),Math.ceil((parent.currp1y+1)/4)));
  142. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currX/4), Math.ceil((parent.currY+1)/4)));
  143. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp2x/4),Math.ceil((parent.currp2y+1)/4)));
  144. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp3x/4),Math.ceil((parent.currp3y+1)/4)));
  145. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil((parent.currp0x-1)/4),Math.ceil(parent.currp0y/4)));
  146. parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil((parent.currp3x+1)/4),Math.ceil(parent.currp3y/4)));
  147. */
  148. //TODO activate linear interpolation
  149. //linear interpolation between handle points
  150. //interpolation between currp0 and currp1
  151. var step = INTERPOLATION_STEP;
  152. while(step < 1) {
  153. var interpolatedPoint = {};
  154. interpolatedPoint.x = parent.linearInterpolation(parent.currp0x, parent.currp1x, step);
  155. interpolatedPoint.y = parent.linearInterpolation(parent.currp0y, parent.currp1y, step);
  156. distanceMapEntry = distanceMap.getDistanceMapEntry(Math.ceil(interpolatedPoint.x/distanceMap.getDistanceMapDivisor()),Math.ceil(interpolatedPoint.y/distanceMap.getDistanceMapDivisor()));
  157. if(distanceMapEntry != null)
  158. parent.occupiedFields.push(distanceMapEntry);
  159. step += INTERPOLATION_STEP;
  160. }
  161. //interpolation between currp1 and currp2
  162. step = INTERPOLATION_STEP;
  163. while(step < 1) {
  164. var interpolatedPoint = {};
  165. interpolatedPoint.x = parent.linearInterpolation(parent.currp1x, parent.currp2x, step);
  166. interpolatedPoint.y = parent.linearInterpolation(parent.currp1y, parent.currp2y, step);
  167. distanceMapEntry = distanceMap.getDistanceMapEntry(Math.ceil(interpolatedPoint.x/distanceMap.getDistanceMapDivisor()),Math.ceil(interpolatedPoint.y/distanceMap.getDistanceMapDivisor()));
  168. if(distanceMapEntry != null)
  169. parent.occupiedFields.push(distanceMapEntry);
  170. step += INTERPOLATION_STEP;
  171. }
  172. //interpolation between currp2 and currp3
  173. step= INTERPOLATION_STEP
  174. while(step < 1) {
  175. var interpolatedPoint = {};
  176. interpolatedPoint.x = parent.linearInterpolation(parent.currp2x, parent.currp3x, step);
  177. interpolatedPoint.y = parent.linearInterpolation(parent.currp2y, parent.currp3y, step);
  178. distanceMapEntry = distanceMap.getDistanceMapEntry(Math.ceil(interpolatedPoint.x/distanceMap.getDistanceMapDivisor()),Math.ceil(interpolatedPoint.y/distanceMap.getDistanceMapDivisor()));
  179. if(distanceMapEntry != null)
  180. parent.occupiedFields.push(distanceMapEntry);
  181. step += INTERPOLATION_STEP;
  182. }
  183. //if any of those potential fields is marked, kill the arrow!
  184. for (var i = 0; i < parent.occupiedFields.length; i++) {
  185. var entryTemp = parent.occupiedFields[i];
  186. if (entryTemp.v == 1) {
  187. //kill arrow
  188. //console.log("killing arrow after collision");
  189. parent.isAlive = false;
  190. parent.opacity = 1e-6;//0;
  191. parent.strokeWidth = 1e-6;//0;
  192. return false;
  193. }
  194. };
  195. //set entries in distance map to occupied
  196. for (var i = 0; i < parent.occupiedFields.length; i++) {
  197. var entryTemp = parent.occupiedFields[i];
  198. var x = entryTemp.xPos;
  199. var y = entryTemp.yPos;
  200. distanceMap.setDistanceMapValue(x, y, 1);
  201. };
  202. return true;
  203. }
  204. /**
  205. * linear interpolation between two numbers
  206. * @param {number} start - start point
  207. * @param {number} end - end point
  208. * @param {number} step - the interpolation step
  209. * @returns {number} interpolated point at interpolation step
  210. */
  211. this.linearInterpolation = function(start, end, step) {
  212. return (start * step) + end * (1-step);
  213. }
  214. //TODO: More clever way of respawning to reduce visual artifacts?
  215. /**
  216. * Respawning the arrow gives it a new position at the moment and resets the handle points
  217. * @param {number} posX - x position
  218. * @param {number} posY - y position
  219. */
  220. this.respawn = function(posX, posY) {
  221. parent.currX = posX;
  222. parent.currY = posY;
  223. parent.len = 0;
  224. parent.currp0x = 0;
  225. parent.currp0y = 0;
  226. parent.currp1x = 0;
  227. parent.currp1y = 0;
  228. parent.currp2x = 0;
  229. parent.currp2y = 0;
  230. parent.currp3x = 0;
  231. parent.currp3y = 0;
  232. //reinit
  233. parent.isAlive = true;
  234. parent.opacity = 1e-6;//0;
  235. parent.strokeWidth = 1e-6;//0;
  236. parent.respawnTime = 0;
  237. parent.recolorTime = 0;
  238. parent.recolor = true;
  239. parent.creationDatetime = new Date().getTime(); //creation datetime or respawn datetime
  240. }
  241. //private functions
  242. /**
  243. * check if boundary of domain reached, if yes --> kill (reset)
  244. * @returns {boolean} false if arrow has reached boundary
  245. */
  246. var checkDomain = function () {
  247. if (parent.currX >= velocityField.widthOfField - 1 || parent.currY >= velocityField.heightOfField - 1 || parent.currX < 0 || parent.currY < 0) {
  248. //kill arrow if it runs out of the field
  249. parent.isAlive = false;
  250. parent.opacity = 1e-6;//0;
  251. parent.strokeWidth = 1e-6//;0;
  252. return false;
  253. }
  254. }
  255. /**
  256. * RK4 using averaged values
  257. * @param {number} x - "real" coordinate of the current point
  258. * @param {number} y - "real" coordinate of the current point
  259. * @param {number} dt - stepsize
  260. * @returns [x_next, y_next] the next coordinates
  261. */
  262. var rk4avg = function (x, y, dt) {
  263. //get indices from coordinates for lookup in or 1D-array
  264. var idx = Math.floor(x);
  265. var idy = Math.floor(y);
  266. var pos = idy * velocityField.widthOfField + idx;
  267. if (pos < 0 || pos >= (velocityField.widthOfField * velocityField.heightOfField) - 1) {
  268. return [-1, -1];
  269. }
  270. var aData = velocityField.getAverageFieldData(idx, idy);
  271. var ax = dt * aData.dirX;
  272. var ay = dt * aData.dirY;
  273. var posA = Math.floor(y + (ay * 0.5)) * velocityField.widthOfField + Math.floor(x + (ax * 0.5));
  274. if (posA < 0 || posA >= (velocityField.widthOfField * velocityField.heightOfField) - 1) {
  275. return [-1, -1];
  276. }
  277. var bData = velocityField.getAverageFieldData(Math.floor(x + (ax * 0.5)), Math.floor(y + (ay * 0.5)));
  278. var bx = dt * bData.dirX;
  279. var by = dt * bData.dirY;
  280. var posB = Math.floor(y + (by * 0.5)) * velocityField.widthOfField + Math.floor(x + (bx * 0.5));
  281. if (posB < 0 || posB >= (velocityField.widthOfField * velocityField.heightOfField) - 1) {
  282. return [-1, -1];
  283. }
  284. var cData = velocityField.getAverageFieldData(Math.floor(x + (bx * 0.5)), Math.floor(y + (by * 0.5)));
  285. var cx = dt * cData.dirX;
  286. var cy = dt * cData.dirY;
  287. var posC = Math.floor(y + cy) * velocityField.widthOfField + Math.floor(x + cx);
  288. if (posC < 0 || posC >= (velocityField.widthOfField * velocityField.heightOfField) - 1) {
  289. return [-1, -1];
  290. }
  291. var dData = velocityField.getAverageFieldData(Math.floor(x + cx), Math.floor(y + cy));
  292. var dx = dt * dData.dirX;
  293. var dy = dt * dData.dirY;
  294. var xNext = idx + (ax + 2 * bx + 2 * cx + dx) / 6; //no *dt here?
  295. var yNext = idy + (ay + 2 * by + 2 * cy + dy) / 6;
  296. return [Math.floor(xNext), Math.floor(yNext)];
  297. }
  298. /**
  299. * determines the angle between p2p3 and x-axis, to rotate the arowhead accordingly
  300. * @returns {number} the angle
  301. */
  302. this.calcArrowheadRotation = function () {
  303. var x = parent.currp3x - parent.currp2x;
  304. var y = parent.currp3y - parent.currp2y;
  305. var len = Math.sqrt(x*x+y*y);
  306. if(len <= 0){
  307. len = 0.000000001;
  308. }
  309. var xNorm = x/len;
  310. //var yNorm = y/len;
  311. //x-Axis = (1,0), so dotproduct would be: 1*xNorm + 0*yNorm
  312. var angle = Math.acos(xNorm)* this.RAD2DEG;
  313. return angle;
  314. }
  315. /**
  316. *
  317. * @returns {number} the distance between first and last handle point
  318. */
  319. this.getArrowLength = function () {
  320. var xLength = parent.currp3x - parent.currp0x;
  321. var yLength = parent.currp3y - parent.currp0y;
  322. var arrowLength = Math.sqrt(xLength*xLength + yLength*yLength);
  323. return arrowLength;
  324. }
  325. /**
  326. *
  327. * @returns {number} the lifetime in milliseconds
  328. */
  329. this.getLifetime = function () {
  330. if(parent.isAlive) {
  331. return new Date().getTime() - parent.creationDatetime;
  332. }else return 0;
  333. }
  334. }
  335. /*
  336. //Runge kutta second order
  337. function rk2(x, y, dt) { //x,y ... "real" coordinates of the current point
  338. //dt... stepsize
  339. //si+1 = si + F(si + F(si)*dt/2 )*dt
  340. //get indices from coordinates for lookup in or 1D-array
  341. var idx = Math.floor(x);
  342. var idy = Math.floor(y);
  343. var pos = idy * WIDTH + idx;
  344. var pos2 = Math.floor(y + fieldData[pos] * dt * 0.5) * WIDTH + Math.floor(x + fieldData[pos] * dt * 0.5);
  345. var xNext = x + fieldData[pos2] * dt;
  346. var yNext = y + fieldData[pos2] * dt;
  347. return [xNext, yNext];
  348. }
  349. */
  350. /*
  351. //1D example: http://mtdevans.com/2013/05/fourth-order-runge-kutta-algorithm-in-javascript-with-demo/
  352. //CODE VIA slides: https://www.cg.tuwien.ac.at/courses/Visualisierung/Folien/VisVO-2008-FlowVis-2-6Slides.pdf
  353. //Runge kutta 4th order (THE method)
  354. function rk4(x, y, dt) { //x,y ... "real" coordinates of the current point
  355. //dt... stepsize
  356. //get indices from coordinates for lookup in or 1D-array
  357. var idx = Math.floor(x);
  358. var idy = Math.floor(y);
  359. var pos = idy * WIDTH + idx;
  360. if (pos < 0 || pos >= (WIDTH * HEIGHT) - 1) {
  361. return [-1, -1];
  362. }
  363. //console.log('pos' + pos);
  364. // a = dt*F(si)
  365. //console.log('pos a: ' + pos);
  366. var ax = dt * fieldData[Math.floor(pos)].dirX;
  367. var ay = dt * fieldData[Math.floor(pos)].dirY;
  368. //b = dt*F(si+a/2)
  369. //console.log('pos b : '+ Math.floor(pos+(ax*0.5)));
  370. var posA = Math.floor(y + (ay * 0.5)) * WIDTH + Math.floor(x + (ax * 0.5));
  371. if (posA < 0 || posA >= (WIDTH * HEIGHT) - 1) {
  372. return [-1, -1];
  373. }
  374. //console.log('posA ' + posA);
  375. var bx = dt * fieldData[posA].dirX;
  376. var by = dt * fieldData[posA].dirY;
  377. //console.log('pos c: ' + Math.floor(pos+(bx*0.5)));
  378. var posB = Math.floor(y + (by * 0.5)) * WIDTH + Math.floor(x + (bx * 0.5));
  379. if (posB < 0 || posB >= (WIDTH * HEIGHT) - 1) {
  380. return [-1, -1];
  381. }
  382. var cx = dt * fieldData[posB].dirX;
  383. var cy = dt * fieldData[posB].dirY;
  384. //console.log('pos d:' + Math.floor(pos+cx));
  385. var posC = Math.floor(y + cy) * WIDTH + Math.floor(x + cx);
  386. if (posC < 0 || posC >= (WIDTH * HEIGHT) - 1) {
  387. return [-1, -1];
  388. }
  389. var dx = dt * fieldData[posC].dirX;
  390. var dy = dt * fieldData[posC].dirY;
  391. var xNext = idx + (ax + 2 * bx + 2 * cx + dx) / 6; //no *dt here?
  392. var yNext = idy + (ay + 2 * by + 2 * cy + dy) / 6;
  393. return [Math.floor(xNext), Math.floor(yNext)];
  394. }
  395. */