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