1. 1 : /**
  2. 2 : * TextualArc is a visualization of the terms in a document that includes a weighted centroid of terms and an arc that follows the terms in document order.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "docIndex": null,
  8. 8 : * "minRawFreq": null,
  9. 9 : * "speed": null,
  10. 10 : * "stopList": null
  11. 11 : * };
  12. 12 : *
  13. 13 : * loadCorpus("austen").tool("textualarc", config);
  14. 14 : *
  15. 15 : * @class TextualArc
  16. 16 : * @tutorial textualarc
  17. 17 : * @memberof Tools
  18. 18 : */
  19. 19 : Ext.define('Voyant.panel.TextualArc', {
  20. 20 : extend: 'Ext.panel.Panel',
  21. 21 : mixins: ['Voyant.panel.Panel'],
  22. 22 : alias: 'widget.textualarc',
  23. 23 : statics: {
  24. 24 : i18n: {
  25. 25 : },
  26. 26 : api: {
  27. 27 : /**
  28. 28 : * @memberof Tools.TextualArc
  29. 29 : * @instance
  30. 30 : * @property {stopList}
  31. 31 : * @default
  32. 32 : */
  33. 33 : stopList: 'auto',
  34. 34 :
  35. 35 : /**
  36. 36 : * @memberof Tools.TextualArc
  37. 37 : * @instance
  38. 38 : * @property {docIndex}
  39. 39 : * @default
  40. 40 : */
  41. 41 : docIndex: 0,
  42. 42 :
  43. 43 : /**
  44. 44 : * @memberof Tools.TextualArc
  45. 45 : * @instance
  46. 46 : * @property {Number} speed How fast to animate the visualization.
  47. 47 : * @default
  48. 48 : */
  49. 49 : speed: 50,
  50. 50 :
  51. 51 : /**
  52. 52 : * @memberof Tools.TextualArc
  53. 53 : * @instance
  54. 54 : * @property {Number} minRawFreq The minimum raw frequency of terms to be considered.
  55. 55 : * @default
  56. 56 : */
  57. 57 : minRawFreq: 2
  58. 58 :
  59. 59 : },
  60. 60 : glyph: 'xf06e@FontAwesome'
  61. 61 : },
  62. 62 : config: {
  63. 63 : options: [{xtype: 'stoplistoption'},{
  64. 64 : xtype: 'container',
  65. 65 : items: {
  66. 66 : xtype: 'numberfield',
  67. 67 : name: 'minRawFreq',
  68. 68 : minValue: 1,
  69. 69 : maxValue: 10,
  70. 70 : value: 2,
  71. 71 : labelWidth: 150,
  72. 72 : labelAlign: 'right',
  73. 73 : initComponent: function() {
  74. 74 : var panel = this.up('window').panel;
  75. 75 : this.fieldLabel = panel.localize(this.fieldLabel);
  76. 76 : this.on("afterrender", function(cmp) {
  77. 77 : Ext.tip.QuickTipManager.register({
  78. 78 : target: cmp.getEl(),
  79. 79 : text: panel.localize('minRawFreqTip')
  80. 80 : });
  81. 81 : });
  82. 82 : this.on('beforedestroy', function(cmp) {
  83. 83 : Ext.tip.QuickTipManager.unregister(cmp.getEl());
  84. 84 : });
  85. 85 : this.callParent(arguments);
  86. 86 : },
  87. 87 : fieldLabel: 'minRawFreq'
  88. 88 : }
  89. 89 : }],
  90. 90 : perim: [],
  91. 91 : diam: undefined
  92. 92 : },
  93. 93 :
  94. 94 : tokensFetch: 500,
  95. 95 :
  96. 96 : constructor: function() {
  97. 97 :
  98. 98 : this.mixins['Voyant.util.Localization'].constructor.apply(this, arguments);
  99. 99 : this.config.options[1].fieldLabel = this.localize(this.config.options[1].fieldLabel);
  100. 100 : Ext.apply(this, {
  101. 101 : title: this.localize('title'),
  102. 102 : html: '<canvas width="800" height="600"></canvas>',
  103. 103 : dockedItems: [{
  104. 104 : dock: 'bottom',
  105. 105 : xtype: 'toolbar',
  106. 106 : overflowHandler: 'scroller',
  107. 107 : items: [{
  108. 108 : xtype: 'combo',
  109. 109 : itemId: 'search',
  110. 110 : queryMode: 'local',
  111. 111 : displayField: 'term',
  112. 112 : valueField: 'term',
  113. 113 : width: 90,
  114. 114 : emptyText: this.localize('search'),
  115. 115 : forceSelection: true,
  116. 116 : disabled: true
  117. 117 : },{
  118. 118 : xtype: 'documentselectorbutton',
  119. 119 : singleSelect: true
  120. 120 : },{
  121. 121 : xtype: 'slider',
  122. 122 : fieldLabel: this.localize('speed'),
  123. 123 : labelAlign: 'right',
  124. 124 : labelWidth: 40,
  125. 125 : width: 100,
  126. 126 : increment: 1,
  127. 127 : minValue: 0,
  128. 128 : maxValue: 100,
  129. 129 : value: 30,
  130. 130 : listeners: {
  131. 131 : render: function(cmp) {
  132. 132 : cmp.setValue(parseInt(this.getApiParam("speed")));
  133. 133 : Ext.tip.QuickTipManager.register({
  134. 134 : target: cmp.getEl(),
  135. 135 : text: this.localize('speedTip')
  136. 136 : });
  137. 137 :
  138. 138 : },
  139. 139 : beforedestroy: function(cmp) {
  140. 140 : Ext.tip.QuickTipManager.unregister(cmp.getEl());
  141. 141 : },
  142. 142 : changecomplete: function(cmp, val) {
  143. 143 : this.setApiParam('speed', val);
  144. 144 : this.isReading = val!==0
  145. 145 : this.draw();
  146. 146 : },
  147. 147 : scope: this
  148. 148 : }
  149. 149 : },{xtype: 'tbfill'}, {
  150. 150 : xtype: 'tbtext',
  151. 151 : html: this.localize('adaptation')
  152. 152 : }]
  153. 153 : }]
  154. 154 : });
  155. 155 : this.callParent(arguments);
  156. 156 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  157. 157 :
  158. 158 : this.on('boxready', function(cmp) {
  159. 159 : var canvas = this.getTargetEl().dom.querySelector("canvas");
  160. 160 : this.draw(canvas);
  161. 161 :
  162. 162 : canvas.addEventListener('mousemove', function(evt) {
  163. 163 : if (cmp.documentTerms) {
  164. 164 : var rect = canvas.getBoundingClientRect(), x = evt.clientX - rect.left, y = evt.clientY - rect.top;
  165. 165 :
  166. 166 : var currentTerms = {};
  167. 167 : cmp.documentTerms.each(function(documentTerm) {
  168. 168 : var dx = documentTerm.get('x'), dy = documentTerm.get('y');
  169. 169 : if (dx>x-15 && dx<x+15 && dy>y-15 && dy<y+15) {
  170. 170 : currentTerms[documentTerm.getTerm()] = true;
  171. 171 : return false;
  172. 172 : }
  173. 173 : })
  174. 174 :
  175. 175 : // no need to do anything if there are no current terms and none found
  176. 176 : if (Object.keys(cmp.currentTerms || {}).length==0 && Object.keys(currentTerms).length==0) {return;}
  177. 177 :
  178. 178 : cmp.currentTerms = currentTerms;
  179. 179 : cmp.draw(canvas); // otherwise redraw
  180. 180 : }
  181. 181 :
  182. 182 : }, false);
  183. 183 : })
  184. 184 :
  185. 185 : this.on('loadedCorpus', function(src, corpus) {
  186. 186 : this.loadDocument();
  187. 187 : }, this);
  188. 188 :
  189. 189 : this.on("documentselected", function(src, doc) {
  190. 190 : this.setApiParam('docIndex', this.getCorpus().getDocument(doc).getIndex());
  191. 191 : this.loadDocument();
  192. 192 : });
  193. 193 :
  194. 194 : this.on("resize", function() {
  195. 195 : var gutter = 20,
  196. 196 : availableWidth = this.getTargetEl().getWidth() - gutter - gutter,
  197. 197 : availableHeight = this.getTargetEl().getHeight() - gutter - gutter,
  198. 198 : diam = Math.max(availableWidth, availableHeight), rad = diam /2,
  199. 199 : ratio = Math.min(availableWidth, availableHeight) / diam,
  200. 200 : canvas = this.getTargetEl().dom.querySelector("canvas");
  201. 201 :
  202. 202 : canvas.width = this.getTargetEl().getWidth();
  203. 203 : canvas.height = this.getTargetEl().getHeight();
  204. 204 : this.setDiam(diam);
  205. 205 : this.setPerim([]);
  206. 206 : var i = parseInt(diam*.75)
  207. 207 : while (this.getPerim().length<diam) {
  208. 208 : this.getPerim().push({
  209. 209 : x: gutter+(availableWidth/2)+(rad * (availableWidth>availableHeight ? 1 : ratio) * Math.cos(2 * Math.PI * i / diam)),
  210. 210 : y: gutter+(availableHeight/2)+(rad * (availableHeight>availableWidth ? 1 : ratio) * Math.sin(2 * Math.PI * i / diam))
  211. 211 : })
  212. 212 : if (i++==diam) {i=0;}
  213. 213 : }
  214. 214 :
  215. 215 : // TODO clear previous/current drawing
  216. 216 : })
  217. 217 : },
  218. 218 :
  219. 219 : draw: function(canvas, ctx) {
  220. 220 : canvas = canvas || this.getTargetEl().dom.querySelector("canvas");
  221. 221 : ctx = ctx || canvas.getContext("2d");
  222. 222 : ctx.clearRect(0,0,canvas.width,canvas.height);
  223. 223 : ctx.fillStyle = "rgba(0,0,0,.1)";
  224. 224 : this.getPerim().forEach(function(p,i) {
  225. 225 : if (i%3==0) {
  226. 226 : ctx.fillRect(p.x-5,p.y,10,1)
  227. 227 : }
  228. 228 : })
  229. 229 : if (this.documentTerms) {
  230. 230 : this.drawTerms(canvas, ctx);
  231. 231 : this.drawReading(canvas,ctx);
  232. 232 : if (this.isReading) {
  233. 233 : var me = this;
  234. 234 : setTimeout(function() {
  235. 235 : me.draw();
  236. 236 : }, 10)
  237. 237 : }
  238. 238 : }
  239. 239 : },
  240. 240 :
  241. 241 : drawReading: function(canvas, ctx) {
  242. 242 : ctx = ctx || this.getTargetEl().dom.querySelector("canvas").getContext("2d");
  243. 243 : var delay = 2000-(parseInt(this.getApiParam('speed'))*1999/100);
  244. 244 : if (this.isReading && this.documentTerms) {
  245. 245 : var current = parseInt(this.readingIndex * this.getPerim().length / this.lastToken);
  246. 246 : ctx.fillStyle = "purple";
  247. 247 : ctx.fillRect(this.getPerim()[current].x,this.getPerim()[current].y, 5, 5)
  248. 248 : var first = this.readingStartTime == undefined;
  249. 249 : this.readingStartTime = this.readingStartTime || new Date().getTime();
  250. 250 : var delta = this.readingStartTime+delay-new Date().getTime();
  251. 251 : if (this.sourceTerm && this.targetTerm) {
  252. 252 : var maxTail = 10;
  253. 253 : if (first || delta<=0) {
  254. 254 : this.previousBeziers = this.previousBeziers || []; // this should be reset by tokens reader during first read
  255. 255 : var sx = this.sourceTerm.get('x'), sy = this.sourceTerm.get('y'), tx = this.targetTerm.get('x'), ty = this.targetTerm.get('y'),
  256. 256 : px = this.previousTerm ? this.previousTerm.get('x') : sx, py = this.previousTerm ? this.previousTerm.get('y') : sy,
  257. 257 : round = 100, multiplier = .3;
  258. 258 :
  259. 259 : var ix, iy, xd = Math.max(round, Math.abs(sx-tx) * .5), yd = Math.max(round, Math.abs(sy-ty) * .5);
  260. 260 : ix = sx > tx ? sx - xd : sx + xd;
  261. 261 : iy = ty > sy ? sy + yd : sy - yd;
  262. 262 : this.previousBeziers.unshift([sx,sy,ix,iy,tx,ty]);
  263. 263 : if (this.previousBeziers.length>maxTail) {this.previousBeziers.pop()}
  264. 264 : }
  265. 265 :
  266. 266 : for (var i=0; i<this.previousBeziers.length; i++) {
  267. 267 : ctx.strokeStyle="rgba(0,0,255,"+(1-(i*.1))+")";
  268. 268 : var start = i+1 == this.previousBeziers.length ? 1-(delta/delay) : 0;
  269. 269 : var end = i==0 ? 1-(delta/delay) : 1;
  270. 270 : this.drawBezierSplit.apply(this, Ext.Array.merge([ctx], this.previousBeziers[i], [start], [end]));
  271. 271 : }
  272. 272 : if (delta<=0) {
  273. 273 : this.readingStartTime = undefined;
  274. 274 : this.read();
  275. 275 : }
  276. 276 : }
  277. 277 : var nextReadingIndex = this.readingIndex+1;
  278. 278 : for (var len=this.tokens.getCount(); nextReadingIndex<len; nextReadingIndex++) {
  279. 279 : if (this.tokens.getAt(nextReadingIndex).getTerm().toLowerCase()==this.targetTerm.getTerm()) {
  280. 280 : break;
  281. 281 : }
  282. 282 : }
  283. 283 : var startReadingIndex = nextReadingIndex-parseInt(delta*(nextReadingIndex-this.readingIndex)/delay), count = this.tokens.getCount();
  284. 284 : for (; startReadingIndex<nextReadingIndex; startReadingIndex++) {
  285. 285 : if (startReadingIndex < count && this.tokens.getAt(startReadingIndex).isWord()) {
  286. 286 : break;
  287. 287 : }
  288. 288 : }
  289. 289 : var tokens = this.tokens.getRange(startReadingIndex, len=Math.min(this.readingIndex+50, this.tokens.getCount())).map(function(token) {
  290. 290 : return token.getTerm();
  291. 291 : })
  292. 292 : ctx.font = "14px sans-serif";
  293. 293 : ctx.fillStyle = "rgba(0,0,0,.5)";
  294. 294 : ctx.textAlign = "left";
  295. 295 : ctx.fillText(tokens.join(""), canvas.width/4, canvas.height-5);
  296. 296 : ctx.clearRect(canvas.width*.75, canvas.height-20, canvas.width, 30)
  297. 297 : } else if (this.documentTerms && this.documentTerms.getCount()<this.documentTerms.getTotalCount()) {
  298. 298 : var x = canvas.width / 4;
  299. 299 : ctx.strokeStyle="rgba(0,0,0,.5)";
  300. 300 : ctx.fillStyle = "rgba(0,0,0,.2)";
  301. 301 : ctx.strokeRect(x,canvas.height-12,x*2,10);
  302. 302 : ctx.fillRect(x,canvas.height-12,(this.documentTerms.getCount()*x*2)/this.documentTerms.getTotalCount(),10);
  303. 303 : }
  304. 304 : },
  305. 305 :
  306. 306 : drawTerms: function(canvas, ctx) {
  307. 307 : canvas = canvas || this.getTargetEl().dom.querySelector("canvas");
  308. 308 : ctx = ctx || canvas.getContext("2d");
  309. 309 : ctx.textAlign = "center";
  310. 310 : if (this.documentTerms && this.getPerim().length > 0) {
  311. 311 : this.documentTerms.each(function(documentTerm) {
  312. 312 : var me = this, freq = documentTerm.getRawFreq(), term = documentTerm.getTerm(),
  313. 313 : x = documentTerm.get('x'), y = documentTerm.get('y');
  314. 314 : isCurrentTerm = me.currentTerms && (term in me.currentTerms);
  315. 315 : isReadingTerm = this.sourceTerm && this.sourceTerm.getTerm() == term;
  316. 316 : ctx.font = ((Math.log(freq)*(canvas.width*10/800)/Math.log(this.maxRawFreq))+(isCurrentTerm || isReadingTerm ? 10 : 5)) + "px sans-serif";
  317. 317 : if (isCurrentTerm) {
  318. 318 : ctx.fillStyle = "red";
  319. 319 : } else if (isReadingTerm) {
  320. 320 : ctx.fillStyle = "blue";
  321. 321 : } else {
  322. 322 : ctx.fillStyle = "rgba(0,0,0,"+((freq*.9/this.maxRawFreq)+.1)+")";
  323. 323 : }
  324. 324 : if (isCurrentTerm || isReadingTerm) {
  325. 325 : ctx.strokeStyle = isCurrentTerm ? "rgba(255,0,0,.2)" : "rgba(0,255,0,.4)";
  326. 326 : documentTerm.getDistributions().forEach(function(d, i) {
  327. 327 : if (d>0 && this.getPerim()[i]) {
  328. 328 : ctx.beginPath();
  329. 329 : ctx.moveTo(x, y);
  330. 330 : ctx.lineTo(this.getPerim()[i].x,this.getPerim()[i].y);
  331. 331 : ctx.stroke();
  332. 332 : }
  333. 333 : }, this)
  334. 334 : }
  335. 335 : ctx.fillText(term, x, y);
  336. 336 :
  337. 337 : }, this)
  338. 338 : }
  339. 339 : },
  340. 340 :
  341. 341 : read: function(index) {
  342. 342 : if (Ext.isNumber(index)) {this.readingIndex=index;}
  343. 343 : else {this.readingIndex++;}
  344. 344 : if (this.sourceTerm) {this.previousTerm=this.sourceTerm;}
  345. 345 : for (var i=this.readingIndex, len = this.tokens.getCount(); i<len; i++) {
  346. 346 : var token = this.tokens.getAt(i), term = token.getTerm().toLowerCase();
  347. 347 : if (term in this.termsMap) {
  348. 348 : this.sourceTerm = this.termsMap[term];
  349. 349 : if (this.sourceTerm.getRawFreq()>=1) {
  350. 350 : this.readingIndex = i;
  351. 351 : break
  352. 352 : }
  353. 353 : }
  354. 354 : }
  355. 355 : for (var i=this.readingIndex+1, len = this.tokens.getCount(); i<len; i++) {
  356. 356 : var token = this.tokens.getAt(i), term = token.getTerm().toLowerCase();
  357. 357 : if (term in this.termsMap) {
  358. 358 : this.targetTerm = this.termsMap[term];
  359. 359 : if (this.targetTerm.getRawFreq()>=1) {
  360. 360 : break;
  361. 361 : }
  362. 362 : }
  363. 363 : }
  364. 364 : if (!this.tokensLoading && this.tokens.getCount()-this.readingIndex<this.tokensFetch) {
  365. 365 : this.fetchMoreTokens();
  366. 366 : }
  367. 367 : this.draw();
  368. 368 : },
  369. 369 :
  370. 370 :
  371. 371 : loadDocument: function() {
  372. 372 : if (this.documentTerms) {this.documentTerms.destroy();this.documentTerms=undefined;}
  373. 373 : this.termsMap = {};
  374. 374 : this.draw();
  375. 375 : var doc = this.getCorpus().getDocument(parseInt(this.getApiParam('docIndex')));
  376. 376 : // if we're not in a tab panel, set the document title as part of the header
  377. 377 : if (!this.up("tabpanel")) {
  378. 378 : this.setTitle(this.localize('title') + " <span class='subtitle'>"+doc.getFullLabel()+"</span>");
  379. 379 : }
  380. 380 : this.lastToken = parseInt(doc.get('lastTokenStartOffset-lexical'));
  381. 381 : this.documentTerms = doc.getDocumentTerms({
  382. 382 : proxy: {
  383. 383 : extraParams: {
  384. 384 : stopList: this.getApiParam('stopList'),
  385. 385 : bins: this.getDiam(),
  386. 386 : withDistributions: 'raw',
  387. 387 : minRawFreq: parseInt(this.getApiParam('minRawFreq'))
  388. 388 : }
  389. 389 : }
  390. 390 : });
  391. 391 : var search = this.queryById('search');
  392. 392 : search.setDisabled(true);
  393. 393 : search.setStore(this.documentTerms);
  394. 394 : this.fetchMoreDocumentTerms();
  395. 395 : },
  396. 396 :
  397. 397 : fetchMoreDocumentTerms: function() {
  398. 398 : if (!this.documentTerms) {this.loadDocument(); return;}
  399. 399 : this.documentTerms.load({
  400. 400 : params: {
  401. 401 : start: this.documentTerms.getCount(),
  402. 402 : limit: this.documentTerms.getCount() == 0 ? 10 : 250
  403. 403 : },
  404. 404 : callback: function(records) {
  405. 405 : if (records.length>0) {
  406. 406 : this.maxRawFreq = this.documentTerms.max('rawFreq');
  407. 407 : records.forEach(function(documentTerm) {
  408. 408 : var x = y = 0;
  409. 409 : documentTerm.get('distributions').forEach(function(d, i) {
  410. 410 : x += (this.getPerim()[i].x*d);
  411. 411 : y += (this.getPerim()[i].y*d);
  412. 412 : }, this)
  413. 413 : documentTerm.set('x', x/documentTerm.getRawFreq());
  414. 414 : documentTerm.set('y', y/documentTerm.getRawFreq());
  415. 415 : }, this);
  416. 416 : Ext.Function.defer(this.fetchMoreDocumentTerms, 0, this);
  417. 417 : this.draw();
  418. 418 : } else {
  419. 419 : this.queryById('search').setDisabled(false);
  420. 420 : this.termsMap = {};
  421. 421 : this.documentTerms.each(function(documentTerm) {
  422. 422 : this.termsMap[documentTerm.getTerm()] = documentTerm;
  423. 423 : }, this)
  424. 424 : if (this.tokens) {this.tokens.removeAll(true)}
  425. 425 : this.fetchMoreTokens();
  426. 426 : }
  427. 427 : },
  428. 428 : addRecords: true,
  429. 429 : scope: this
  430. 430 : })
  431. 431 : },
  432. 432 :
  433. 433 : fetchMoreTokens: function() {
  434. 434 : if (!this.tokens) {
  435. 435 : this.tokens = this.getCorpus().getDocument(parseInt(this.getApiParam('docIndex'))).getTokens({
  436. 436 : proxy: {
  437. 437 : extraParams: {
  438. 438 : stripTags: 'all'
  439. 439 : }
  440. 440 : }
  441. 441 : });
  442. 442 : this.noMoreTokens = false;
  443. 443 : } else if (this.noMoreTokens) {return;}
  444. 444 :
  445. 445 : var first = this.tokens.getCount() == 0;
  446. 446 : this.tokensLoading = true;
  447. 447 : var speed = parseInt(this.getApiParam('speed'));
  448. 448 : this.tokens.load({
  449. 449 : params: {
  450. 450 : start: this.tokens.getCount(),
  451. 451 : limit: speed==50 && first ? 200 : Math.pow(110-speed, 2)
  452. 452 : },
  453. 453 : callback: function(records) {
  454. 454 : this.tokensLoading = false;
  455. 455 : if (records.length>0) {
  456. 456 : records.forEach(function(token) {
  457. 457 : if (token.getTokenType()=='other') {
  458. 458 : token.set('term', token.getTerm().replace(/\s+/g, " "))
  459. 459 : }
  460. 460 : })
  461. 461 : if (first) {
  462. 462 : this.previousBeziers = [];
  463. 463 : // TODO
  464. 464 : // if (this.getApiParam('speed') > 0) {
  465. 465 : this.isReading = true;
  466. 466 : this.read(0);
  467. 467 : // }
  468. 468 : }
  469. 469 : } else {
  470. 470 : this.noMoreTokens = true;
  471. 471 : }
  472. 472 : },
  473. 473 : addRecords: true,
  474. 474 : scope: this
  475. 475 :
  476. 476 : });
  477. 477 : },
  478. 478 :
  479. 479 : /* The functions below adapted from http://www.pjgalbraith.com/drawing-animated-curves-javascript/ */
  480. 480 :
  481. 481 : /**
  482. 482 : * Animates bezier-curve
  483. 483 : *
  484. 484 : * @param ctx The canvas context to draw to
  485. 485 : * @param x0 The x-coord of the start point
  486. 486 : * @param y0 The y-coord of the start point
  487. 487 : * @param x1 The x-coord of the control point
  488. 488 : * @param y1 The y-coord of the control point
  489. 489 : * @param x2 The x-coord of the end point
  490. 490 : * @param y2 The y-coord of the end point
  491. 491 : * @param duration The duration in milliseconds
  492. 492 : * @private
  493. 493 : */
  494. 494 : animatePathDrawing: function(ctx, x0, y0, x1, y1, x2, y2, duration) {
  495. 495 : var start = null;
  496. 496 :
  497. 497 : var step = function animatePathDrawingStep(timestamp) {
  498. 498 : if (start === null)
  499. 499 : start = timestamp;
  500. 500 :
  501. 501 : var delta = timestamp - start,
  502. 502 : progress = Math.min(delta / duration, 1);
  503. 503 :
  504. 504 : // Clear canvas
  505. 505 : ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  506. 506 :
  507. 507 : // Draw curve
  508. 508 : drawBezierSplit(ctx, x0, y0, x1, y1, x2, y2, 0, progress);
  509. 509 :
  510. 510 : if (progress < 1) {
  511. 511 : window.requestAnimationFrame(step);
  512. 512 : }
  513. 513 : };
  514. 514 :
  515. 515 : window.requestAnimationFrame(step);
  516. 516 : },
  517. 517 :
  518. 518 : /**
  519. 519 : * Draws a splitted bezier-curve
  520. 520 : *
  521. 521 : * @param ctx The canvas context to draw to
  522. 522 : * @param x0 The x-coord of the start point
  523. 523 : * @param y0 The y-coord of the start point
  524. 524 : * @param x1 The x-coord of the control point
  525. 525 : * @param y1 The y-coord of the control point
  526. 526 : * @param x2 The x-coord of the end point
  527. 527 : * @param y2 The y-coord of the end point
  528. 528 : * @param t0 The start ratio of the splitted bezier from 0.0 to 1.0
  529. 529 : * @param t1 The start ratio of the splitted bezier from 0.0 to 1.0
  530. 530 : * @private
  531. 531 : */
  532. 532 : drawBezierSplit: function(ctx, x0, y0, x1, y1, x2, y2, t0, t1) {
  533. 533 : ctx.beginPath();
  534. 534 :
  535. 535 : if( 0.0 == t0 && t1 == 1.0 ) {
  536. 536 : ctx.moveTo( x0, y0 );
  537. 537 : ctx.quadraticCurveTo( x1, y1, x2, y2 );
  538. 538 : } else if( t0 != t1 ) {
  539. 539 : var t00 = t0 * t0,
  540. 540 : t01 = 1.0 - t0,
  541. 541 : t02 = t01 * t01,
  542. 542 : t03 = 2.0 * t0 * t01;
  543. 543 :
  544. 544 : var nx0 = t02 * x0 + t03 * x1 + t00 * x2,
  545. 545 : ny0 = t02 * y0 + t03 * y1 + t00 * y2;
  546. 546 :
  547. 547 : t00 = t1 * t1;
  548. 548 : t01 = 1.0 - t1;
  549. 549 : t02 = t01 * t01;
  550. 550 : t03 = 2.0 * t1 * t01;
  551. 551 :
  552. 552 : var nx2 = t02 * x0 + t03 * x1 + t00 * x2,
  553. 553 : ny2 = t02 * y0 + t03 * y1 + t00 * y2;
  554. 554 :
  555. 555 : var nx1 = this.lerp ( this.lerp ( x0 , x1 , t0 ) , this.lerp ( x1 , x2 , t0 ) , t1 ),
  556. 556 : ny1 = this.lerp ( this.lerp ( y0 , y1 , t0 ) , this.lerp ( y1 , y2 , t0 ) , t1 );
  557. 557 :
  558. 558 : ctx.moveTo( nx0, ny0 );
  559. 559 : ctx.quadraticCurveTo( nx1, ny1, nx2, ny2 );
  560. 560 : }
  561. 561 :
  562. 562 : ctx.stroke();
  563. 563 : ctx.closePath();
  564. 564 : },
  565. 565 :
  566. 566 : /**
  567. 567 : * Linearly interpolate between two numbers v0, v1 by t
  568. 568 : * @private
  569. 569 : */
  570. 570 : lerp: function(v0, v1, t) {
  571. 571 : return ( 1.0 - t ) * v0 + t * v1;
  572. 572 : }
  573. 573 :
  574. 574 :
  575. 575 :
  576. 576 : });