/examples/addonsgallery/public/excanvas.js
JavaScript | 704 lines | 539 code | 92 blank | 73 comment | 60 complexity | 1f48564d9be1a2b986399b98af5e74ca MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0
1// Copyright 2006 Google Inc. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// TODO: Patterns 16// TODO: Radial gradient 17// TODO: Clipping paths 18// TODO: Coordsize 19// TODO: Painting mode 20// TODO: Optimize 21// TODO: canvas width/height sets content size in moz, border size in ie 22// TODO: Painting outside the canvas should not be allowed 23 24// only add this code if we do not already have a canvas implementation 25if (!window.CanvasRenderingContext2D) { 26 27(function () { 28 29 var G_vmlCanvasManager_ = { 30 init: function (opt_doc) { 31 var doc = opt_doc || document; 32 if (/MSIE/.test(navigator.userAgent) && !window.opera) { 33 var self = this; 34 doc.attachEvent("onreadystatechange", function () { 35 self.init_(doc); 36 }); 37 } 38 }, 39 40 init_: function (doc, e) { 41 if (doc.readyState == "complete") { 42 // create xmlns 43 if (!doc.namespaces["g_vml_"]) { 44 doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml"); 45 } 46 47 // setup default css 48 var ss = doc.createStyleSheet(); 49 ss.cssText = "canvas{display:inline-block;overflow:hidden;" + 50 "text-align:left;}" + 51 "canvas *{behavior:url(#default#VML)}"; 52 53 // find all canvas elements 54 var els = doc.getElementsByTagName("canvas"); 55 for (var i = 0; i < els.length; i++) { 56 if (!els[i].getContext) { 57 this.initElement(els[i]); 58 } 59 } 60 } 61 }, 62 63 fixElement_: function (el) { 64 // in IE before version 5.5 we would need to add HTML: to the tag name 65 // but we do not care about IE before version 6 66 var outerHTML = el.outerHTML; 67 var newEl = document.createElement(outerHTML); 68 // if the tag is still open IE has created the children as siblings and 69 // it has also created a tag with the name "/FOO" 70 if (outerHTML.slice(-2) != "/>") { 71 var tagName = "/" + el.tagName; 72 var ns; 73 // remove content 74 while ((ns = el.nextSibling) && ns.tagName != tagName) { 75 ns.removeNode(); 76 } 77 // remove the incorrect closing tag 78 if (ns) { 79 ns.removeNode(); 80 } 81 } 82 el.parentNode.replaceChild(newEl, el); 83 return newEl; 84 }, 85 86 /** 87 * Public initializes a canvas element so that it can be used as canvas 88 * element from now on. This is called automatically before the page is 89 * loaded but if you are creating elements using createElement yuo need to 90 * make sure this is called on the element. 91 * @param el {HTMLElement} The canvas element to initialize. 92 */ 93 initElement: function (el) { 94 el = this.fixElement_(el); 95 el.getContext = function () { 96 if (this.context_) { 97 return this.context_; 98 } 99 return this.context_ = new CanvasRenderingContext2D_(this); 100 }; 101 102 var self = this; //bind 103 el.attachEvent("onpropertychange", function (e) { 104 // we need to watch changes to width and height 105 switch (e.propertyName) { 106 case "width": 107 case "height": 108 // coord size changed? 109 break; 110 } 111 }); 112 113 // if style.height is set 114 115 var attrs = el.attributes; 116 if (attrs.width && attrs.width.specified) { 117 // TODO: use runtimeStyle and coordsize 118 // el.getContext().setWidth_(attrs.width.nodeValue); 119 el.style.width = attrs.width.nodeValue + "px"; 120 } 121 if (attrs.height && attrs.height.specified) { 122 // TODO: use runtimeStyle and coordsize 123 // el.getContext().setHeight_(attrs.height.nodeValue); 124 el.style.height = attrs.height.nodeValue + "px"; 125 } 126 //el.getContext().setCoordsize_() 127 } 128 }; 129 130 G_vmlCanvasManager_.init(); 131 132 // precompute "00" to "FF" 133 var dec2hex = []; 134 for (var i = 0; i < 16; i++) { 135 for (var j = 0; j < 16; j++) { 136 dec2hex[i * 16 + j] = i.toString(16) + j.toString(16); 137 } 138 } 139 140 function createMatrixIdentity() { 141 return [ 142 [1, 0, 0], 143 [0, 1, 0], 144 [0, 0, 1] 145 ]; 146 } 147 148 function matrixMultiply(m1, m2) { 149 var result = createMatrixIdentity(); 150 151 for (var x = 0; x < 3; x++) { 152 for (var y = 0; y < 3; y++) { 153 var sum = 0; 154 155 for (var z = 0; z < 3; z++) { 156 sum += m1[x][z] * m2[z][y]; 157 } 158 159 result[x][y] = sum; 160 } 161 } 162 return result; 163 } 164 165 function copyState(o1, o2) { 166 o2.fillStyle = o1.fillStyle; 167 o2.lineCap = o1.lineCap; 168 o2.lineJoin = o1.lineJoin; 169 o2.lineWidth = o1.lineWidth; 170 o2.miterLimit = o1.miterLimit; 171 o2.shadowBlur = o1.shadowBlur; 172 o2.shadowColor = o1.shadowColor; 173 o2.shadowOffsetX = o1.shadowOffsetX; 174 o2.shadowOffsetY = o1.shadowOffsetY; 175 o2.strokeStyle = o1.strokeStyle; 176 } 177 178 function processStyle(styleString) { 179 var str, alpha = 1; 180 181 styleString = String(styleString); 182 if (styleString.substring(0, 3) == "rgb") { 183 var start = styleString.indexOf("(", 3); 184 var end = styleString.indexOf(")", start + 1); 185 var guts = styleString.substring(start + 1, end).split(","); 186 187 str = "#"; 188 for (var i = 0; i < 3; i++) { 189 str += dec2hex[parseInt(guts[i])]; 190 } 191 192 if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) { 193 alpha = guts[3]; 194 } 195 } else { 196 str = styleString; 197 } 198 199 return [str, alpha]; 200 } 201 202 function processLineCap(lineCap) { 203 switch (lineCap) { 204 case "butt": 205 return "flat"; 206 case "round": 207 return "round"; 208 case "square": 209 default: 210 return "square"; 211 } 212 } 213 214 /** 215 * This class implements CanvasRenderingContext2D interface as described by 216 * the WHATWG. 217 * @param surfaceElement {HTMLElement} The element that the 2D context should 218 * be associated with 219 */ 220 function CanvasRenderingContext2D_(surfaceElement) { 221 this.m_ = createMatrixIdentity(); 222 this.element_ = surfaceElement; 223 224 this.mStack_ = []; 225 this.aStack_ = []; 226 this.currentPath_ = []; 227 228 // Canvas context properties 229 this.strokeStyle = "#000"; 230 this.fillStyle = "#ccc"; 231 232 this.lineWidth = 1; 233 this.lineJoin = "miter"; 234 this.lineCap = "butt"; 235 this.miterLimit = 10; 236 this.globalAlpha = 1; 237 }; 238 239 var contextPrototype = CanvasRenderingContext2D_.prototype; 240 contextPrototype.clearRect = function() { 241 this.element_.innerHTML = ""; 242 this.currentPath_ = []; 243 }; 244 245 contextPrototype.beginPath = function() { 246 // TODO: Branch current matrix so that save/restore has no effect 247 // as per safari docs. 248 249 this.currentPath_ = []; 250 }; 251 252 contextPrototype.moveTo = function(aX, aY) { 253 this.currentPath_.push({type: "moveTo", x: aX, y: aY}); 254 }; 255 256 contextPrototype.lineTo = function(aX, aY) { 257 this.currentPath_.push({type: "lineTo", x: aX, y: aY}); 258 }; 259 260 contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, 261 aCP2x, aCP2y, 262 aX, aY) { 263 this.currentPath_.push({type: "bezierCurveTo", 264 cp1x: aCP1x, 265 cp1y: aCP1y, 266 cp2x: aCP2x, 267 cp2y: aCP2y, 268 x: aX, 269 y: aY}); 270 }; 271 272 contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { 273 // VML's qb produces different output to Firefox's 274 // FF's behaviour seems to have changed in 1.5.0.1, check this 275 this.bezierCurveTo(aCPx, aCPy, aCPx, aCPy, aX, aY); 276 }; 277 278 contextPrototype.arc = function(aX, aY, aRadius, 279 aStartAngle, aEndAngle, aClockwise) { 280 if (!aClockwise) { 281 var t = aStartAngle; 282 aStartAngle = aEndAngle; 283 aEndAngle = t; 284 } 285 286 var xStart = aX + (Math.cos(aStartAngle) * aRadius); 287 var yStart = aY + (Math.sin(aStartAngle) * aRadius); 288 289 var xEnd = aX + (Math.cos(aEndAngle) * aRadius); 290 var yEnd = aY + (Math.sin(aEndAngle) * aRadius); 291 292 this.currentPath_.push({type: "arc", 293 x: aX, 294 y: aY, 295 radius: aRadius, 296 xStart: xStart, 297 yStart: yStart, 298 xEnd: xEnd, 299 yEnd: yEnd}); 300 301 }; 302 303 contextPrototype.rect = function(aX, aY, aWidth, aHeight) { 304 this.moveTo(aX, aY); 305 this.lineTo(aX + aWidth, aY); 306 this.lineTo(aX + aWidth, aY + aHeight); 307 this.lineTo(aX, aY + aHeight); 308 this.closePath(); 309 }; 310 311 contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { 312 // Will destroy any existing path (same as FF behaviour) 313 this.beginPath(); 314 this.moveTo(aX, aY); 315 this.lineTo(aX + aWidth, aY); 316 this.lineTo(aX + aWidth, aY + aHeight); 317 this.lineTo(aX, aY + aHeight); 318 this.closePath(); 319 this.stroke(); 320 }; 321 322 contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { 323 // Will destroy any existing path (same as FF behaviour) 324 this.beginPath(); 325 this.moveTo(aX, aY); 326 this.lineTo(aX + aWidth, aY); 327 this.lineTo(aX + aWidth, aY + aHeight); 328 this.lineTo(aX, aY + aHeight); 329 this.closePath(); 330 this.fill(); 331 }; 332 333 contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { 334 var gradient = new CanvasGradient_("gradient"); 335 return gradient; 336 }; 337 338 contextPrototype.createRadialGradient = function(aX0, aY0, 339 aR0, aX1, 340 aY1, aR1) { 341 var gradient = new CanvasGradient_("gradientradial"); 342 gradient.radius1_ = aR0; 343 gradient.radius2_ = aR1; 344 gradient.focus_.x = aX0; 345 gradient.focus_.y = aY0; 346 return gradient; 347 }; 348 349 contextPrototype.drawImage = function (image, var_args) { 350 var dx, dy, dw, dh, sx, sy, sw, sh; 351 var w = image.width; 352 var h = image.height; 353 354 if (arguments.length == 3) { 355 dx = arguments[1]; 356 dy = arguments[2]; 357 sx = sy = 0; 358 sw = dw = w; 359 sh = dh = h; 360 } else if (arguments.length == 5) { 361 dx = arguments[1]; 362 dy = arguments[2]; 363 dw = arguments[3]; 364 dh = arguments[4]; 365 sx = sy = 0; 366 sw = w; 367 sh = h; 368 } else if (arguments.length == 9) { 369 sx = arguments[1]; 370 sy = arguments[2]; 371 sw = arguments[3]; 372 sh = arguments[4]; 373 dx = arguments[5]; 374 dy = arguments[6]; 375 dw = arguments[7]; 376 dh = arguments[8]; 377 } else { 378 throw "Invalid number of arguments"; 379 } 380 381 var d = this.getCoords_(dx, dy); 382 383 var w2 = (sw / 2); 384 var h2 = (sh / 2); 385 386 var vmlStr = []; 387 388 // For some reason that I've now forgotten, using divs didn't work 389 vmlStr.push(' <g_vml_:group', 390 ' coordsize="100,100"', 391 ' coordorigin="0, 0"' , 392 ' style="width:100px;height:100px;position:absolute;'); 393 394 // If filters are necessary (rotation exists), create them 395 // filters are bog-slow, so only create them if abbsolutely necessary 396 // The following check doesn't account for skews (which don't exist 397 // in the canvas spec (yet) anyway. 398 399 if (this.m_[0][0] != 1 || this.m_[0][1]) { 400 var filter = []; 401 402 // Note the 12/21 reversal 403 filter.push("M11='", this.m_[0][0], "',", 404 "M12='", this.m_[1][0], "',", 405 "M21='", this.m_[0][1], "',", 406 "M22='", this.m_[1][1], "',", 407 "Dx='", d.x, "',", 408 "Dy='", d.y, "'"); 409 410 // Bounding box calculation (need to minimize displayed area so that 411 // filters don't waste time on unused pixels. 412 var max = d; 413 var c2 = this.getCoords_(dx+dw, dy); 414 var c3 = this.getCoords_(dx, dy+dh); 415 var c4 = this.getCoords_(dx+dw, dy+dh); 416 417 max.x = Math.max(max.x, c2.x, c3.x, c4.x); 418 max.y = Math.max(max.y, c2.y, c3.y, c4.y); 419 420 vmlStr.push(" padding:0 ", Math.floor(max.x), "px ", Math.floor(max.y), 421 "px 0;filter:progid:DXImageTransform.Microsoft.Matrix(", 422 filter.join(""), ", sizingmethod='clip');") 423 } else { 424 vmlStr.push(" top:", d.y, "px;left:", d.x, "px;") 425 } 426 427 vmlStr.push(' ">' , 428 '<g_vml_:image src="', image.src, '"', 429 ' style="width:', dw, ';', 430 ' height:', dh, ';"', 431 ' cropleft="', sx / w, '"', 432 ' croptop="', sy / h, '"', 433 ' cropright="', (w - sx - sw) / w, '"', 434 ' cropbottom="', (h - sy - sh) / h, '"', 435 ' />', 436 '</g_vml_:group>'); 437 438 this.element_.insertAdjacentHTML("BeforeEnd", 439 vmlStr.join("")); 440 }; 441 442 contextPrototype.stroke = function(aFill) { 443 var lineStr = []; 444 var lineOpen = false; 445 var a = processStyle(aFill ? this.fillStyle : this.strokeStyle); 446 var color = a[0]; 447 var opacity = a[1] * this.globalAlpha; 448 449 lineStr.push('<g_vml_:shape', 450 ' fillcolor="', color, '"', 451 ' filled="', Boolean(aFill), '"', 452 ' style="position:absolute;width:10;height:10;"', 453 ' coordorigin="0 0" coordsize="10 10"', 454 ' stroked="', !aFill, '"', 455 ' strokeweight="', this.lineWidth, '"', 456 ' strokecolor="', color, '"', 457 ' path="'); 458 459 var newSeq = false; 460 var min = {x: null, y: null}; 461 var max = {x: null, y: null}; 462 463 for (var i = 0; i < this.currentPath_.length; i++) { 464 var p = this.currentPath_[i]; 465 466 if (p.type == "moveTo") { 467 lineStr.push(" m "); 468 var c = this.getCoords_(p.x, p.y); 469 lineStr.push(Math.floor(c.x), ",", Math.floor(c.y)); 470 } else if (p.type == "lineTo") { 471 lineStr.push(" l "); 472 var c = this.getCoords_(p.x, p.y); 473 lineStr.push(Math.floor(c.x), ",", Math.floor(c.y)); 474 } else if (p.type == "close") { 475 lineStr.push(" x "); 476 } else if (p.type == "bezierCurveTo") { 477 lineStr.push(" c "); 478 var c = this.getCoords_(p.x, p.y); 479 var c1 = this.getCoords_(p.cp1x, p.cp1y); 480 var c2 = this.getCoords_(p.cp2x, p.cp2y); 481 lineStr.push(Math.floor(c1.x), ",", Math.floor(c1.y), ",", 482 Math.floor(c2.x), ",", Math.floor(c2.y), ",", 483 Math.floor(c.x), ",", Math.floor(c.y)); 484 } else if (p.type == "arc") { 485 lineStr.push(" ar "); 486 var c = this.getCoords_(p.x, p.y); 487 var cStart = this.getCoords_(p.xStart, p.yStart); 488 var cEnd = this.getCoords_(p.xEnd, p.yEnd); 489 490 // TODO: FIX (matricies (scale+rotation) now buggered this up) 491 // VML arc also doesn't seem able to do rotated non-circular 492 // arcs without parent grouping. 493 var absXScale = this.m_[0][0]; 494 var absYScale = this.m_[1][1]; 495 496 lineStr.push(Math.floor(c.x - absXScale * p.radius), ",", 497 Math.floor(c.y - absYScale * p.radius), " ", 498 Math.floor(c.x + absXScale * p.radius), ",", 499 Math.floor(c.y + absYScale * p.radius), " ", 500 Math.floor(cStart.x), ",", Math.floor(cStart.y), " ", 501 Math.floor(cEnd.x), ",", Math.floor(cEnd.y)); 502 } 503 504 505 // TODO: Following is broken for curves due to 506 // move to proper paths. 507 508 // Figure out dimensions so we can do gradient fills 509 // properly 510 if(c) { 511 if (min.x == null || c.x < min.x) { 512 min.x = c.x; 513 } 514 if (max.x == null || c.x > max.x) { 515 max.x = c.x; 516 } 517 if (min.y == null || c.y < min.y) { 518 min.y = c.y; 519 } 520 if (max.y == null || c.y > max.y) { 521 max.y = c.y; 522 } 523 } 524 } 525 lineStr.push(' ">'); 526 527 if (typeof this.fillStyle == "object") { 528 var focus = {x: "50%", y: "50%"}; 529 var width = (max.x - min.x); 530 var height = (max.y - min.y); 531 var dimension = (width > height) ? width : height; 532 533 focus.x = Math.floor((this.fillStyle.focus_.x / width) * 100 + 50) + "%"; 534 focus.y = Math.floor((this.fillStyle.focus_.y / height) * 100 + 50) + "%"; 535 536 var colors = []; 537 538 // inside radius (%) 539 if (this.fillStyle.type_ == "gradientradial") { 540 var inside = (this.fillStyle.radius1_ / dimension * 100); 541 542 // percentage that outside radius exceeds inside radius 543 var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside; 544 } else { 545 var inside = 0; 546 var expansion = 100; 547 } 548 549 var insidecolor = {offset: null, color: null}; 550 var outsidecolor = {offset: null, color: null}; 551 552 // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie 553 // won't interpret it correctly 554 this.fillStyle.colors_.sort(function (cs1, cs2) { 555 return cs1.offset - cs2.offset; 556 }); 557 558 for (var i = 0; i < this.fillStyle.colors_.length; i++) { 559 var fs = this.fillStyle.colors_[i]; 560 561 colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ","); 562 563 if (fs.offset > insidecolor.offset || insidecolor.offset == null) { 564 insidecolor.offset = fs.offset; 565 insidecolor.color = fs.color; 566 } 567 568 if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) { 569 outsidecolor.offset = fs.offset; 570 outsidecolor.color = fs.color; 571 } 572 } 573 colors.pop(); 574 575 lineStr.push('<g_vml_:fill', 576 ' color="', outsidecolor.color, '"', 577 ' color2="', insidecolor.color, '"', 578 ' type="', this.fillStyle.type_, '"', 579 ' focusposition="', focus.x, ', ', focus.y, '"', 580 ' colors="', colors.join(""), '"', 581 ' opacity="', opacity, '" />'); 582 } else if (aFill) { 583 lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity, '" />'); 584 } else { 585 lineStr.push( 586 '<g_vml_:stroke', 587 ' opacity="', opacity,'"', 588 ' joinstyle="', this.lineJoin, '"', 589 ' miterlimit="', this.miterLimit, '"', 590 ' endcap="', processLineCap(this.lineCap) ,'"', 591 ' weight="', this.lineWidth, 'px"', 592 ' color="', color,'" />' 593 ); 594 } 595 596 lineStr.push("</g_vml_:shape>"); 597 598 this.element_.insertAdjacentHTML("beforeEnd", lineStr.join("")); 599 600 this.currentPath_ = []; 601 }; 602 603 contextPrototype.fill = function() { 604 this.stroke(true); 605 } 606 607 contextPrototype.closePath = function() { 608 this.currentPath_.push({type: "close"}); 609 }; 610 611 /** 612 * @private 613 */ 614 contextPrototype.getCoords_ = function(aX, aY) { 615 return { 616 x: (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]), 617 y: (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) 618 } 619 }; 620 621 contextPrototype.save = function() { 622 var o = {}; 623 copyState(this, o); 624 this.aStack_.push(o); 625 this.mStack_.push(this.m_); 626 this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); 627 }; 628 629 contextPrototype.restore = function() { 630 copyState(this.aStack_.pop(), this); 631 this.m_ = this.mStack_.pop(); 632 }; 633 634 contextPrototype.translate = function(aX, aY) { 635 var m1 = [ 636 [1, 0, 0], 637 [0, 1, 0], 638 [aX, aY, 1] 639 ]; 640 641 this.m_ = matrixMultiply(m1, this.m_); 642 }; 643 644 contextPrototype.rotate = function(aRot) { 645 var c = Math.cos(aRot); 646 var s = Math.sin(aRot); 647 648 var m1 = [ 649 [c, s, 0], 650 [-s, c, 0], 651 [0, 0, 1] 652 ]; 653 654 this.m_ = matrixMultiply(m1, this.m_); 655 }; 656 657 contextPrototype.scale = function(aX, aY) { 658 var m1 = [ 659 [aX, 0, 0], 660 [0, aY, 0], 661 [0, 0, 1] 662 ]; 663 664 this.m_ = matrixMultiply(m1, this.m_); 665 }; 666 667 /******** STUBS ********/ 668 contextPrototype.clip = function() { 669 // TODO: Implement 670 }; 671 672 contextPrototype.arcTo = function() { 673 // TODO: Implement 674 }; 675 676 contextPrototype.createPattern = function() { 677 return new CanvasPattern_; 678 }; 679 680 // Gradient / Pattern Stubs 681 function CanvasGradient_(aType) { 682 this.type_ = aType; 683 this.radius1_ = 0; 684 this.radius2_ = 0; 685 this.colors_ = []; 686 this.focus_ = {x: 0, y: 0}; 687 } 688 689 CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { 690 aColor = processStyle(aColor); 691 this.colors_.push({offset: 1-aOffset, color: aColor}); 692 }; 693 694 function CanvasPattern_() {} 695 696 // set up externs 697 G_vmlCanvasManager = G_vmlCanvasManager_; 698 CanvasRenderingContext2D = CanvasRenderingContext2D_; 699 CanvasGradient = CanvasGradient_; 700 CanvasPattern = CanvasPattern_; 701 702})(); 703 704} // if