1 // Copyright 2006 Google Inc.
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
7 // http://www.apache.org/licenses/LICENSE
-2.0
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.
16 // TODO: Radial gradient
17 // TODO: Clipping paths
18 // TODO: Coordsize (still need to support stretching)
19 // TODO: Painting mode
21 // TODO: canvas width/height sets content size
in moz
, border size
in ie
23 // only add this code if we do not already have a canvas implementation
24 if (!window
.CanvasRenderingContext2D
) {
28 // alias some functions to make (compiled) code shorter
34 var G_vmlCanvasManager_
= {
35 init
: function (opt_doc
) {
36 var doc
= opt_doc
|| document
;
37 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
) {
39 doc
.attachEvent("onreadystatechange", function () {
45 init_
: function (doc
, e
) {
46 if (doc
.readyState
== "complete") {
48 if (!doc
.namespaces
["g_vml_"]) {
49 doc
.namespaces
.add("g_vml_", "urn:schemas-microsoft-com:vml");
53 var ss
= doc
.createStyleSheet();
54 ss
.cssText
= "canvas{display:inline-block;overflow:hidden;" +
56 "g_vml_\\:*{behavior:url(#default#VML)}";
58 // find all canvas elements
59 var els
= doc
.getElementsByTagName("canvas");
60 for (var i
= 0; i
< els
.length
; i
++) {
61 if (!els
[i
].getContext
) {
62 this.initElement(els
[i
]);
68 fixElement_
: function (el
) {
69 // in IE before version 5.5 we would need to add HTML: to the tag name
70 // but we do not care about IE before version 6
71 var outerHTML
= el
.outerHTML
;
72 var newEl
= document
.createElement(outerHTML
);
73 // if the tag is still open IE has created the children as siblings and
74 // it has also created a tag with the name "/FOO
"
75 if (outerHTML.slice(-2) != "/>") {
76 var tagName = "/" + el.tagName;
79 while ((ns = el.nextSibling) && ns.tagName != tagName) {
82 // remove the incorrect closing tag
87 el.parentNode.replaceChild(newEl, el);
92 * Public initializes a canvas element so that it can be used as canvas
93 * element from now on. This is called automatically before the page is
94 * loaded but if you are creating elements using createElement you need to
95 * make sure this is called on the element.
96 * @param {HTMLElement} el The canvas element to initialize.
97 * @return {HTMLElement} the element that was created.
99 initElement: function (el) {
100 el = this.fixElement_(el);
101 el.getContext = function () {
103 return this.context_;
105 return this.context_ = new CanvasRenderingContext2D_(this);
108 // do not use inline function because that will leak memory
109 // el.attachEvent('onpropertychange', onPropertyChange)
110 el.attachEvent('onresize', onResize);
112 var attrs = el.attributes;
113 if (attrs.width && attrs.width.specified) {
114 // TODO: use runtimeStyle and coordsize
115 // el.getContext().setWidth_(attrs.width.nodeValue);
116 el.style.width = attrs.width.nodeValue + "px
";
118 if (attrs.height && attrs.height.specified) {
119 // TODO: use runtimeStyle and coordsize
120 // el.getContext().setHeight_(attrs.height.nodeValue);
121 el.style.height = attrs.height.nodeValue + "px
";
123 //el.getContext().setCoordsize_()
128 function onPropertyChange(e) {
129 // we need to watch changes to width and height
130 switch (e.propertyName) {
133 // TODO: coordsize and size
138 function onResize(e) {
139 var el = e.srcElement;
141 el.firstChild.style.width = el.clientWidth + 'px';
142 el.firstChild.style.height = el.clientHeight + 'px';
146 G_vmlCanvasManager_.init();
148 // precompute "00" to "FF
"
150 for (var i = 0; i < 16; i++) {
151 for (var j = 0; j < 16; j++) {
152 dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
156 function createMatrixIdentity() {
164 function matrixMultiply(m1, m2) {
165 var result = createMatrixIdentity();
167 for (var x = 0; x < 3; x++) {
168 for (var y = 0; y < 3; y++) {
171 for (var z = 0; z < 3; z++) {
172 sum += m1[x][z] * m2[z][y];
181 function copyState(o1, o2) {
182 o2.fillStyle = o1.fillStyle;
183 o2.lineCap = o1.lineCap;
184 o2.lineJoin = o1.lineJoin;
185 o2.lineWidth = o1.lineWidth;
186 o2.miterLimit = o1.miterLimit;
187 o2.shadowBlur = o1.shadowBlur;
188 o2.shadowColor = o1.shadowColor;
189 o2.shadowOffsetX = o1.shadowOffsetX;
190 o2.shadowOffsetY = o1.shadowOffsetY;
191 o2.strokeStyle = o1.strokeStyle;
194 function processStyle(styleString) {
197 styleString = String(styleString);
198 if (styleString.substring(0, 3) == "rgb
") {
199 var start = styleString.indexOf("(", 3);
200 var end = styleString.indexOf(")", start + 1);
201 var guts = styleString.substring(start + 1, end).split(",");
204 for (var i = 0; i < 3; i++) {
205 str += dec2hex[parseInt(guts[i])];
208 if ((guts.length == 4) && (styleString.substr(3, 1) == "a
")) {
218 function processLineCap(lineCap) {
231 * This class implements CanvasRenderingContext2D interface as described by
233 * @param {HTMLElement} surfaceElement The element that the 2D context should
236 function CanvasRenderingContext2D_(surfaceElement) {
237 this.m_ = createMatrixIdentity();
241 this.currentPath_ = [];
243 // Canvas context properties
244 this.strokeStyle = "#000";
245 this.fillStyle = "#ccc
";
248 this.lineJoin = "miter
";
249 this.lineCap = "butt
";
250 this.miterLimit = 10;
251 this.globalAlpha = 1;
253 var el = document.createElement('div');
254 el.style.width = surfaceElement.clientWidth + 'px';
255 el.style.height = surfaceElement.clientHeight + 'px';
256 el.style.overflow = 'hidden';
257 el.style.position = 'absolute';
258 surfaceElement.appendChild(el);
265 var contextPrototype = CanvasRenderingContext2D_.prototype;
266 contextPrototype.clearRect = function() {
267 this.element_.innerHTML = "";
268 this.currentPath_ = [];
271 contextPrototype.beginPath = function() {
272 // TODO: Branch current matrix so that save/restore has no effect
273 // as per safari docs.
275 this.currentPath_ = [];
278 contextPrototype.moveTo = function(aX, aY) {
279 this.currentPath_.push({type: "moveTo
", x: aX, y: aY});
282 contextPrototype.lineTo = function(aX, aY) {
283 this.currentPath_.push({type: "lineTo
", x: aX, y: aY});
286 contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
289 this.currentPath_.push({type: "bezierCurveTo
",
298 contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
299 // VML's qb produces different output to Firefox's
300 // FF's behaviour seems to have changed in 1.5.0.1, check this
301 this.bezierCurveTo(aCPx, aCPy, aCPx, aCPy, aX, aY);
304 contextPrototype.arc = function(aX, aY, aRadius,
305 aStartAngle, aEndAngle, aClockwise) {
307 var arcType = aClockwise ? "at
" : "wa
";
309 var xStart = aX + (mc(aStartAngle) * aRadius) - 5;
310 var yStart = aY + (ms(aStartAngle) * aRadius) - 5;
312 var xEnd = aX + (mc(aEndAngle) * aRadius) - 5;
313 var yEnd = aY + (ms(aEndAngle) * aRadius) - 5;
315 this.currentPath_.push({type: arcType,
326 contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
328 this.lineTo(aX + aWidth, aY);
329 this.lineTo(aX + aWidth, aY + aHeight);
330 this.lineTo(aX, aY + aHeight);
334 contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
335 // Will destroy any existing path (same as FF behaviour)
338 this.lineTo(aX + aWidth, aY);
339 this.lineTo(aX + aWidth, aY + aHeight);
340 this.lineTo(aX, aY + aHeight);
345 contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
346 // Will destroy any existing path (same as FF behaviour)
349 this.lineTo(aX + aWidth, aY);
350 this.lineTo(aX + aWidth, aY + aHeight);
351 this.lineTo(aX, aY + aHeight);
356 contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
357 var gradient = new CanvasGradient_("gradient
");
361 contextPrototype.createRadialGradient = function(aX0, aY0,
364 var gradient = new CanvasGradient_("gradientradial
");
365 gradient.radius1_ = aR0;
366 gradient.radius2_ = aR1;
367 gradient.focus_.x = aX0;
368 gradient.focus_.y = aY0;
372 contextPrototype.drawImage = function (image, var_args) {
373 var dx, dy, dw, dh, sx, sy, sw, sh;
375 var h = image.height;
377 if (arguments.length == 3) {
383 } else if (arguments.length == 5) {
391 } else if (arguments.length == 9) {
401 throw "Invalid number of arguments
";
404 var d = this.getCoords_(dx, dy);
411 // For some reason that I've now forgotten, using divs didn't work
412 vmlStr.push(' <g_vml_:group',
413 ' coordsize="1000,1000"',
414 ' coordorigin="0, 0"' ,
415 ' style="width
:100px
;height
:100px
;position
:absolute
;');
417 // If filters are necessary (rotation exists), create them
418 // filters are bog-slow, so only create them if abbsolutely necessary
419 // The following check doesn't account
for skews (which don
't exist
420 // in the canvas spec (yet) anyway.
422 if (this.m_[0][0] != 1 || this.m_[0][1]) {
425 // Note the 12/21 reversal
426 filter.push("M11='", this.m_[0][0], "',",
427 "M12='", this.m_[1][0], "',",
428 "M21='", this.m_[0][1], "',",
429 "M22='", this.m_[1][1], "',",
433 // Bounding box calculation (need to minimize displayed area so that
434 // filters don't waste time on unused pixels
.
436 var c2
= this.getCoords_(dx
+dw
, dy
);
437 var c3
= this.getCoords_(dx
, dy
+dh
);
438 var c4
= this.getCoords_(dx
+dw
, dy
+dh
);
440 max
.x
= Math
.max(max
.x
, c2
.x
, c3
.x
, c4
.x
);
441 max
.y
= Math
.max(max
.y
, c2
.y
, c3
.y
, c4
.y
);
443 vmlStr
.push(" padding:0 ", mr(max
.x
), "px ", mr(max
.y
),
444 "px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",
445 filter
.join(""), ", sizingmethod='clip');")
447 vmlStr
.push(" top:", d
.y
, "px;left:", d
.x
, "px;")
451 '<g_vml_:image src="', image
.src
, '"',
452 ' style="width:', dw
, ';',
453 ' height:', dh
, ';"',
454 ' cropleft="', sx
/ w
, '"',
455 ' croptop="', sy
/ h
, '"',
456 ' cropright="', (w
- sx
- sw
) / w
, '"',
457 ' cropbottom="', (h
- sy
- sh
) / h
, '"',
461 this.element_
.insertAdjacentHTML("BeforeEnd",
465 contextPrototype
.stroke
= function(aFill
) {
467 var lineOpen
= false;
468 var a
= processStyle(aFill
? this.fillStyle
: this.strokeStyle
);
470 var opacity
= a
[1] * this.globalAlpha
;
472 lineStr
.push('<g_vml_:shape',
473 ' fillcolor="', color
, '"',
474 ' filled="', Boolean(aFill
), '"',
475 ' style="position:absolute;width:10;height:10;"',
476 ' coordorigin="0 0" coordsize="100 100"',
477 ' stroked="', !aFill
, '"',
478 ' strokeweight="', this.lineWidth
, '"',
479 ' strokecolor="', color
, '"',
483 var min
= {x
: null, y
: null};
484 var max
= {x
: null, y
: null};
486 for (var i
= 0; i
< this.currentPath_
.length
; i
++) {
487 var p
= this.currentPath_
[i
];
489 if (p
.type
== "moveTo") {
491 var c
= this.getCoords_(p
.x
, p
.y
);
492 lineStr
.push(mr(c
.x
), ",", mr(c
.y
));
493 } else if (p
.type
== "lineTo") {
495 var c
= this.getCoords_(p
.x
, p
.y
);
496 lineStr
.push(mr(c
.x
), ",", mr(c
.y
));
497 } else if (p
.type
== "close") {
499 } else if (p
.type
== "bezierCurveTo") {
501 var c
= this.getCoords_(p
.x
, p
.y
);
502 var c1
= this.getCoords_(p
.cp1x
, p
.cp1y
);
503 var c2
= this.getCoords_(p
.cp2x
, p
.cp2y
);
504 lineStr
.push(mr(c1
.x
), ",", mr(c1
.y
), ",",
505 mr(c2
.x
), ",", mr(c2
.y
), ",",
506 mr(c
.x
), ",", mr(c
.y
));
507 } else if (p
.type
== "at" || p
.type
== "wa") {
508 lineStr
.push(" ", p
.type
, " ");
509 var c
= this.getCoords_(p
.x
, p
.y
);
510 var cStart
= this.getCoords_(p
.xStart
, p
.yStart
);
511 var cEnd
= this.getCoords_(p
.xEnd
, p
.yEnd
);
513 lineStr
.push(mr(c
.x
- this.arcScaleX_
* p
.radius
), ",",
514 mr(c
.y
- this.arcScaleY_
* p
.radius
), " ",
515 mr(c
.x
+ this.arcScaleX_
* p
.radius
), ",",
516 mr(c
.y
+ this.arcScaleY_
* p
.radius
), " ",
517 mr(cStart
.x
), ",", mr(cStart
.y
), " ",
518 mr(cEnd
.x
), ",", mr(cEnd
.y
));
522 // TODO: Following is broken for curves due to
523 // move to proper paths.
525 // Figure out dimensions so we can do gradient fills
528 if (min
.x
== null || c
.x
< min
.x
) {
531 if (max
.x
== null || c
.x
> max
.x
) {
534 if (min
.y
== null || c
.y
< min
.y
) {
537 if (max
.y
== null || c
.y
> max
.y
) {
544 if (typeof this.fillStyle
== "object") {
545 var focus
= {x
: "50%", y
: "50%"};
546 var width
= (max
.x
- min
.x
);
547 var height
= (max
.y
- min
.y
);
548 var dimension
= (width
> height
) ? width
: height
;
550 focus
.x
= mr((this.fillStyle
.focus_
.x
/ width
) * 100 + 50) + "%";
551 focus
.y
= mr((this.fillStyle
.focus_
.y
/ height
) * 100 + 50) + "%";
556 if (this.fillStyle
.type_
== "gradientradial") {
557 var inside
= (this.fillStyle
.radius1_
/ dimension
* 100);
559 // percentage that outside radius exceeds inside radius
560 var expansion
= (this.fillStyle
.radius2_
/ dimension
* 100) - inside
;
566 var insidecolor
= {offset
: null, color
: null};
567 var outsidecolor
= {offset
: null, color
: null};
569 // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie
570 // won't interpret it correctly
571 this.fillStyle
.colors_
.sort(function (cs1
, cs2
) {
572 return cs1
.offset
- cs2
.offset
;
575 for (var i
= 0; i
< this.fillStyle
.colors_
.length
; i
++) {
576 var fs
= this.fillStyle
.colors_
[i
];
578 colors
.push( (fs
.offset
* expansion
) + inside
, "% ", fs
.color
, ",");
580 if (fs
.offset
> insidecolor
.offset
|| insidecolor
.offset
== null) {
581 insidecolor
.offset
= fs
.offset
;
582 insidecolor
.color
= fs
.color
;
585 if (fs
.offset
< outsidecolor
.offset
|| outsidecolor
.offset
== null) {
586 outsidecolor
.offset
= fs
.offset
;
587 outsidecolor
.color
= fs
.color
;
592 lineStr
.push('<g_vml_:fill',
593 ' color="', outsidecolor
.color
, '"',
594 ' color2="', insidecolor
.color
, '"',
595 ' type="', this.fillStyle
.type_
, '"',
596 ' focusposition="', focus
.x
, ', ', focus
.y
, '"',
597 ' colors="', colors
.join(""), '"',
598 ' opacity="', opacity
, '" />');
600 lineStr
.push('<g_vml_:fill color="', color
, '" opacity="', opacity
, '" />');
604 ' opacity="', opacity
,'"',
605 ' joinstyle="', this.lineJoin
, '"',
606 ' miterlimit="', this.miterLimit
, '"',
607 ' endcap="', processLineCap(this.lineCap
) ,'"',
608 ' weight="', this.lineWidth
, 'px"',
609 ' color="', color
,'" />'
613 lineStr
.push("</g_vml_:shape>");
615 this.element_
.insertAdjacentHTML("beforeEnd", lineStr
.join(""));
617 this.currentPath_
= [];
620 contextPrototype
.fill
= function() {
624 contextPrototype
.closePath
= function() {
625 this.currentPath_
.push({type
: "close"});
631 contextPrototype
.getCoords_
= function(aX
, aY
) {
633 x
: 10 * (aX
* this.m_
[0][0] + aY
* this.m_
[1][0] + this.m_
[2][0]) - 5,
634 y
: 10 * (aX
* this.m_
[0][1] + aY
* this.m_
[1][1] + this.m_
[2][1]) - 5
638 contextPrototype
.save
= function() {
641 this.aStack_
.push(o
);
642 this.mStack_
.push(this.m_
);
643 this.m_
= matrixMultiply(createMatrixIdentity(), this.m_
);
646 contextPrototype
.restore
= function() {
647 copyState(this.aStack_
.pop(), this);
648 this.m_
= this.mStack_
.pop();
651 contextPrototype
.translate
= function(aX
, aY
) {
658 this.m_
= matrixMultiply(m1
, this.m_
);
661 contextPrototype
.rotate
= function(aRot
) {
671 this.m_
= matrixMultiply(m1
, this.m_
);
674 contextPrototype
.scale
= function(aX
, aY
) {
675 this.arcScaleX_
*= aX
;
676 this.arcScaleY_
*= aY
;
683 this.m_
= matrixMultiply(m1
, this.m_
);
686 /******** STUBS ********/
687 contextPrototype
.clip
= function() {
691 contextPrototype
.arcTo
= function() {
695 contextPrototype
.createPattern
= function() {
696 return new CanvasPattern_
;
699 // Gradient / Pattern Stubs
700 function CanvasGradient_(aType
) {
705 this.focus_
= {x
: 0, y
: 0};
708 CanvasGradient_
.prototype.addColorStop
= function(aOffset
, aColor
) {
709 aColor
= processStyle(aColor
);
710 this.colors_
.push({offset
: 1-aOffset
, color
: aColor
});
713 function CanvasPattern_() {}
716 G_vmlCanvasManager
= G_vmlCanvasManager_
;
717 CanvasRenderingContext2D
= CanvasRenderingContext2D_
;
718 CanvasGradient
= CanvasGradient_
;
719 CanvasPattern
= CanvasPattern_
;