1 <html> 2 <head> 3 <div style="height:0"> 4 5 <div id="cubic1"> 6 {{3.13,2.74}, {1.08,4.62}, {3.71,0.94}, {2.01,3.81}} 7 {{6.71,3.14}, {7.99,2.75}, {8.27,1.96}, {6.35,3.57}} 8 {{9.45,10.67}, {10.05,5.78}, {13.95,7.46}, {14.72,5.29}} 9 {{3.34,8.98}, {1.95,10.27}, {3.76,7.65}, {4.96,10.64}} 10 </div> 11 12 </div> 13 14 <script type="text/javascript"> 15 16 var testDivs = [ 17 cubic1, 18 ]; 19 20 var scale, columns, rows, xStart, yStart; 21 22 var ticks = 10; 23 var at_x = 13 + 0.5; 24 var at_y = 23 + 0.5; 25 var decimal_places = 3; 26 var tests = []; 27 var testTitles = []; 28 var testIndex = 0; 29 var ctx; 30 var minScale = 1; 31 var subscale = 1; 32 var curveT = -1; 33 var xmin, xmax, ymin, ymax; 34 35 var mouseX, mouseY; 36 var mouseDown = false; 37 38 var draw_deriviatives = false; 39 var draw_endpoints = true; 40 var draw_hodo = false; 41 var draw_hodo2 = false; 42 var draw_hodo_origin = true; 43 var draw_midpoint = false; 44 var draw_tangents = true; 45 var draw_sequence = true; 46 47 function parse(test, title) { 48 var curveStrs = test.split("{{"); 49 if (curveStrs.length == 1) 50 curveStrs = test.split("=("); 51 var pattern = /[a-z$=]?-?\d+\.*\d*e?-?\d*/g; 52 var curves = []; 53 for (var c in curveStrs) { 54 var curveStr = curveStrs[c]; 55 var points = curveStr.match(pattern); 56 var pts = []; 57 for (var wd in points) { 58 var num = parseFloat(points[wd]); 59 if (isNaN(num)) continue; 60 pts.push(num); 61 } 62 if (pts.length > 2) 63 curves.push(pts); 64 } 65 if (curves.length >= 1) { 66 tests.push(curves); 67 testTitles.push(title); 68 } 69 } 70 71 function init(test) { 72 var canvas = document.getElementById('canvas'); 73 if (!canvas.getContext) return; 74 canvas.width = window.innerWidth - 20; 75 canvas.height = window.innerHeight - 20; 76 ctx = canvas.getContext('2d'); 77 xmin = Infinity; 78 xmax = -Infinity; 79 ymin = Infinity; 80 ymax = -Infinity; 81 for (var curves in test) { 82 var curve = test[curves]; 83 var last = curve.length; 84 for (var idx = 0; idx < last; idx += 2) { 85 xmin = Math.min(xmin, curve[idx]); 86 xmax = Math.max(xmax, curve[idx]); 87 ymin = Math.min(ymin, curve[idx + 1]); 88 ymax = Math.max(ymax, curve[idx + 1]); 89 } 90 } 91 xmin -= 1; 92 var testW = xmax - xmin; 93 var testH = ymax - ymin; 94 subscale = 1; 95 while (testW * subscale < 0.1 && testH * subscale < 0.1) { 96 subscale *= 10; 97 } 98 while (testW * subscale > 10 && testH * subscale > 10) { 99 subscale /= 10; 100 } 101 calcFromScale(); 102 } 103 104 function hodograph(cubic) { 105 var hodo = []; 106 hodo[0] = 3 * (cubic[2] - cubic[0]); 107 hodo[1] = 3 * (cubic[3] - cubic[1]); 108 hodo[2] = 3 * (cubic[4] - cubic[2]); 109 hodo[3] = 3 * (cubic[5] - cubic[3]); 110 hodo[4] = 3 * (cubic[6] - cubic[4]); 111 hodo[5] = 3 * (cubic[7] - cubic[5]); 112 return hodo; 113 } 114 115 function hodograph2(cubic) { 116 var quad = hodograph(cubic); 117 var hodo = []; 118 hodo[0] = 2 * (quad[2] - quad[0]); 119 hodo[1] = 2 * (quad[3] - quad[1]); 120 hodo[2] = 2 * (quad[4] - quad[2]); 121 hodo[3] = 2 * (quad[5] - quad[3]); 122 return hodo; 123 } 124 125 function quadraticRootsReal(A, B, C, s) { 126 if (A == 0) { 127 if (B == 0) { 128 s[0] = 0; 129 return C == 0; 130 } 131 s[0] = -C / B; 132 return 1; 133 } 134 /* normal form: x^2 + px + q = 0 */ 135 var p = B / (2 * A); 136 var q = C / A; 137 var p2 = p * p; 138 if (p2 < q) { 139 return 0; 140 } 141 var sqrt_D = 0; 142 if (p2 > q) { 143 sqrt_D = sqrt(p2 - q); 144 } 145 s[0] = sqrt_D - p; 146 s[1] = -sqrt_D - p; 147 return 1 + s[0] != s[1]; 148 } 149 150 function add_valid_ts(s, realRoots, t) { 151 var foundRoots = 0; 152 for (var index = 0; index < realRoots; ++index) { 153 var tValue = s[index]; 154 if (tValue >= 0 && tValue <= 1) { 155 for (var idx2 = 0; idx2 < foundRoots; ++idx2) { 156 if (t[idx2] != tValue) { 157 t[foundRoots++] = tValue; 158 } 159 } 160 } 161 } 162 return foundRoots; 163 } 164 165 function quadraticRootsValidT(a, b, c, t) { 166 var s = []; 167 var realRoots = quadraticRootsReal(A, B, C, s); 168 var foundRoots = add_valid_ts(s, realRoots, t); 169 return foundRoots != 0; 170 } 171 172 function find_cubic_inflections(cubic, tValues) 173 { 174 var Ax = src[2] - src[0]; 175 var Ay = src[3] - src[1]; 176 var Bx = src[4] - 2 * src[2] + src[0]; 177 var By = src[5] - 2 * src[3] + src[1]; 178 var Cx = src[6] + 3 * (src[2] - src[4]) - src[0]; 179 var Cy = src[7] + 3 * (src[3] - src[5]) - src[1]; 180 return quadraticRootsValidT(Bx * Cy - By * Cx, (Ax * Cy - Ay * Cx), 181 Ax * By - Ay * Bx, tValues); 182 } 183 184 function dx_at_t(cubic, t) { 185 var one_t = 1 - t; 186 var a = cubic[0]; 187 var b = cubic[2]; 188 var c = cubic[4]; 189 var d = cubic[6]; 190 return 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t); 191 } 192 193 function dy_at_t(cubic, t) { 194 var one_t = 1 - t; 195 var a = cubic[1]; 196 var b = cubic[3]; 197 var c = cubic[5]; 198 var d = cubic[7]; 199 return 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t); 200 } 201 202 function x_at_t(cubic, t) { 203 var one_t = 1 - t; 204 var one_t2 = one_t * one_t; 205 var a = one_t2 * one_t; 206 var b = 3 * one_t2 * t; 207 var t2 = t * t; 208 var c = 3 * one_t * t2; 209 var d = t2 * t; 210 return a * cubic[0] + b * cubic[2] + c * cubic[4] + d * cubic[6]; 211 } 212 213 function y_at_t(cubic, t) { 214 var one_t = 1 - t; 215 var one_t2 = one_t * one_t; 216 var a = one_t2 * one_t; 217 var b = 3 * one_t2 * t; 218 var t2 = t * t; 219 var c = 3 * one_t * t2; 220 var d = t2 * t; 221 return a * cubic[1] + b * cubic[3] + c * cubic[5] + d * cubic[7]; 222 } 223 224 function calcFromScale() { 225 xStart = Math.floor(xmin * subscale) / subscale; 226 yStart = Math.floor(ymin * subscale) / subscale; 227 var xEnd = Math.ceil(xmin * subscale) / subscale; 228 var yEnd = Math.ceil(ymin * subscale) / subscale; 229 var cCelsW = Math.floor(ctx.canvas.width / 10); 230 var cCelsH = Math.floor(ctx.canvas.height / 10); 231 var testW = xEnd - xStart; 232 var testH = yEnd - yStart; 233 var scaleWH = 1; 234 while (cCelsW > testW * scaleWH * 10 && cCelsH > testH * scaleWH * 10) { 235 scaleWH *= 10; 236 } 237 while (cCelsW * 10 < testW * scaleWH && cCelsH * 10 < testH * scaleWH) { 238 scaleWH /= 10; 239 } 240 241 columns = Math.ceil(xmax * subscale) - Math.floor(xmin * subscale) + 1; 242 rows = Math.ceil(ymax * subscale) - Math.floor(ymin * subscale) + 1; 243 244 var hscale = ctx.canvas.width / columns / ticks; 245 var vscale = ctx.canvas.height / rows / ticks; 246 minScale = Math.floor(Math.min(hscale, vscale)); 247 scale = minScale * subscale; 248 } 249 250 function drawLine(x1, y1, x2, y2) { 251 var unit = scale * ticks; 252 var xoffset = xStart * -unit + at_x; 253 var yoffset = yStart * -unit + at_y; 254 ctx.beginPath(); 255 ctx.moveTo(xoffset + x1 * unit, yoffset + y1 * unit); 256 ctx.lineTo(xoffset + x2 * unit, yoffset + y2 * unit); 257 ctx.stroke(); 258 } 259 260 function drawPoint(px, py) { 261 var unit = scale * ticks; 262 var xoffset = xStart * -unit + at_x; 263 var yoffset = yStart * -unit + at_y; 264 var _px = px * unit + xoffset; 265 var _py = py * unit + yoffset; 266 ctx.beginPath(); 267 ctx.arc(_px, _py, 3, 0, Math.PI*2, true); 268 ctx.closePath(); 269 ctx.stroke(); 270 } 271 272 function drawPointSolid(px, py) { 273 drawPoint(px, py); 274 ctx.fillStyle = "rgba(0,0,0, 0.4)"; 275 ctx.fill(); 276 } 277 278 function drawLabel(num, px, py) { 279 ctx.beginPath(); 280 ctx.arc(px, py, 8, 0, Math.PI*2, true); 281 ctx.closePath(); 282 ctx.strokeStyle = "rgba(0,0,0, 0.4)"; 283 ctx.lineWidth = num == 0 || num == 3 ? 2 : 1; 284 ctx.stroke(); 285 ctx.fillStyle = "black"; 286 ctx.font = "normal 10px Arial"; 287 // ctx.rotate(0.001); 288 ctx.fillText(num, px - 2, py + 3); 289 // ctx.rotate(-0.001); 290 } 291 292 function drawLabelX(ymin, num, loc) { 293 var unit = scale * ticks; 294 var xoffset = xStart * -unit + at_x; 295 var yoffset = yStart * -unit + at_y; 296 var px = loc * unit + xoffset; 297 var py = ymin * unit + yoffset - 20; 298 drawLabel(num, px, py); 299 } 300 301 function drawLabelY(xmin, num, loc) { 302 var unit = scale * ticks; 303 var xoffset = xStart * -unit + at_x; 304 var yoffset = yStart * -unit + at_y; 305 var px = xmin * unit + xoffset - 20; 306 var py = loc * unit + yoffset; 307 drawLabel(num, px, py); 308 } 309 310 function drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY) { 311 ctx.beginPath(); 312 ctx.moveTo(hx, hy - 100); 313 ctx.lineTo(hx, hy); 314 ctx.strokeStyle = hMinY < 0 ? "green" : "blue"; 315 ctx.stroke(); 316 ctx.beginPath(); 317 ctx.moveTo(hx, hy); 318 ctx.lineTo(hx, hy + 100); 319 ctx.strokeStyle = hMaxY > 0 ? "green" : "blue"; 320 ctx.stroke(); 321 ctx.beginPath(); 322 ctx.moveTo(hx - 100, hy); 323 ctx.lineTo(hx, hy); 324 ctx.strokeStyle = hMinX < 0 ? "green" : "blue"; 325 ctx.stroke(); 326 ctx.beginPath(); 327 ctx.moveTo(hx, hy); 328 ctx.lineTo(hx + 100, hy); 329 ctx.strokeStyle = hMaxX > 0 ? "green" : "blue"; 330 ctx.stroke(); 331 } 332 333 function logCurves(test) { 334 for (curves in test) { 335 var curve = test[curves]; 336 if (curve.length != 8) { 337 continue; 338 } 339 var str = "{{"; 340 for (i = 0; i < 8; i += 2) { 341 str += curve[i].toFixed(2) + "," + curve[i + 1].toFixed(2); 342 if (i < 6) { 343 str += "}, {"; 344 } 345 } 346 str += "}}"; 347 console.log(str); 348 } 349 } 350 351 function scalexy(x, y, mag) { 352 var length = Math.sqrt(x * x + y * y); 353 return mag / length; 354 } 355 356 function drawArrow(x, y, dx, dy) { 357 var unit = scale * ticks; 358 var xoffset = xStart * -unit + at_x; 359 var yoffset = yStart * -unit + at_y; 360 var dscale = scalexy(dx, dy, 1); 361 dx *= dscale; 362 dy *= dscale; 363 ctx.beginPath(); 364 ctx.moveTo(xoffset + x * unit, yoffset + y * unit); 365 x += dx; 366 y += dy; 367 ctx.lineTo(xoffset + x * unit, yoffset + y * unit); 368 dx /= 10; 369 dy /= 10; 370 ctx.lineTo(xoffset + (x - dy) * unit, yoffset + (y + dx) * unit); 371 ctx.lineTo(xoffset + (x + dx * 2) * unit, yoffset + (y + dy * 2) * unit); 372 ctx.lineTo(xoffset + (x + dy) * unit, yoffset + (y - dx) * unit); 373 ctx.lineTo(xoffset + x * unit, yoffset + y * unit); 374 ctx.strokeStyle = "rgba(0,75,0, 0.4)"; 375 ctx.stroke(); 376 } 377 378 function draw(test, title) { 379 ctx.fillStyle = "rgba(0,0,0, 0.1)"; 380 ctx.font = "normal 50px Arial"; 381 ctx.fillText(title, 50, 50); 382 ctx.font = "normal 10px Arial"; 383 var unit = scale * ticks; 384 // ctx.lineWidth = "1.001"; "0.999"; 385 var xoffset = xStart * -unit + at_x; 386 var yoffset = yStart * -unit + at_y; 387 388 for (curves in test) { 389 var curve = test[curves]; 390 if (curve.length != 8) { 391 continue; 392 } 393 ctx.lineWidth = 1; 394 if (draw_tangents) { 395 ctx.strokeStyle = "rgba(0,0,255, 0.3)"; 396 drawLine(curve[0], curve[1], curve[2], curve[3]); 397 drawLine(curve[2], curve[3], curve[4], curve[5]); 398 drawLine(curve[4], curve[5], curve[6], curve[7]); 399 } 400 if (draw_deriviatives) { 401 var dx = dx_at_t(curve, 0); 402 var dy = dy_at_t(curve, 0); 403 drawArrow(curve[0], curve[1], dx, dy); 404 dx = dx_at_t(curve, 1); 405 dy = dy_at_t(curve, 1); 406 drawArrow(curve[6], curve[7], dx, dy); 407 if (draw_midpoint) { 408 var midX = x_at_t(curve, 0.5); 409 var midY = y_at_t(curve, 0.5); 410 dx = dx_at_t(curve, 0.5); 411 dy = dy_at_t(curve, 0.5); 412 drawArrow(midX, midY, dx, dy); 413 } 414 } 415 ctx.beginPath(); 416 ctx.moveTo(xoffset + curve[0] * unit, yoffset + curve[1] * unit); 417 ctx.bezierCurveTo( 418 xoffset + curve[2] * unit, yoffset + curve[3] * unit, 419 xoffset + curve[4] * unit, yoffset + curve[5] * unit, 420 xoffset + curve[6] * unit, yoffset + curve[7] * unit); 421 ctx.strokeStyle = "black"; 422 ctx.stroke(); 423 if (draw_endpoints) { 424 drawPoint(curve[0], curve[1]); 425 drawPoint(curve[2], curve[3]); 426 drawPoint(curve[4], curve[5]); 427 drawPoint(curve[6], curve[7]); 428 } 429 if (draw_midpoint) { 430 var midX = x_at_t(curve, 0.5); 431 var midY = y_at_t(curve, 0.5); 432 drawPointSolid(midX, midY); 433 } 434 if (draw_hodo) { 435 var hodo = hodograph(curve); 436 var hMinX = Math.min(0, hodo[0], hodo[2], hodo[4]); 437 var hMinY = Math.min(0, hodo[1], hodo[3], hodo[5]); 438 var hMaxX = Math.max(0, hodo[0], hodo[2], hodo[4]); 439 var hMaxY = Math.max(0, hodo[1], hodo[3], hodo[5]); 440 var hScaleX = hMaxX - hMinX > 0 ? ctx.canvas.width / (hMaxX - hMinX) : 1; 441 var hScaleY = hMaxY - hMinY > 0 ? ctx.canvas.height / (hMaxY - hMinY) : 1; 442 var hUnit = Math.min(hScaleX, hScaleY); 443 hUnit /= 2; 444 var hx = xoffset - hMinX * hUnit; 445 var hy = yoffset - hMinY * hUnit; 446 ctx.moveTo(hx + hodo[0] * hUnit, hy + hodo[1] * hUnit); 447 ctx.quadraticCurveTo( 448 hx + hodo[2] * hUnit, hy + hodo[3] * hUnit, 449 hx + hodo[4] * hUnit, hy + hodo[5] * hUnit); 450 ctx.strokeStyle = "red"; 451 ctx.stroke(); 452 if (draw_hodo_origin) { 453 drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY); 454 } 455 } 456 if (draw_hodo2) { 457 var hodo = hodograph2(curve); 458 var hMinX = Math.min(0, hodo[0], hodo[2]); 459 var hMinY = Math.min(0, hodo[1], hodo[3]); 460 var hMaxX = Math.max(0, hodo[0], hodo[2]); 461 var hMaxY = Math.max(0, hodo[1], hodo[3]); 462 var hScaleX = hMaxX - hMinX > 0 ? ctx.canvas.width / (hMaxX - hMinX) : 1; 463 var hScaleY = hMaxY - hMinY > 0 ? ctx.canvas.height / (hMaxY - hMinY) : 1; 464 var hUnit = Math.min(hScaleX, hScaleY); 465 hUnit /= 2; 466 var hx = xoffset - hMinX * hUnit; 467 var hy = yoffset - hMinY * hUnit; 468 ctx.moveTo(hx + hodo[0] * hUnit, hy + hodo[1] * hUnit); 469 ctx.lineTo(hx + hodo[2] * hUnit, hy + hodo[3] * hUnit); 470 ctx.strokeStyle = "red"; 471 ctx.stroke(); 472 drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY); 473 } 474 if (draw_sequence) { 475 var ymin = Math.min(curve[1], curve[3], curve[5], curve[7]); 476 for (var i = 0; i < 8; i+= 2) { 477 drawLabelX(ymin, i >> 1, curve[i]); 478 } 479 var xmin = Math.min(curve[0], curve[2], curve[4], curve[6]); 480 for (var i = 1; i < 8; i+= 2) { 481 drawLabelY(xmin, i >> 1, curve[i]); 482 } 483 } 484 } 485 } 486 487 function drawTop() { 488 init(tests[testIndex]); 489 redraw(); 490 } 491 492 function redraw() { 493 ctx.beginPath(); 494 ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height); 495 ctx.fillStyle="white"; 496 ctx.fill(); 497 draw(tests[testIndex], testTitles[testIndex]); 498 } 499 500 function doKeyPress(evt) { 501 var char = String.fromCharCode(evt.charCode); 502 switch (char) { 503 case '2': 504 draw_hodo2 ^= true; 505 redraw(); 506 break; 507 case 'd': 508 draw_deriviatives ^= true; 509 redraw(); 510 break; 511 case 'e': 512 draw_endpoints ^= true; 513 redraw(); 514 break; 515 case 'h': 516 draw_hodo ^= true; 517 redraw(); 518 break; 519 case 'N': 520 testIndex += 9; 521 case 'n': 522 if (++testIndex >= tests.length) 523 testIndex = 0; 524 drawTop(); 525 break; 526 case 'l': 527 logCurves(tests[testIndex]); 528 break; 529 case 'm': 530 draw_midpoint ^= true; 531 redraw(); 532 break; 533 case 'o': 534 draw_hodo_origin ^= true; 535 redraw(); 536 break; 537 case 'P': 538 testIndex -= 9; 539 case 'p': 540 if (--testIndex < 0) 541 testIndex = tests.length - 1; 542 drawTop(); 543 break; 544 case 's': 545 draw_sequence ^= true; 546 redraw(); 547 break; 548 case 't': 549 draw_tangents ^= true; 550 redraw(); 551 break; 552 } 553 } 554 555 function calcXY() { 556 var e = window.event; 557 var tgt = e.target || e.srcElement; 558 var left = tgt.offsetLeft; 559 var top = tgt.offsetTop; 560 var unit = scale * ticks; 561 mouseX = (e.clientX - left - Math.ceil(at_x) + 1) / unit + xStart; 562 mouseY = (e.clientY - top - Math.ceil(at_y)) / unit + yStart; 563 } 564 565 var lastX, lastY; 566 var activeCurve = []; 567 var activePt; 568 569 function handleMouseClick() { 570 calcXY(); 571 } 572 573 function initDown() { 574 var unit = scale * ticks; 575 var xoffset = xStart * -unit + at_x; 576 var yoffset = yStart * -unit + at_y; 577 var test = tests[testIndex]; 578 var bestDistance = 1000000; 579 activePt = -1; 580 for (curves in test) { 581 var testCurve = test[curves]; 582 if (testCurve.length != 8) { 583 continue; 584 } 585 for (var i = 0; i < 8; i += 2) { 586 var testX = testCurve[i]; 587 var testY = testCurve[i + 1]; 588 var dx = testX - mouseX; 589 var dy = testY - mouseY; 590 var dist = dx * dx + dy * dy; 591 if (dist > bestDistance) { 592 continue; 593 } 594 activeCurve = testCurve; 595 activePt = i; 596 bestDistance = dist; 597 } 598 } 599 if (activePt >= 0) { 600 lastX = mouseX; 601 lastY = mouseY; 602 } 603 } 604 605 function handleMouseOver() { 606 if (!mouseDown) { 607 activePt = -1; 608 return; 609 } 610 calcXY(); 611 if (activePt < 0) { 612 initDown(); 613 return; 614 } 615 var unit = scale * ticks; 616 var deltaX = (mouseX - lastX) /* / unit */; 617 var deltaY = (mouseY - lastY) /*/ unit */; 618 lastX = mouseX; 619 lastY = mouseY; 620 activeCurve[activePt] += deltaX; 621 activeCurve[activePt + 1] += deltaY; 622 redraw(); 623 } 624 625 function start() { 626 for (i = 0; i < testDivs.length; ++i) { 627 var title = testDivs[i].id.toString(); 628 var str = testDivs[i].firstChild.data; 629 parse(str, title); 630 } 631 drawTop(); 632 window.addEventListener('keypress', doKeyPress, true); 633 window.onresize = function() { 634 drawTop(); 635 } 636 } 637 638 </script> 639 </head> 640 641 <body onLoad="start();"> 642 <canvas id="canvas" width="750" height="500" 643 onmousedown="mouseDown = true" 644 onmouseup="mouseDown = false" 645 onmousemove="handleMouseOver()" 646 onclick="handleMouseClick()" 647 ></canvas > 648 </body> 649 </html> 650