define(["./_base", "dojo/_base/lang", "dojo/_base/array", "dojo/_base/declare", "dojo/_base/window", "dojo/dom-geometry", "dojo/dom", "./shape", "./path", "./arc", "./matrix", "./decompose", "./bezierutils"], function(g, lang, arr, declare, win, domGeom, dom, gs, pathLib, ga, m, decompose, bezierUtils ){ var canvas = g.canvas = { // summary: // This the graphics rendering bridge for W3C Canvas compliant browsers. // Since Canvas is an immediate mode graphics api, with no object graph or // eventing capabilities, use of this module alone will only add in drawing support. // The additional module, canvasWithEvents extends this module with additional support // for handling events on Canvas. By default, the support for events is now included // however, if only drawing capabilities are needed, canvas event module can be disabled // using the dojoConfig option, canvasEvents:true|false. // The id of the Canvas renderer is 'canvas'. This id can be used when switch Dojo's // graphics context between renderer implementations. See dojox/gfx/_base.switchRenderer // API. }; var pattrnbuffer = null, mp = m.multiplyPoint, pi = Math.PI, twoPI = 2 * pi, halfPI = pi /2, extend = lang.extend; if(win.global.CanvasRenderingContext2D){ var ctx2d = win.doc.createElement("canvas").getContext("2d"); var hasNativeDash = typeof ctx2d.setLineDash == "function"; var hasFillText = typeof ctx2d.fillText == "function"; } var dasharray = { solid: "none", shortdash: [4, 1], shortdot: [1, 1], shortdashdot: [4, 1, 1, 1], shortdashdotdot: [4, 1, 1, 1, 1, 1], dot: [1, 3], dash: [4, 3], longdash: [8, 3], dashdot: [4, 3, 1, 3], longdashdot: [8, 3, 1, 3], longdashdotdot: [8, 3, 1, 3, 1, 3] }; function drawDashedArc(/*CanvasRenderingContext2D*/ctx, /*Number[]*/dash, /*int*/cx, /*int*/cy, /*int*/r, /*Number*/sa, /*Number*/ea, /*Boolean*/ccw, /*Boolean?*/apply, prevResidue){ var residue, angle, l = dash.length, i= 0; // if there's a previous dash residue from the previous arc, start with it. if(prevResidue){ angle = prevResidue.l/r; i = prevResidue.i; }else{ angle = dash[0]/r; } while(sa < ea){ // if the dash segment length is longer than what remains to stroke, keep it for next arc. (aka residue) if(sa+angle > ea){ residue = {l: (sa+angle-ea)*r, i: i}; angle = ea-sa; } if(!(i%2)){ ctx.beginPath(); ctx.arc(cx, cy, r, sa, sa+angle, ccw); if(apply) ctx.stroke(); } sa += angle; ++i; angle = dash[i%l]/r; } return residue; } function splitToDashedBezier(/*Number[]*/points, /*Number[]*/dashArray, /*Number[]*/newPoints, /*Object*/prevResidue){ var residue = 0, t = 0, dash, i = 0; if(prevResidue){ dash = prevResidue.l; i = prevResidue.i; }else{ dash = dashArray[0]; } while(t<1){ // get the 't' corresponding to the given dash value. t = bezierUtils.tAtLength(points, dash); if(t==1){ var rl = bezierUtils.computeLength(points); residue = {l: dash-rl, i: i}; } // split bezier at t: left part is the "dash" curve, right part is the remaining bezier points var curves = bezierUtils.splitBezierAtT(points, t); if(!(i%2)){ // only keep the "dash" curve newPoints.push(curves[0]); } points = curves[1]; ++i; dash = dashArray[i%dashArray.length]; } return residue; } function toDashedCurveTo(/*Array||CanvasRenderingContext2D*/ctx, /*shape.Path*/shape, /*Number[]*/points, /*Object*/prevResidue){ // summary: // Builds a set of bezier (cubic || quadratic)curveTo' canvas instructions that represents a dashed stroke of the specified bezier geometry. var pts = [shape.last.x, shape.last.y].concat(points), quadratic = points.length === 4, ctx2d = !(ctx instanceof Array), api = quadratic ? "quadraticCurveTo" : "bezierCurveTo", curves = []; var residue = splitToDashedBezier(pts, shape.canvasDash, curves, prevResidue); for(var c=0; c0.01){ if(dal>tlength){ residue = {l:dal-tlength,i:i}; dal=tlength; } r = dal/tlength; x = x1 + (x2-x1)*r; y = y1 + (y2-y1)*r; if(!(i++%2)){ if(ctx2d){ ctx.moveTo(prevx, prevy); ctx.lineTo(x, y); }else{ ctx.push("moveTo", [prevx, prevy]); ctx.push("lineTo", [x, y]); } } prevx = x; prevy = y; dal += dash[i%dash.length]; } return residue; } canvas.Shape = declare("dojox.gfx.canvas.Shape", gs.Shape, { _render: function(/* Object */ ctx){ // summary: // render the shape ctx.save(); this._renderTransform(ctx); this._renderClip(ctx); this._renderShape(ctx); this._renderFill(ctx, true); this._renderStroke(ctx, true); ctx.restore(); }, _renderClip: function(ctx){ if (this.canvasClip){ this.canvasClip.render(ctx); ctx.clip(); } }, _renderTransform: function(/* Object */ ctx){ if("canvasTransform" in this){ var t = this.canvasTransform; ctx.translate(t.dx, t.dy); ctx.rotate(t.angle2); ctx.scale(t.sx, t.sy); ctx.rotate(t.angle1); // The future implementation when vendors catch up with the spec: // var t = this.matrix; // ctx.transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy); } }, _renderShape: function(/* Object */ ctx){ // nothing }, _renderFill: function(/* Object */ ctx, /* Boolean */ apply){ if("canvasFill" in this){ var fs = this.fillStyle; if("canvasFillImage" in this){ var w = fs.width, h = fs.height, iw = this.canvasFillImage.width, ih = this.canvasFillImage.height, // let's match the svg default behavior wrt. aspect ratio: xMidYMid meet sx = w == iw ? 1 : w / iw, sy = h == ih ? 1 : h / ih, s = Math.min(sx,sy), //meet->math.min , slice->math.max dx = (w - s * iw)/2, dy = (h - s * ih)/2; // the buffer used to scaled the image pattrnbuffer.width = w; pattrnbuffer.height = h; var copyctx = pattrnbuffer.getContext("2d"); copyctx.clearRect(0, 0, w, h); copyctx.drawImage(this.canvasFillImage, 0, 0, iw, ih, dx, dy, s*iw, s*ih); this.canvasFill = ctx.createPattern(pattrnbuffer, "repeat"); delete this.canvasFillImage; } ctx.fillStyle = this.canvasFill; if(apply){ // offset the pattern if (fs.type==="pattern" && (fs.x !== 0 || fs.y !== 0)) { ctx.translate(fs.x,fs.y); } ctx.fill(); } }else{ ctx.fillStyle = "rgba(0,0,0,0.0)"; } }, _renderStroke: function(/* Object */ ctx, /* Boolean */ apply){ var s = this.strokeStyle; if(s){ ctx.strokeStyle = s.color.toString(); ctx.lineWidth = s.width; ctx.lineCap = s.cap; if(typeof s.join == "number"){ ctx.lineJoin = "miter"; ctx.miterLimit = s.join; }else{ ctx.lineJoin = s.join; } if(this.canvasDash){ if(hasNativeDash){ ctx.setLineDash(this.canvasDash); if(apply){ ctx.stroke(); } }else{ this._renderDashedStroke(ctx, apply); } }else{ if(apply){ ctx.stroke(); } } }else if(!apply){ ctx.strokeStyle = "rgba(0,0,0,0.0)"; } }, _renderDashedStroke: function(ctx, apply){}, // events are not implemented getEventSource: function(){ return null; }, on: function(){}, connect: function(){}, disconnect: function(){}, canvasClip:null, setClip: function(/*Object*/clip){ this.inherited(arguments); var clipType = clip ? "width" in clip ? "rect" : "cx" in clip ? "ellipse" : "points" in clip ? "polyline" : "d" in clip ? "path" : null : null; if(clip && !clipType){ return this; } this.canvasClip = clip ? makeClip(clipType, clip) : null; if(this.parent){this.parent._makeDirty();} return this; } }); var makeClip = function(clipType, geometry){ switch(clipType){ case "ellipse": return { canvasEllipse: makeEllipse({shape:geometry}), render: function(ctx){return canvas.Ellipse.prototype._renderShape.call(this, ctx);} }; case "rect": return { shape: lang.delegate(geometry,{r:0}), render: function(ctx){return canvas.Rect.prototype._renderShape.call(this, ctx);} }; case "path": return { canvasPath: makeClipPath(geometry), render: function(ctx){this.canvasPath._renderShape(ctx);} }; case "polyline": return { canvasPolyline: geometry.points, render: function(ctx){return canvas.Polyline.prototype._renderShape.call(this, ctx);} }; } return null; }; var makeClipPath = function(geo){ var p = new dojox.gfx.canvas.Path(); p.canvasPath = []; p._setPath(geo.d); return p; }; var modifyMethod = function(shape, method, extra){ var old = shape.prototype[method]; shape.prototype[method] = extra ? function(){ if(this.parent){this.parent._makeDirty();} old.apply(this, arguments); extra.call(this); return this; } : function(){ if(this.parent){this.parent._makeDirty();} return old.apply(this, arguments); }; }; modifyMethod(canvas.Shape, "setTransform", function(){ // prepare Canvas-specific structures if(this.matrix){ this.canvasTransform = g.decompose(this.matrix); }else{ delete this.canvasTransform; } }); modifyMethod(canvas.Shape, "setFill", function(){ // prepare Canvas-specific structures var fs = this.fillStyle, f; if(fs){ if(typeof(fs) == "object" && "type" in fs){ var ctx = this.surface.rawNode.getContext("2d"); switch(fs.type){ case "linear": case "radial": f = fs.type == "linear" ? ctx.createLinearGradient(fs.x1, fs.y1, fs.x2, fs.y2) : ctx.createRadialGradient(fs.cx, fs.cy, 0, fs.cx, fs.cy, fs.r); arr.forEach(fs.colors, function(step){ f.addColorStop(step.offset, g.normalizeColor(step.color).toString()); }); break; case "pattern": if (!pattrnbuffer) { pattrnbuffer = document.createElement("canvas"); } // no need to scale the image since the canvas.createPattern uses // the original image data and not the scaled ones (see spec.) // the scaling needs to be done at rendering time in a context buffer var img =new Image(); this.surface.downloadImage(img, fs.src); this.canvasFillImage = img; } }else{ // Set fill color using CSS RGBA func style f = fs.toString(); } this.canvasFill = f; }else{ delete this.canvasFill; } }); modifyMethod(canvas.Shape, "setStroke", function(){ var st = this.strokeStyle; if(st){ var da = this.strokeStyle.style.toLowerCase(); if(da in dasharray){ da = dasharray[da]; } if(da instanceof Array){ da = da.slice(); this.canvasDash = da; var i; for(i = 0; i < da.length; ++i){ da[i] *= st.width; } if(st.cap != "butt"){ for(i = 0; i < da.length; i += 2){ da[i] -= st.width; if(da[i] < 1){ da[i] = 1; } } for(i = 1; i < da.length; i += 2){ da[i] += st.width; } } }else{ delete this.canvasDash; } }else{ delete this.canvasDash; } this._needsDash = !hasNativeDash && !!this.canvasDash; }); modifyMethod(canvas.Shape, "setShape"); canvas.Group = declare("dojox.gfx.canvas.Group", canvas.Shape, { // summary: // a group shape (Canvas), which can be used // to logically group shapes (e.g, to propagate matricies) constructor: function(){ gs.Container._init.call(this); }, _render: function(/* Object */ ctx){ // summary: // render the group ctx.save(); this._renderTransform(ctx); this._renderClip(ctx); for(var i = 0; i < this.children.length; ++i){ this.children[i]._render(ctx); } ctx.restore(); }, destroy: function(){ // summary: // Releases all internal resources owned by this shape. Once this method has been called, // the instance is considered disposed and should not be used anymore. // don't call canvas impl to avoid makeDirty' gs.Container.clear.call(this, true); // avoid this.inherited canvas.Shape.prototype.destroy.apply(this, arguments); } }); canvas.Rect = declare("dojox.gfx.canvas.Rect", [canvas.Shape, gs.Rect], { // summary: // a rectangle shape (Canvas) _renderShape: function(/* Object */ ctx){ var s = this.shape, r = Math.min(s.r, s.height / 2, s.width / 2), xl = s.x, xr = xl + s.width, yt = s.y, yb = yt + s.height, xl2 = xl + r, xr2 = xr - r, yt2 = yt + r, yb2 = yb - r; ctx.beginPath(); ctx.moveTo(xl2, yt); if(r){ ctx.arc(xr2, yt2, r, -halfPI, 0, false); ctx.arc(xr2, yb2, r, 0, halfPI, false); ctx.arc(xl2, yb2, r, halfPI, pi, false); ctx.arc(xl2, yt2, r, pi, pi + halfPI, false); }else{ ctx.lineTo(xr2, yt); ctx.lineTo(xr, yb2); ctx.lineTo(xl2, yb); ctx.lineTo(xl, yt2); } ctx.closePath(); }, _renderDashedStroke: function(ctx, apply){ var s = this.shape, residue, r = Math.min(s.r, s.height / 2, s.width / 2), xl = s.x, xr = xl + s.width, yt = s.y, yb = yt + s.height, xl2 = xl + r, xr2 = xr - r, yt2 = yt + r, yb2 = yb - r; if(r){ ctx.beginPath(); residue = toDashedLineTo(ctx, this, xl2, yt, xr2, yt); if(apply) ctx.stroke(); residue = drawDashedArc(ctx, this.canvasDash, xr2, yt2, r, -halfPI, 0, false, apply, residue); ctx.beginPath(); residue = toDashedLineTo(ctx, this, xr, yt2, xr, yb2, residue); if(apply) ctx.stroke(); residue = drawDashedArc(ctx, this.canvasDash, xr2, yb2, r, 0, halfPI, false, apply, residue); ctx.beginPath(); residue = toDashedLineTo(ctx, this, xr2, yb, xl2, yb, residue); if(apply) ctx.stroke(); residue = drawDashedArc(ctx, this.canvasDash, xl2, yb2, r, halfPI, pi, false, apply, residue); ctx.beginPath(); residue = toDashedLineTo(ctx, this, xl, yb2, xl, yt2,residue); if(apply) ctx.stroke(); drawDashedArc(ctx, this.canvasDash, xl2, yt2, r, pi, pi + halfPI, false, apply, residue); }else{ ctx.beginPath(); residue = toDashedLineTo(ctx, this, xl2, yt, xr2, yt); residue = toDashedLineTo(ctx, this, xr2, yt, xr, yb2, residue); residue = toDashedLineTo(ctx, this, xr, yb2, xl2, yb, residue); toDashedLineTo(ctx, this, xl2, yb, xl, yt2, residue); if(apply) ctx.stroke(); } } }); var bezierCircle = []; (function(){ var u = ga.curvePI4; bezierCircle.push(u.s, u.c1, u.c2, u.e); for(var a = 45; a < 360; a += 45){ var r = m.rotateg(a); bezierCircle.push(mp(r, u.c1), mp(r, u.c2), mp(r, u.e)); } })(); var makeEllipse = function(shape){ // prepare Canvas-specific structures var t, c1, c2, r = [], s = shape.shape, M = m.normalize([m.translate(s.cx, s.cy), m.scale(s.rx, s.ry)]); t = mp(M, bezierCircle[0]); r.push([t.x, t.y]); for(var i = 1; i < bezierCircle.length; i += 3){ c1 = mp(M, bezierCircle[i]); c2 = mp(M, bezierCircle[i + 1]); t = mp(M, bezierCircle[i + 2]); r.push([c1.x, c1.y, c2.x, c2.y, t.x, t.y]); } if(shape._needsDash){ var points = [], p1 = r[0]; for(i = 1; i < r.length; ++i){ var curves = []; splitToDashedBezier(p1.concat(r[i]), shape.canvasDash, curves); p1 = [r[i][4],r[i][5]]; points.push(curves); } shape._dashedPoints = points; } return r; }; canvas.Ellipse = declare("dojox.gfx.canvas.Ellipse", [canvas.Shape, gs.Ellipse], { // summary: // an ellipse shape (Canvas) setShape: function(){ this.inherited(arguments); this.canvasEllipse = makeEllipse(this); return this; }, setStroke: function(){ this.inherited(arguments); if(!hasNativeDash){ this.canvasEllipse = makeEllipse(this); } return this; }, _renderShape: function(/* Object */ ctx){ var r = this.canvasEllipse, i; ctx.beginPath(); ctx.moveTo.apply(ctx, r[0]); for(i = 1; i < r.length; ++i){ ctx.bezierCurveTo.apply(ctx, r[i]); } ctx.closePath(); }, _renderDashedStroke: function(ctx, apply){ var r = this._dashedPoints; ctx.beginPath(); for(var i = 0; i < r.length; ++i){ var curves = r[i]; for(var j=0;j 0 ? --this._batch : 0; this._makeDirty(); return this; }, _makeDirty: function(){ if(!this._batch){ this.surface.makeDirty(); } }, add: function(shape){ this._makeDirty(); return C.add.apply(this, arguments); }, remove: function(shape, silently){ this._makeDirty(); return C.remove.apply(this, arguments); }, clear: function(){ this._makeDirty(); return C.clear.apply(this, arguments); }, getBoundingBox: C.getBoundingBox, _moveChildToFront: function(shape){ this._makeDirty(); return C._moveChildToFront.apply(this, arguments); }, _moveChildToBack: function(shape){ this._makeDirty(); return C._moveChildToBack.apply(this, arguments); } }; var Creator = { // summary: // Canvas shape creators createObject: function(shapeType, rawShape) { // summary: // creates an instance of the passed shapeType class // shapeType: Function // a class constructor to create an instance of // rawShape: Object // properties to be passed in to the classes "setShape" method // overrideSize: Boolean // set the size explicitly, if true var shape = new shapeType(); shape.surface = this.surface; shape.setShape(rawShape); this.add(shape); return shape; // dojox/gfx/shape.Shape } }; extend(canvas.Group, Container); extend(canvas.Group, gs.Creator); extend(canvas.Group, Creator); extend(canvas.Surface, Container); extend(canvas.Surface, gs.Creator); extend(canvas.Surface, Creator); // no event support -> nothing to fix. canvas.fixTarget = function(event, gfxElement){ // tags: // private return true; }; return canvas; });