1/**
2 * mxJsCanvas
3 *
4 * Open Issues:
5 *
6 * - Canvas has no built-in dash-pattern for strokes
7 *   - Use AS code for straight lines
8 * - Must use proxy for cross domain images
9 * - Use html2canvas for HTML rendering (Replaces complete page with
10 *   canvas currently, needs API call to render elt to canvas)
11 */
12
13/**
14 * Extends mxAbstractCanvas2D
15 */
16function mxJsCanvas(canvas)
17{
18	mxAbstractCanvas2D.call(this);
19
20	this.ctx = canvas.getContext('2d');
21	this.ctx.textBaseline = 'top';
22	this.ctx.fillStyle = 'rgba(255,255,255,0)';
23	this.ctx.strokeStyle = 'rgba(0, 0, 0, 0)';
24
25	//this.ctx.translate(0.5, 0.5);
26
27	this.M_RAD_PER_DEG = Math.PI / 180;
28
29	this.images = this.images == null ? [] : this.images;
30	this.subCanvas = this.subCanvas == null ? [] : this.subCanvas;
31};
32
33/**
34 * Extends mxAbstractCanvas2D
35 */
36mxUtils.extend(mxJsCanvas, mxAbstractCanvas2D);
37
38/**
39 * Variable: ctx
40 *
41 * Holds the current canvas context
42 */
43mxJsCanvas.prototype.ctx = null;
44
45/**
46 * Variable: ctx
47 *
48 * Holds the current canvas context
49 */
50mxJsCanvas.prototype.waitCounter = 0;
51
52/**
53 * Variable: ctx
54 *
55 * Holds the current canvas context
56 */
57mxJsCanvas.prototype.onComplete = null;
58
59/**
60 * Variable: images
61 *
62 * Ordered array of images used in this canvas
63 */
64mxJsCanvas.prototype.images = null;
65
66/**
67 * Variable: subCanvas
68 *
69 * Ordered array of sub canvas elements in this canvas
70 */
71mxJsCanvas.prototype.subCanvas = null;
72
73/**
74 * Variable: canvasIndex
75 *
76 * The current index into the canvas sub-canvas array being processed
77 */
78mxJsCanvas.prototype.canvasIndex = 0;
79
80mxJsCanvas.prototype.hexToRgb = function(hex) {
81    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
82    var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
83    hex = hex.replace(shorthandRegex, function(m, r, g, b) {
84        return r + r + g + g + b + b;
85    });
86
87    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
88    return result ? {
89        r: parseInt(result[1], 16),
90        g: parseInt(result[2], 16),
91        b: parseInt(result[3], 16)
92    } : null;
93};
94
95mxJsCanvas.prototype.incWaitCounter = function()
96{
97	this.waitCounter++;
98};
99
100mxJsCanvas.prototype.decWaitCounter = function()
101{
102	this.waitCounter--;
103
104	if (this.waitCounter == 0 && this.onComplete != null)
105	{
106		this.onComplete();
107		this.onComplete = null;
108	}
109};
110
111mxJsCanvas.prototype.updateFont = function()
112{
113	var style = '';
114
115	if ((this.state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
116	{
117		style += 'bold ';
118	}
119
120	if ((this.state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
121	{
122		style += 'italic ';
123	}
124
125	this.ctx.font = style + this.state.fontSize + 'px ' + this.state.fontFamily;
126};
127
128mxJsCanvas.prototype.save = function()
129{
130	this.states.push(this.state);
131	this.state = mxUtils.clone(this.state);
132	this.ctx.save();
133};
134
135mxJsCanvas.prototype.restore = function()
136{
137	this.state = this.states.pop();
138	this.ctx.restore();
139};
140
141mxJsCanvas.prototype.scale = function(s)
142{
143	this.state.scale *= s;
144	this.state.strokeWidth *= s;
145	this.ctx.scale(s, s);
146};
147
148mxJsCanvas.prototype.translate = function(dx, dy)
149{
150	this.state.dx += dx;
151	this.state.dy += dy;
152	this.ctx.translate(dx, dy);
153};
154
155mxJsCanvas.prototype.rotate = function(theta, flipH, flipV, cx, cy)
156{
157    // This is a special case where the rotation center is scaled so dx/dy,
158    // which are also scaled, must be applied after scaling the center.
159    cx -= this.state.dx;
160    cy -= this.state.dy;
161
162	this.ctx.translate(cx, cy);
163
164	if (flipH || flipV)
165	{
166		var sx = (flipH) ? -1 : 1;
167		var sy = (flipV) ? -1 : 1;
168
169		this.ctx.scale(sx, sy);
170	}
171
172	this.ctx.rotate(theta * this.M_RAD_PER_DEG);
173	this.ctx.translate(-cx, -cy);
174};
175
176mxJsCanvas.prototype.setAlpha = function(alpha)
177{
178	this.state.alpha = alpha;
179	this.ctx.globalAlpha = alpha;
180};
181
182/**
183 * Function: setFillColor
184 *
185 * Sets the current fill color.
186 */
187mxJsCanvas.prototype.setFillColor = function(value)
188{
189	if (value == mxConstants.NONE)
190	{
191		value = null;
192	}
193
194	this.state.fillColor = value;
195	this.state.gradientColor = null;
196	this.ctx.fillStyle = value;
197};
198
199mxJsCanvas.prototype.setGradient = function(color1, color2, x, y, w, h, direction, alpha1, alpha2)
200{
201	var gradient = this.ctx.createLinearGradient(0, y, 0, y + h);
202
203	var s = this.state;
204	s.fillColor = color1;
205	s.fillAlpha = (alpha1 != null) ? alpha1 : 1;
206	s.gradientColor = color2;
207	s.gradientAlpha = (alpha2 != null) ? alpha2 : 1;
208	s.gradientDirection = direction;
209
210	var rgb1 = this.hexToRgb(color1);
211	var rgb2 = this.hexToRgb(color2);
212
213	if (rgb1 != null)
214	{
215		gradient.addColorStop(0, 'rgba(' + rgb1.r + ',' + rgb1.g + ',' + rgb1.b + ',' + s.fillAlpha + ')');
216	}
217
218	if (rgb2 != null)
219	{
220		gradient.addColorStop(1, 'rgba(' + rgb2.r + ',' + rgb2.g + ',' + rgb2.b + ',' + s.gradientAlpha + ')');
221	}
222
223	this.ctx.fillStyle = gradient;
224};
225
226mxJsCanvas.prototype.setStrokeColor = function(value)
227{
228	if (value == null)
229	{
230		// null value ignored
231	}
232	else if (value == mxConstants.NONE)
233	{
234		this.state.strokeColor = null;
235		this.ctx.strokeStyle = 'rgba(0, 0, 0, 0)';
236	}
237	else
238	{
239		this.ctx.strokeStyle = value;
240		this.state.strokeColor = value;
241	}
242};
243
244mxJsCanvas.prototype.setStrokeWidth = function(value)
245{
246	this.ctx.lineWidth = value;
247};
248
249mxJsCanvas.prototype.setDashed = function(value)
250{
251	this.state.dashed = value;
252
253	if (value)
254	{
255		var dashArray = this.state.dashPattern.split(" ");
256
257		for (var i = 0; i < dashArray.length; i++)
258		{
259			dashArray[i] = parseInt(dashArray[i], 10);
260		}
261
262		this.setLineDash(dashArray);
263	}
264	else
265	{
266		this.setLineDash([0]);
267	}
268};
269
270mxJsCanvas.prototype.setLineDash = function(value)
271{
272	try
273	{
274		if (typeof this.ctx.setLineDash === "function")
275		{
276			this.ctx.setLineDash(value);
277		}
278		else
279		{
280			// Line dash not supported IE 10-
281		}
282	}
283	catch (e)
284	{
285		// ignore
286	}
287};
288
289mxJsCanvas.prototype.setDashPattern = function(value)
290{
291	this.state.dashPattern = value;
292
293	if (this.state.dashed)
294	{
295		var dashArray = value.split(" ");
296
297		for (var i = 0; i < dashArray.length; i++)
298		{
299			dashArray[i] = parseInt(dashArray[i], 10);
300		}
301
302		this.ctx.setLineDash(dashArray);
303	}
304};
305
306mxJsCanvas.prototype.setLineCap = function(value)
307{
308	this.ctx.lineCap = value;
309};
310
311mxJsCanvas.prototype.setLineJoin = function(value)
312{
313	this.ctx.lineJoin = value;
314};
315
316mxJsCanvas.prototype.setMiterLimit = function(value)
317{
318	this.ctx.lineJoin = value;
319};
320
321mxJsCanvas.prototype.setFontColor = function(value)
322{
323	this.ctx.fillStyle = value;
324};
325
326mxJsCanvas.prototype.setFontBackgroundColor = function(value)
327{
328	if (value == mxConstants.NONE)
329	{
330		value = null;
331	}
332
333	this.state.fontBackgroundColor = value;
334};
335
336mxJsCanvas.prototype.setFontBorderColor = function(value)
337{
338	if (value == mxConstants.NONE)
339	{
340		value = null;
341	}
342
343	this.state.fontBorderColor = value;
344};
345
346mxJsCanvas.prototype.setFontSize = function(value)
347{
348	this.state.fontSize = value;
349};
350
351mxJsCanvas.prototype.setFontFamily = function(value)
352{
353	this.state.fontFamily = value;
354};
355
356mxJsCanvas.prototype.setFontStyle = function(value)
357{
358	this.state.fontStyle = value;
359};
360
361/**
362* Function: setShadow
363*
364* Enables or disables and configures the current shadow.
365*/
366mxJsCanvas.prototype.setShadow = function(enabled)
367{
368	this.state.shadow = enabled;
369
370	if (enabled)
371	{
372		this.setShadowOffset(this.state.shadowDx, this.state.shadowDy);
373		this.setShadowAlpha(this.state.shadowAlpha);
374	}
375	else
376	{
377		this.ctx.shadowColor = 'transparent';
378		this.ctx.shadowBlur = 0;
379		this.ctx.shadowOffsetX = 0;
380		this.ctx.shadowOffsetY = 0;
381	}
382};
383
384/**
385* Function: setShadowColor
386*
387* Enables or disables and configures the current shadow.
388*/
389mxJsCanvas.prototype.setShadowColor = function(value)
390{
391	if (value == null || value == mxConstants.NONE)
392	{
393		value = null;
394		this.ctx.shadowColor = 'transparent';
395	}
396
397	this.state.shadowColor = value;
398
399	if (this.state.shadow && value != null)
400	{
401		var alpha = (this.state.shadowAlpha != null) ? this.state.shadowAlpha : 1;
402		var rgb = this.hexToRgb(value);
403
404		this.ctx.shadowColor = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + alpha + ')';
405	}
406};
407
408/**
409* Function: setShadowAlpha
410*
411* Enables or disables and configures the current shadow.
412*/
413mxJsCanvas.prototype.setShadowAlpha = function(value)
414{
415	this.state.shadowAlpha = value;
416	this.setShadowColor(this.state.shadowColor);
417};
418
419/**
420* Function: setShadowOffset
421*
422* Enables or disables and configures the current shadow.
423*/
424
425mxJsCanvas.prototype.setShadowOffset = function(dx, dy)
426{
427	this.state.shadowDx = dx;
428	this.state.shadowDy = dy;
429
430	if (this.state.shadow)
431	{
432		this.ctx.shadowOffsetX = dx;
433		this.ctx.shadowOffsetY = dy;
434	}
435};
436
437mxJsCanvas.prototype.moveTo = function(x, y)
438{
439	this.ctx.moveTo(x, y);
440	this.lastMoveX = x;
441	this.lastMoveY = y;
442};
443
444mxJsCanvas.prototype.lineTo = function(x, y)
445{
446	this.ctx.lineTo(x, y);
447	this.lastMoveX = x;
448	this.lastMoveY = y;
449};
450
451mxJsCanvas.prototype.quadTo = function(x1, y1, x2, y2)
452{
453	this.ctx.quadraticCurveTo(x1, y1, x2, y2);
454	this.lastMoveX = x2;
455	this.lastMoveY = y2;
456};
457
458mxJsCanvas.prototype.arcTo = function(rx, ry, angle, largeArcFlag, sweepFlag, x, y)
459{
460	var curves = mxUtils.arcToCurves(this.lastMoveX, this.lastMoveY, rx, ry, angle, largeArcFlag, sweepFlag, x, y);
461
462	if (curves != null)
463	{
464		for (var i = 0; i < curves.length; i += 6)
465		{
466			this.curveTo(curves[i], curves[i + 1], curves[i + 2],
467				curves[i + 3], curves[i + 4], curves[i + 5]);
468		}
469	}
470};
471
472mxJsCanvas.prototype.curveTo = function(x1, y1, x2, y2, x3, y3)
473{
474	this.ctx.bezierCurveTo(x1, y1, x2, y2 , x3, y3);
475	this.lastMoveX = x3;
476	this.lastMoveY = y3;
477};
478
479mxJsCanvas.prototype.rect = function(x, y, w, h)
480{
481	// TODO: Check if fillRect/strokeRect is faster
482	this.begin();
483	this.moveTo(x, y);
484	this.lineTo(x + w, y);
485	this.lineTo(x + w, y + h);
486	this.lineTo(x, y + h);
487	this.close();
488};
489
490mxJsCanvas.prototype.roundrect = function(x, y, w, h, dx, dy)
491{
492	this.begin();
493	this.moveTo(x + dx, y);
494	this.lineTo(x + w - dx, y);
495	this.quadTo(x + w, y, x + w, y + dy);
496	this.lineTo(x + w, y + h - dy);
497	this.quadTo(x + w, y + h, x + w - dx, y + h);
498	this.lineTo(x + dx, y + h);
499	this.quadTo(x, y + h, x, y + h - dy);
500	this.lineTo(x, y + dy);
501	this.quadTo(x, y, x + dx, y);
502};
503
504mxJsCanvas.prototype.ellipse = function(x, y, w, h)
505{
506	this.ctx.save();
507	this.ctx.translate((x + w / 2), (y + h / 2));
508	this.ctx.scale(w / 2, h / 2);
509	this.ctx.beginPath();
510	this.ctx.arc(0, 0, 1, 0, 2 * Math.PI, false);
511	this.ctx.restore();
512};
513
514//Redirect can be implemented via a hook
515mxJsCanvas.prototype.rewriteImageSource = function(src)
516{
517	if (src.substring(0, 7) == 'http://' || src.substring(0, 8) == 'https://')
518	{
519		src = '/proxy?url=' + encodeURIComponent(src);
520	}
521
522	return src;
523};
524
525mxJsCanvas.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV)
526{
527	var scale = this.state.scale;
528
529//	x = this.state.tx + x / scale;
530//	y = this.state.ty + y / scale;
531//	w /= scale;
532//	h /= scale;
533
534	src = this.rewriteImageSource(src);
535	var image = this.images[src];
536
537	function drawImage(ctx, image, x, y, w, h)
538	{
539		ctx.save();
540
541		if (aspect)
542		{
543			var iw = image.width;
544			var ih = image.height;
545
546			var s = Math.min(w / iw, h / ih);
547			var x0 = (w - iw * s) / 2;
548			var y0 = (h - ih * s) / 2;
549
550			x += x0;
551			y += y0;
552			w = iw * s;
553			h = ih * s;
554		}
555
556		var s = this.state.scale;
557
558		if (flipH)
559		{
560			ctx.translate(2 * x + w, 0);
561			ctx.scale(-1, 1);
562		}
563
564		if (flipV)
565		{
566			ctx.translate(0, 2 * y + h);
567			ctx.scale(1, -1);
568		}
569
570		ctx.drawImage(image, x, y, w, h);
571		ctx.restore();
572	};
573
574	if (image != null && image.height > 0 && image.width > 0)
575	{
576		drawImage.call(this, this.ctx, image, x, y, w, h);
577	}
578	else
579	{
580		// TODO flag error that image wasn't obtaining in canvas preprocessing
581	}
582};
583
584mxJsCanvas.prototype.begin = function()
585{
586	this.ctx.beginPath();
587};
588
589mxJsCanvas.prototype.close = function()
590{
591	this.ctx.closePath();
592};
593
594mxJsCanvas.prototype.fill = function()
595{
596	this.ctx.fill();
597};
598
599mxJsCanvas.prototype.stroke = function()
600{
601	this.ctx.stroke();
602};
603
604mxJsCanvas.prototype.fillAndStroke = function()
605{
606	// If you fill then stroke, the shadow of the stroke appears over the fill
607	// So stroke, fill, disable shadow, stroke, restore previous shadow
608	if (!this.state.shadow)
609	{
610		this.ctx.fill();
611		this.ctx.stroke();
612	}
613	else
614	{
615		this.ctx.stroke();
616		this.ctx.fill();
617
618		var shadowColor = this.ctx.shadowColor;
619		var shadowOffsetX = this.ctx.shadowOffsetX;
620		var shadowOffsetY = this.ctx.shadowOffsetY;
621
622		this.ctx.shadowColor = 'transparent';
623		this.ctx.shadowOffsetX = 0;
624		this.ctx.shadowOffsetY = 0;
625
626		this.ctx.stroke();
627
628		this.ctx.shadowColor = shadowColor;
629		this.ctx.shadowOffsetX = shadowOffsetX;
630		this.ctx.shadowOffsetY = shadowOffsetY;
631	}
632};
633
634mxJsCanvas.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation)
635{
636	if (str == null || str.length == 0)
637	{
638		return;
639	}
640
641	var sc = this.state.scale;
642	w *= sc;
643	h *= sc;
644
645	if (rotation != 0)
646	{
647		this.ctx.translate(Math.round(x), Math.round(y));
648		this.ctx.rotate(rotation * Math.PI / 180);
649		this.ctx.translate(Math.round(-x), Math.round(-y));
650	}
651
652	if (format == 'html')
653	{
654		var subCanvas = this.subCanvas[this.canvasIndex++];
655		var cavHeight = subCanvas.height;
656		var cavWidth = subCanvas.width;
657
658		switch (valign)
659		{
660		 case mxConstants.ALIGN_MIDDLE:
661			 y -= cavHeight / 2 /sc;
662			 break;
663		 case mxConstants.ALIGN_BOTTOM:
664			 y -= cavHeight / sc;
665			 break;
666		}
667
668		switch (align)
669		{
670		 case mxConstants.ALIGN_CENTER:
671			 x -= cavWidth / 2 / sc;
672			 break;
673		 case mxConstants.ALIGN_RIGHT:
674			 x -= cavWidth / sc;
675			 break;
676		}
677
678		this.ctx.save();
679
680		if (this.state.fontBackgroundColor != null || this.state.fontBorderColor != null)
681		{
682
683			if (this.state.fontBackgroundColor != null)
684			{
685				this.ctx.fillStyle = this.state.fontBackgroundColor;
686				this.ctx.fillRect(Math.round(x) - 0.5, Math.round(y) - 0.5, Math.round(subCanvas.width / sc), Math.round(subCanvas.height / sc));
687			}
688			if (this.state.fontBorderColor != null)
689			{
690				this.ctx.strokeStyle = this.state.fontBorderColor;
691				this.ctx.lineWidth = 1;
692				this.ctx.strokeRect(Math.round(x) - 0.5, Math.round(y) - 0.5, Math.round(subCanvas.width / sc), Math.round(subCanvas.height / sc));
693			}
694		}
695
696		//if (sc < 1)
697		//{
698			this.ctx.scale(1/sc, 1/sc);
699		//}
700
701    	this.ctx.drawImage(subCanvas, Math.round(x * sc) ,Math.round(y * sc));
702
703		this.ctx.restore();
704
705	}
706	else
707	{
708		this.ctx.save();
709		this.updateFont();
710
711		var div = document.createElement("div");
712	    div.innerHTML = str;
713	    div.style.position = 'absolute';
714	    div.style.top  = '-9999px';
715	    div.style.left = '-9999px';
716	    div.style.fontFamily = this.state.fontFamily;
717	    div.style.fontWeight = 'bold';
718	    div.style.fontSize = this.state.fontSize + 'pt';
719	    document.body.appendChild(div);
720	    var measuredFont = [div.offsetWidth, div.offsetHeight];
721	    document.body.removeChild(div);
722
723	    var lines = str.split('\n');
724		var lineHeight = measuredFont[1];
725
726		this.ctx.textBaseline = 'top';
727		var backgroundY = y;
728
729		switch (valign)
730		{
731		 case mxConstants.ALIGN_MIDDLE:
732			 this.ctx.textBaseline = 'middle';
733			 y -= (lines.length-1) * lineHeight / 2;
734			 backgroundY = y - this.state.fontSize / 2;
735			 break;
736		 case mxConstants.ALIGN_BOTTOM:
737			 this.ctx.textBaseline = 'alphabetic';
738			 y -= lineHeight * (lines.length-1);
739			 backgroundY = y - this.state.fontSize;
740			 break;
741		}
742
743	    var lineWidth = [];
744	    var lineX = [];
745
746		for (var i = 0; i < lines.length; i++)
747		{
748			lineX[i] = x;
749			lineWidth[i] = this.ctx.measureText(lines[i]).width;
750
751			if (align != null && align != mxConstants.ALIGN_LEFT)
752			{
753				lineX[i] -= lineWidth[i];
754
755				if (align == mxConstants.ALIGN_CENTER)
756				{
757					lineX[i] += lineWidth[i] / 2;
758				}
759			}
760		}
761
762		if (this.state.fontBackgroundColor != null || this.state.fontBorderColor != null)
763		{
764			var startMostX = lineX[0];
765			var maxWidth = lineWidth[0];
766
767			for (var i = 1; i < lines.length; i++)
768			{
769				startMostX = Math.min(startMostX, lineX[i]);
770				maxWidth = Math.max(maxWidth, lineWidth[i]);
771			}
772
773			this.ctx.save();
774
775			startMostX = Math.round(startMostX) - 0.5;
776			backgroundY = Math.round(backgroundY) - 0.5;
777
778			if (this.state.fontBackgroundColor != null)
779			{
780				this.ctx.fillStyle = this.state.fontBackgroundColor;
781				this.ctx.fillRect(startMostX, backgroundY, maxWidth, this.state.fontSize * mxConstants.LINE_HEIGHT * lines.length);
782			}
783			if (this.state.fontBorderColor != null)
784			{
785				this.ctx.strokeStyle = this.state.fontBorderColor;
786				this.ctx.lineWidth = 1;
787				this.ctx.strokeRect(startMostX, backgroundY, maxWidth, this.state.fontSize * mxConstants.LINE_HEIGHT * lines.length);
788			}
789
790			this.ctx.restore();
791		}
792
793		for (var i = 0; i < lines.length; i++)
794		{
795			this.ctx.fillText(lines[i], lineX[i], y);
796			y += this.state.fontSize * mxConstants.LINE_HEIGHT;
797		}
798
799		this.ctx.restore();
800	}
801};
802
803mxJsCanvas.prototype.getCanvas = function()
804{
805	return canvas;
806};
807
808mxJsCanvas.prototype.finish = function(handler)
809{
810	// TODO: Check if waitCounter updates need a monitor. Question is
811	// if image load-handler can be executed in parallel leading to
812	// race conditions when updating the "shared" waitCounter.
813	if (this.waitCounter == 0)
814	{
815		handler();
816	}
817	else
818	{
819		this.onComplete = handler;
820	}
821};