PageRenderTime 134ms CodeModel.GetById 104ms app.highlight 24ms RepoModel.GetById 1ms app.codeStats 0ms

/examples/addonsgallery/public/excanvas.js

http://pyjamas.googlecode.com/
JavaScript | 704 lines | 539 code | 92 blank | 73 comment | 60 complexity | 1f48564d9be1a2b986399b98af5e74ca MD5 | raw file
  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