1/// @brief The SketchCanvas class accepts a canvas object to draw on.
2/// @param canvas The canvas to draw the figure to.
3/// @param options An initialization parameters table that contains following items:
4///                editmode: The canvas is used for editing when true.
5///                debug: A function with one argument to output debug string.
6///
7/// Make sure to invoke this class's constructor with "new" prepended
8/// and keep the returned object to some variable.
9///
10/// It has following event handlers that can be assigned as this object's method.
11///
12/// function onLocalChange();
13/// This event is invoked when the canvas saves its contents into localStorage of the
14/// browser.  Use this event to update list of locally saved figures.
15///
16/// function onUpdateServerList(list);
17/// This event is invoked when the object requests the server to refresh figure list
18/// and receives response.
19///
20/// function onUpdateData(data);
21/// This event is invoked when the contents of figure data is modified.
22import jsyaml from './js-yaml.mjs';
23import i18n from 'https://deno.land/x/i18next@v23.11.4/index.js';
24import { resources } from './translation.js';
25
26export function SketchCanvas(canvas, options){
27'use strict';
28var editmode = options && options.editmode;
29var scale = options && options.scale ? options.scale : 1;
30
31// Obtain the browser's preferred language.
32var currentLanguage = (window.navigator.language || window.navigator.userLanguage || window.navigator.userLanguage);
33currentLanguage = currentLanguage.substr(0, 2);
34
35i18n.init({lng: currentLanguage, fallbackLng: 'en', resources: resources, getAsync: false});
36
37var dobjs; // Drawing objects
38var dhistory; // Drawing object history (for undoing)
39var selectobj = [];
40
41var handleSize = 4;
42var gridEnable = false;
43var gridSize = 8;
44var toolButtonInterval = 36;
45
46// Called at the end of the constructor
47function onload(){
48
49  // Check existence of canvas element and treating not compatible browsers
50  if ( ! canvas || ! canvas.getContext ) {
51    return false;
52  }
53
54	// Ignore mouse events if it's non-edit mode.
55	if(editmode){
56		canvas.onclick = mouseLeftClick;
57		canvas.onmousedown = mouseDown;
58		canvas.onmouseup = mouseUp;
59		canvas.onmousemove = mouseMove;
60		canvas.onmouseout = mouseleave;
61		canvas.setAttribute("tabindex", 0); // Make sure the canvas can have a key focus
62		canvas.onkeydown = keyDown;
63	}
64	else
65		canvas.onclick = viewModeClick;
66
67  // 2D context
68  ctx = canvas.getContext('2d');
69	// Set a placeholder function to ignore setLineDash method for browsers that don't support it.
70	// The method is not compatible with many browsers, but we don't care if it's not supported
71	// because it's only used for selected object designation.
72	if(!ctx.setLineDash) {
73		ctx.setLineDash = function(){};
74	}
75
76	var aw = 20; // Arrow width
77
78	// Previous toolbar page button
79	buttons.push(new Button(mx0, my0, mx0 + aw, my0 + mh0, function(){
80		ctx.fillStyle = 'rgb(127,127,127)';
81		ctx.beginPath();
82		ctx.moveTo(mx0, my0 + mh0 / 2);
83		ctx.lineTo(mx0 + aw, my0);
84		ctx.lineTo(mx0 + aw, my0 + mh0);
85		ctx.closePath();
86		ctx.fill();
87	}, function(){
88		debug("clicked left");
89		toolbarPage = Math.max(toolbarPage - 1, 0);
90		toolbar = toolbars[toolbarPage];
91		cur_tool = toolbar[0];
92		draw();
93	}));
94
95	// Next toolbar page button
96	buttons.push(new Button(mx0 + mw0 - aw, my0, mx0 + mw0, my0 + mh0, function(){
97		ctx.beginPath();
98		ctx.moveTo(mx0 + mw0, my0 + mh0 / 2);
99		ctx.lineTo(mx0 + mw0 - aw, my0);
100		ctx.lineTo(mx0 + mw0 - aw, my0 + mh0);
101		ctx.closePath();
102		ctx.fill();
103	}, function(){
104		debug("clicked right");
105		toolbarPage = Math.min(toolbarPage + 1, toolbars.length-1);
106		toolbar = toolbars[toolbarPage];
107		cur_tool = toolbar[0];
108		draw();
109	}));
110
111  resizeCanvas();
112  draw();
113
114  // Draw objects
115  dobjs = [];
116
117  // And the history of operations
118  dhistory = [];
119}
120
121var datadir = "data";
122
123function draw() {
124	if(!editmode)
125		return;
126	// Draw a rectangle
127	ctx.beginPath();
128	ctx.strokeStyle = 'rgb(192, 192, 77)'; // yellow
129		ctx.font = i18n.t("14px 'Courier'");
130	ctx.strokeText('SketchCanvas Editor v0.1.6', 420, 10);
131	ctx.rect(x0, y0, w0, h0);
132	ctx.rect(x1, y1, w1, h1);
133	ctx.closePath();
134	ctx.stroke();
135
136	// Background for the page text
137	ctx.fillStyle = 'rgb(255,255,255)';
138	ctx.fillRect(x0 + 1, y0 + 1, x1 - 2, y0 + h0 - 2);
139
140	// Text indicating current toolbar page
141	ctx.fillStyle = 'rgb(0,0,0)';
142	var text = (toolbarPage + 1) + "/" + (toolbars.length);
143	var width = ctx.measureText(text).width;
144	ctx.fillText(text, mx0 + mw0 / 2 - width / 2, my0 + mh0 / 2);
145
146	// menu
147	drawMenu();
148	drawTBox();
149	drawButtons();
150	drawCBox(cur_col);
151	drawHBox(cur_thin);
152}
153
154// draw coord(for Debug)
155function drawPos(x, y) {
156  ctx.strokeText('X='+x+' Y='+y, x, y);
157}
158
159// Menu
160function drawMenu() {
161	for(var i=0;i<menus.length;i++) {
162		ctx.fillStyle = 'rgb(100, 200, 100)'; // green
163		ctx.fillRect(mx1+(i+0)*(mw1+10), my0, mw1, mh0);
164		//ctx.strokeStyle = 'rgb(50, 192, 177)'; // cyan
165		ctx.strokeStyle = 'rgb(250, 250, 250)'; // white
166		ctx.strokeText(menus[i].text, mx1+10+(i+0)*(mw1+10), my0+20);
167	}
168}
169
170/// Returns bounding box of a toolbar button.
171function getTBox(i){
172	return {minx: mx0, miny: my0 + toolButtonInterval + toolButtonInterval*i,
173		maxx: mx0 + mw0, maxy: my0 + toolButtonInterval + toolButtonInterval*i+mh0};
174}
175
176// Tool Box
177function drawTBox() {
178	ctx.fillStyle = 'rgb(255,255,255)';
179	ctx.fillRect(x0 + 1, my0 + mh0 + 1, x1 - 2, my0 + h0 - 2);
180
181	for(var i=0;i<toolbar.length;i++) {
182		if (cur_tool === toolbar[i])
183			ctx.fillStyle = 'rgb(255, 80, 77)'; // red
184		else
185			ctx.fillStyle = 'rgb(192, 80, 77)'; // red
186		var r = getTBox(i);
187		ctx.fillRect(r.minx, r.miny, r.maxx - r.minx, r.maxy - r.miny);
188		ctx.strokeStyle = 'rgb(250, 250, 250)'; // white
189		drawParts(toolbar[i], mx0+10, my0 + toolButtonInterval + 10 + (mh0+8)*i);
190	}
191}
192
193function drawButtons(){
194	for(var i = 0; i < buttons.length; i++){
195		if("ondraw" in buttons[i])
196			buttons[i].ondraw();
197	}
198}
199
200// Color Palette
201function drawCBox(no) {
202	for(var i=0;i<5;i++) {
203		ctx.beginPath();
204		ctx.fillStyle = colstr[i];
205		var x = mx2+(mw2+10)*i;
206		ctx.fillRect(x, my0, mw2, mh0);
207		ctx.stroke();
208		if (4==i) {		// border line if white
209			ctx.beginPath();
210			//ctx.lineWidth = 1;
211			ctx.strokeStyle = gray;
212			ctx.rect(x, my0, mw2, mh0);
213			ctx.stroke();
214		}
215		if (no == i+31) {
216			ctx.beginPath();
217			ctx.strokeStyle = gray;
218			ctx.strokeText('○', x+9, my0+20);
219		}
220	}
221}
222
223// Thin Box
224function drawHBox(no) {
225	for(var i=0;i<3;i++) {
226		ctx.beginPath();
227		if (no == i+41)
228			ctx.fillStyle = 'rgb(255, 80, 77)'; // red
229		else
230			ctx.fillStyle = 'rgb(192, 80, 77)'; // red
231		ctx.fillRect(mx3+(mw2+10)*i, my0, mw2, mh0);
232		ctx.beginPath();
233		ctx.strokeStyle = white;
234		ctx.lineWidth = i + 1;
235		ctx.moveTo(mx3+(mw2+10)*i+10, my0+15);
236		ctx.lineTo(mx3+(mw2+10)*i+25, my0+15);
237		ctx.stroke();
238	}
239	ctx.lineWidth = 1;
240}
241
242// draw toolbox
243function drawParts(tool, x, y) {
244	if(tool.drawTool)
245		tool.drawTool(x, y);
246	else
247		ctx.strokeText(i18n.t('Unimplemented'), x, y);
248}
249
250// Returns bounding box for a drawing object.
251function objBounds(obj, rawvalue){
252	var ret = obj.getBoundingRect(rawvalue);
253	if(!rawvalue){
254		ret.minx += offset.x;
255		ret.maxx += offset.x;
256		ret.miny += offset.y;
257		ret.maxy += offset.y;
258	}
259	return ret;
260}
261
262// Expand given rectangle by an offset
263function expandRect(r, offset){
264	return {minx: r.minx - offset, miny : r.miny - offset,
265		maxx: r.maxx + offset, maxy: r.maxy + offset};
266}
267
268// Check if a point intersects with a rectangle
269function hitRect(r, x, y){
270	return r.minx < x && x < r.maxx && r.miny < y && y < r.maxy;
271}
272
273// Check if two rectangles intersect each other
274function intersectRect(r1, r2){
275	return r1.minx < r2.maxx && r2.minx < r1.maxx && r1.miny < r2.maxy && r2.miny < r1.maxy;
276}
277
278function mouseLeftClick(e) {
279	if (3 == e.which) mouseRightClick(e);
280	else {
281		var clrect = canvas.getBoundingClientRect();
282		var mx = e.clientX - clrect.left;
283		var my = e.clientY - clrect.top;
284
285		for(var i = 0; i < buttons.length; i++){
286			if(hitRect(buttons[i], mx, my)){
287				buttons[i].onclick();
288				return;
289			}
290		}
291
292		if(choiceTBox(mx, my))
293			return;
294
295		var menuno = checkMenu(mx, my);
296		debug(menuno);
297		if (menuno < 0) {		// draw area
298			if(mx < offset.x || my < offset.y)
299				return;
300			if(cur_tool.name === "delete"){ // delete
301				for(var i = 0; i < dobjs.length; i++){
302					// For the time being, we use the bounding boxes of the objects
303					// to determine the clicked object.  It may be surprising
304					// when a diagonal line gets deleted by clicking on seemingly
305					// empty space, but we could fix it in the future.
306					var bounds = expandRect(objBounds(dobjs[i]), 10);
307					if(hitRect(bounds, mx, my)){
308						// If any dobj hits, save the current state to the undo buffer and delete the object.
309						dhistory.push(cloneObject(dobjs));
310						// Delete from selected object list, too.
311						for(var j = 0; j < selectobj.length;){
312							if(selectobj[j] === dobjs[i])
313								selectobj.splice(j, 1);
314							else
315								j++;
316						}
317						dobjs.splice(i, 1);
318						redraw(dobjs);
319						updateDrawData();
320						return;
321					}
322				}
323				return;
324			}
325			else if(cur_tool.name !== "select" && cur_tool.name !== "pathedit")
326				cur_tool.appendPoint(mx, my);
327		}
328		else if (menuno < 10) {
329			drawMenu();
330			menus[menuno].onclick();
331		}
332		else if (menuno <= 40) {
333			drawCBox(menuno);
334			cur_col = colnames[menuno-31];
335			if(0 < selectobj.length){
336				dhistory.push(cloneObject(dobjs));
337				for(var i = 0; i < selectobj.length; i++)
338					selectobj[i].color = cur_col;
339				updateDrawData();
340				redraw(dobjs);
341			}
342		}
343		else {
344			drawHBox(menuno);
345			cur_thin = menuno - 40;
346			if(0 < selectobj.length){
347				dhistory.push(cloneObject(dobjs));
348				for(var i = 0; i < selectobj.length; i++)
349					selectobj[i].width = cur_thin;
350				updateDrawData();
351				redraw(dobjs);
352			}
353		}
354		//if (1 == menuno) debug('x='+arr[0].x + 'y='+arr[0].y);
355	}
356}
357
358function mouseRightClick(e) {
359	//drawPos(e.pageX, e.pageY);
360	//cood = { x:e.pageX, y:e.pageY }
361	//arr[idx] = cood;
362	//idx++;
363	debug('idx='+idx);
364}
365
366var movebase = [0,0];
367var dragstart = [0,0];
368var dragend = [0,0];
369var moving = false;
370var sizing = null; // Reference to object being resized. Null if no resizing is in progress.
371var sizedir = 0;
372var pointMoving = null;
373var pointMovingIdx = 0;
374var pointMovingCP = ""; ///< Control point for moving, "": vertex, "c": first control point, "d": second control point
375var boxselecting = false;
376function pathEditMouseDown(e){
377	var clrect = canvas.getBoundingClientRect();
378	var mx = e.clientX - clrect.left;
379	var my = e.clientY - clrect.top;
380
381	// Prioritize path editing over shape selection.
382
383	// Check to see if we are dragging any of control points.
384	for(var n = 0; n < selectobj.length; n++){
385		// Do not try to change shape of non-sizeable object.
386		if(!selectobj[n].isSizeable())
387			continue;
388		var pts = selectobj[n].points;
389		// Grab a control point uder the mouse cursor to move it.
390		for(var i = 0; i < pts.length; i++){
391			var p = pts[i];
392
393			// Function to check if we should start dragging one of vertices or
394			// control points.  Written as a local function to clarify logic.
395			var checkVertexHandle = function(self){
396				if(!hitRect(pointHandle(p.x, p.y), mx - offset.x, my - offset.y))
397					return false;
398				// Check control points for only the "path" shapes.
399				// Pressing Ctrl key before dragging the handle pulls out the
400				// control point, even if the vertex didn't have one.
401				if(selectobj[n].tool === "path" && e.ctrlKey){
402					if(0 < i && !("dx" in p))
403						pointMovingCP = "d";
404					else if(i+1 < pts.length && !("cx" in pts[i+1])){
405						pointMovingCP = "c";
406						pointMoving = selectobj[n];
407						pointMovingIdx = i+1;
408						return true;
409					}
410					else
411						return false;
412				}
413				else
414					pointMovingCP = "";
415				pointMoving = selectobj[n];
416				pointMovingIdx = i;
417
418				// Remember clicked vertex for showing control handles
419				var needsRedraw = self.selectPointShape !== selectobj[n] || self.selectPointIdx !== pointMovingIdx;
420				self.selectPointShape = selectobj[n];
421				self.selectPointIdx = pointMovingIdx;
422				if(needsRedraw) // Redraw only if the selected
423					redraw(dobjs);
424
425				dhistory.push(cloneObject(dobjs));
426				return true;
427			}
428
429			// do{ ... }while(false) statement could do a similar trick, but we prefer
430			// local functions for the ease of debugging and more concise expression.
431			if(checkVertexHandle(this))
432				return;
433
434			// Check for existing control points
435			// Select only the control points around the selected vertex
436			if(this.selectPointShape !== selectobj[n] || i < this.selectPointIdx || this.selectPointIdx + 1 < i)
437				continue;
438			if("cx" in p && "cy" in p && hitRect(pointHandle(p.cx, p.cy), mx - offset.x, my - offset.y)){
439				pointMoving = selectobj[n];
440				pointMovingIdx = i;
441				pointMovingCP = "c";
442				dhistory.push(cloneObject(dobjs));
443				return;
444			}
445			if("dx" in p && "dy" in p && hitRect(pointHandle(p.dx, p.dy), mx - offset.x, my - offset.y)){
446				pointMoving = selectobj[n];
447				pointMovingIdx = i;
448				pointMovingCP = "d";
449				dhistory.push(cloneObject(dobjs));
450				return;
451			}
452		}
453	}
454
455	selectCommonMouseDown(mx, my);
456}
457
458/// A common method to select and pathedit tools.
459/// The pathedit tool can select objects, not to mention the select tool.
460function selectCommonMouseDown(mx, my){
461
462	var menuno = checkMenu(mx, my);
463	if(0 <= menuno) // If we are clicking on a menu button, ignore this event
464		return null;
465
466	// First, check if we clicked on one of already selected shapes.
467	// if so, do not clear the selection and get ready to manipulate already selected shapes.
468	for(var i = 0; i < selectobj.length; i++){
469		var bounds = expandRect(objBounds(selectobj[i]), 10);
470		if(hitRect(bounds, mx, my))
471			return true;
472	}
473
474	// Second, check if we clicked other shapes.
475	// If so, clear the selection and select the new shape.
476	for(var i = 0; i < dobjs.length; i++){
477		// For the time being, we use the bounding boxes of the objects
478		// to determine the clicked object.  It may be surprising
479		// when a diagonal line gets deleted by clicking on seemingly
480		// empty space, but we could fix it in the future.
481		var bounds = expandRect(objBounds(dobjs[i]), 10);
482		if(hitRect(bounds, mx, my)){
483			// If we haven't selected an object but clicked on an object, select it.
484			// Single click always selects a single shape.
485			selectobj = [dobjs[i]];
486			// Forget point selection in pathedit tool if shape selection is changed.
487			toolmap.pathedit.selectPointIdx = -1;
488			redraw(dobjs);
489			return true;
490		}
491	}
492
493	return false;
494}
495
496function selectMouseDown(e){
497	var clrect = canvas.getBoundingClientRect();
498	var mx = e.clientX - clrect.left;
499	var my = e.clientY - clrect.top;
500
501	// Prevent the select tool from clearing selection even if the mouse is on the
502	// toolbar or the menu bar.
503	if(mx < offset.x || my < offset.y)
504		return;
505
506	// Enter sizing, moving or box-selecting mode only with select tool.
507	// The pathedit tool should not bother interfering with these modes.
508
509	// Check to see if we are dragging any of scaling handles.
510	for(var n = 0; n < selectobj.length; n++){
511		// Do not try to change size of non-sizeable object.
512		if(!selectobj[n].isSizeable())
513			continue;
514		var bounds = objBounds(selectobj[n]);
515		// Do not enter sizing mode if the object is point sized.
516		if(1 <= Math.abs(bounds.maxx - bounds.minx) && 1 <= Math.abs(bounds.maxy - bounds.miny)){
517			for(var i = 0; i < 8; i++){
518				if(hitRect(getHandleRect(bounds, i), mx, my)){
519					sizedir = i;
520					sizing = selectobj[n];
521					dhistory.push(cloneObject(dobjs));
522					return;
523				}
524			}
525		}
526	}
527
528	var pointOnSelection = selectCommonMouseDown(mx, my);
529
530	// If we're starting dragging on a selected object, enter moving mode.
531	if(pointOnSelection){
532		var mx = gridEnable ? Math.round(mx / gridSize) * gridSize : mx;
533		var my = gridEnable ? Math.round(my / gridSize) * gridSize : my;
534		movebase = [mx, my];
535		moving = true;
536		dhistory.push(cloneObject(dobjs));
537	}
538	else{
539		// If no object is selected and dragging is started, it's box selection mode.
540		boxselecting = true;
541		selectobj = [];
542		dragstart = [mx, my];
543	}
544}
545
546function mouseDown(e){
547	if(cur_tool)
548		cur_tool.mouseDown(e);
549}
550
551function mouseUp(e){
552	if(cur_tool)
553		cur_tool.mouseUp(e);
554}
555
556function selectMouseMove(e){
557	var clrect = canvas.getBoundingClientRect();
558	var mx = (gridEnable ? Math.round(e.clientX / gridSize) * gridSize : e.clientX) - clrect.left;
559	var my = (gridEnable ? Math.round(e.clientY / gridSize) * gridSize : e.clientY) - clrect.top;
560
561	if(0 < selectobj.length){
562		if(moving){
563			var dx = mx - movebase[0];
564			var dy = my - movebase[1];
565			for(var n = 0; n < selectobj.length; n++){
566				var obj = selectobj[n];
567				for(var i = 0; i < obj.points.length; i++){
568					var pt = obj.points[i];
569					pt.x += dx;
570					pt.y += dy;
571					// Move control points, too
572					if("cx" in pt && "cy" in pt)
573						pt.cx += dx, pt.cy += dy;
574					if("dx" in pt && "dy" in pt)
575						pt.dx += dx, pt.dy += dy;
576				}
577			}
578			movebase = [mx, my];
579			redraw(dobjs);
580		}
581		else if(sizing){
582			/* Definition of handle index and position
583				0 --- 1 --- 2
584				|           |
585				7           3
586				|           |
587				6 --- 5 --- 4
588			*/
589			mx -= offset.x;
590			my -= offset.y;
591			var bounds = objBounds(sizing, true);
592			var ux = [-1,0,1,1,1,0,-1,-1][sizedir];
593			var uy = [-1,-1,-1,0,1,1,1,0][sizedir];
594			var xscale = ux === 0 ? 1 : (ux === 1 ? mx - bounds.minx : bounds.maxx - mx) / (bounds.maxx - bounds.minx);
595			var yscale = uy === 0 ? 1 : (uy === 1 ? my - bounds.miny : bounds.maxy - my) / (bounds.maxy - bounds.miny);
596			var obj = sizing;
597			for(var i = 0; i < obj.points.length; i++){
598				var pt = obj.points[i];
599				if(ux !== 0 && xscale !== 0){
600					// Scale control points, too
601					var props = ["x", "cx", "dx"];
602					for(var j = 0; j < props.length; j++){
603						var prop = props[j];
604						if(!(prop in pt))
605							continue;
606						pt[prop] = ux === 1 ?
607							(pt[prop] - bounds.minx) * xscale + bounds.minx :
608							(pt[prop] - bounds.maxx) * xscale + bounds.maxx;
609					}
610				}
611				if(uy !== 0 && yscale !== 0){
612					// Scale control points, too
613					var props = ["y", "cy", "dy"];
614					for(var j = 0; j < props.length; j++){
615						var prop = props[j];
616						if(!(prop in pt))
617							continue;
618						pt[prop] = uy === 1 ?
619							(pt[prop] - bounds.miny) * yscale + bounds.miny :
620							(pt[prop] - bounds.maxy) * yscale + bounds.maxy;
621					}
622				}
623			}
624			// Invert handle selection when the handle is dragged to the other side to enable mirror scaling.
625			if(ux !== 0 && xscale < 0)
626				sizedir = [2,1,0,7,6,5,4,3][sizedir];
627			if(uy !== 0 && yscale < 0)
628				sizedir = [6,5,4,3,2,1,0,7][sizedir];
629			redraw(dobjs);
630		}
631	}
632
633	// We could use e.buttons to check if it's supported by all the browsers,
634	// but it seems not much trusty.
635	if(!moving && !sizing && boxselecting){
636		dragend = [mx, my];
637		var box = {
638			minx: Math.min(dragstart[0], mx),
639			maxx: Math.max(dragstart[0], mx),
640			miny: Math.min(dragstart[1], my),
641			maxy: Math.max(dragstart[1], my),
642		}
643		selectobj = [];
644		// Select all intersecting objects with the dragged box.
645		for(var i = 0; i < dobjs.length; i++){
646			var bounds = expandRect(objBounds(dobjs[i]), 10);
647			if(intersectRect(bounds, box))
648				selectobj.push(dobjs[i]);
649		}
650		redraw(dobjs);
651	}
652}
653
654function pathEditMouseMove(e){
655	var clrect = canvas.getBoundingClientRect();
656	var mx = (gridEnable ? Math.round(e.clientX / gridSize) * gridSize : e.clientX) - clrect.left;
657	var my = (gridEnable ? Math.round(e.clientY / gridSize) * gridSize : e.clientY) - clrect.top;
658
659	if(0 < selectobj.length){
660		if(pointMoving){
661			mx -= offset.x;
662			my -= offset.y;
663
664			// Relatively move a control point if it exists
665			var relmove = function(name, idx, dx, dy){
666				if(pointMovingIdx + idx < 0 || pointMoving.points.length <= pointMovingIdx + idx)
667					return;
668				var p1 = pointMoving.points[pointMovingIdx + idx];
669				if((name + "x") in p1) p1[name + "x"] += dx;
670				if((name + "y") in p1) p1[name + "y"] += dy;
671			}
672
673			if(pointMovingCP === ""){
674				// When moving a vertex, Bezier control points associated with that vertex
675				// should move as well.  We remember delta of position change and apply it
676				// to the control points later.
677				var dx = mx - pointMoving.points[pointMovingIdx].x;
678				var dy = my - pointMoving.points[pointMovingIdx].y;
679				pointMoving.points[pointMovingIdx].x = mx;
680				pointMoving.points[pointMovingIdx].y = my;
681				// If it's the last vertex, there's no control point toward the next vertex.
682				if(pointMovingIdx + 1 < pointMoving.points.length)
683					relmove("c", 1, dx, dy);
684				relmove("d", 0, dx, dy);
685			}
686			else{
687				var curpoint = pointMoving.points[pointMovingIdx];
688				curpoint[pointMovingCP + "x"] = mx;
689				curpoint[pointMovingCP + "y"] = my;
690
691				// Move the control point in the opposite side of the vertex so that the
692				// curve seems smooth.  A line between a cubic Bezier spline curve's control point
693				// and its vertex is tangent to the curve, so keeping the control points parallel
694				// makes the connected Bezier curves look smooth.  This could be done by hand-editing,
695				// but it's very cumbersome to do everytime even single control point of spline is
696				// edited.
697				// Note that SVG's "S" or "s" path command can reflect the Bezier control point to
698				// achieve similar effect, but the commands cannot specify distinct lengths for each
699				// control points from the central vertex.  So we keep using "C" command.
700				// Sometimes the user wants to make a non-smooth path, so this functionality can be
701				// disabled by pressing ALT key while dragging.
702				if(!e.altKey && pointMovingCP === "d" && pointMovingIdx + 1 < pointMoving.points.length){
703					var nextpoint = pointMoving.points[pointMovingIdx + 1];
704					if("cx" in nextpoint && "cy" in nextpoint){
705						var dx = nextpoint.cx - curpoint.x;
706						var dy = nextpoint.cy - curpoint.y;
707						var len = Math.sqrt(dx * dx + dy * dy);
708						var angle = Math.atan2(dy, dx);
709						var newangle = Math.atan2(curpoint.y - curpoint.dy, curpoint.x - curpoint.dx);
710						nextpoint.cx = curpoint.x + len * Math.cos(newangle);
711						nextpoint.cy = curpoint.y + len * Math.sin(newangle);
712					}
713				}
714				else if(!e.altKey && pointMovingCP === "c" && 0 <= pointMovingIdx - 1){
715					var prevpoint = pointMoving.points[pointMovingIdx - 1];
716					if("dx" in prevpoint && "dy" in prevpoint){
717						var dx = prevpoint.dx - prevpoint.x;
718						var dy = prevpoint.dy - prevpoint.y;
719						var len = Math.sqrt(dx * dx + dy * dy);
720						var angle = Math.atan2(dy, dx);
721						var newangle = Math.atan2(prevpoint.y - curpoint.cy, prevpoint.x - curpoint.cx);
722						prevpoint.dx = prevpoint.x + len * Math.cos(newangle);
723						prevpoint.dy = prevpoint.y + len * Math.sin(newangle);
724					}
725				}
726			}
727			redraw(dobjs);
728		}
729	}
730}
731
732function mouseMove(e){
733	if(cur_tool.mouseMove)
734		cur_tool.mouseMove(e);
735}
736
737function mouseleave(e){
738	moving = false;
739	sizing = null;
740	boxselecting = false;
741}
742
743/// Mouse click event handler for view mode.
744/// Some shapes respond to the event in view mode.
745function viewModeClick(e){
746	var clrect = canvas.getBoundingClientRect();
747	var mx = (gridEnable ? Math.round(e.clientX / gridSize) * gridSize : e.clientX) - clrect.left;
748	var my = (gridEnable ? Math.round(e.clientY / gridSize) * gridSize : e.clientY) - clrect.top;
749	var m = {x: mx / scale, y: my / scale}; /// Convert to the logical coordinate system before hit test
750
751	// Check to see if we are dragging any of scaling handles.
752	for(var i = 0; i < dobjs.length; i++){
753		if(!("viewModeClick" in dobjs[i]))
754			continue;
755		var bounds = expandRect(objBounds(dobjs[i]), 10);
756		if(hitRect(bounds, m.x, m.y)){
757			if(dobjs[i].viewModeClick(e))
758				return true;
759		}
760	}
761
762}
763
764function keyDown(e){
765	e = e || window.event;
766	if(cur_tool.keyDown)
767		cur_tool.keyDown(e);
768}
769
770/// Delete given shapes from the canvas.
771/// Also deletes from selection list if matched.
772/// Passing selectobj variable itself as the argument clears the whole selection.
773function deleteShapes(shapes){
774	for (var i = 0; i < shapes.length; i++) {
775		var s = shapes[i];
776		for (var j = 0; j < dobjs.length; j++) {
777			if(dobjs[j] === s){
778				dobjs.splice(j, 1);
779				break;
780			}
781		}
782		if(shapes !== selectobj){
783			for (var j = 0; j < selectobj.length; j++) {
784				if(selectobj[j] === s){
785					selectobj.splice(j, 1);
786					break;
787				}
788			}
789		}
790	}
791	if(shapes === selectobj)
792		selectobj = [];
793}
794
795/// Constrain coordinate values if the grid is enabled.
796function constrainCoord(coord){
797	if(gridEnable)
798		return { x: Math.round(coord.x / gridSize) * gridSize, y: Math.round(coord.y / gridSize) * gridSize };
799	else
800		return { x: coord.x, y: coord.y };
801}
802
803/// Convert canvas coordinates to the source coordinates.
804/// These coordinates can differ when offset is not zero.
805function canvasToSrc(coord){
806	return { x: (coord.x - offset.x) / scale, y: (coord.y - offset.y) / scale };
807}
808
809// A local function to set font size with the global scaling factor in mind.
810function setFont(baseSize) {
811	ctx.font = baseSize + "px 'Noto Sans Japanese', sans-serif";
812}
813
814/// Draw a shape on the canvas.
815function drawCanvas(obj) {
816	var tool = toolmap[obj.tool];
817	var numPoints = tool.points;
818
819	tool.setColor(coltable[obj.color]);
820	tool.setWidth(obj.width);
821	if(numPoints <= obj.points.length)
822		if(tool.draw(obj))
823			return;
824}
825
826function getHandleRect(bounds, i){
827	var x, y;
828	switch(i){
829	case 0: x = bounds.minx, y = bounds.miny; break;
830	case 1: x = (bounds.minx+bounds.maxx)/2, y = bounds.miny; break;
831	case 2: x = bounds.maxx, y = bounds.miny; break;
832	case 3: x = bounds.maxx, y = (bounds.miny+bounds.maxy)/2; break;
833	case 4: x = bounds.maxx, y = bounds.maxy; break;
834	case 5: x = (bounds.minx+bounds.maxx)/2, y = bounds.maxy; break;
835	case 6: x = bounds.minx, y = bounds.maxy; break;
836	case 7: x = bounds.minx, y = (bounds.miny+bounds.maxy)/2; break;
837	default: return;
838	}
839	return pointHandle(x, y);
840}
841
842/// Returns a small rectangle surrounding the given point which is suitable for mouse-picking.
843function pointHandle(x, y){
844	return {minx: x - handleSize, miny: y - handleSize, maxx: x + handleSize, maxy: y + handleSize};
845}
846
847// Resize the canvas so that it fits the contents.
848// Note that the canvas contents are also cleared, so we might need to redraw everything.
849function resizeCanvas(){
850	if(!editmode){
851		// Resize the canvas so that the figure fits the size of canvas.
852		// It's only done in view mode because we should show the toolbar and the menu bar
853		// in edit mode.
854		canvas.width = metaObj.size[0] * scale;
855		canvas.height = metaObj.size[1] * scale;
856		x1 = 0;
857		y1 = 0;
858		offset = {x:0, y:0};
859	}
860	else{
861		canvas.width = 1024;
862		canvas.height = 640;
863		x1 = 90;
864		y1 = 50;
865		offset = {x:x1, y:y1};
866	}
867}
868
869/// Draw a handle rectangle or a circle around a vertex or a control point
870function drawHandle(x, y, color, circle){
871	var r = pointHandle(x, y);
872	ctx.fillStyle = color;
873	if(!circle)
874		ctx.fillRect(r.minx, r.miny, r.maxx - r.minx, r.maxy-r.miny);
875	ctx.beginPath();
876	ctx.strokeStyle = '#000';
877	if(circle){
878		ctx.arc((r.minx + r.maxx) / 2., (r.miny + r.maxy) / 2, handleSize, 0, 2 * Math.PI, false);
879		ctx.fill();
880	}
881	else
882		ctx.rect(r.minx, r.miny, r.maxx - r.minx, r.maxy-r.miny);
883	ctx.stroke();
884}
885
886// redraw
887function redraw(pt) {
888
889	clearCanvas();
890
891	// Clip the drawing region when in edit mode in order to prevent shapes from
892	// contaminate the menu bar and the toolbar.
893	if(editmode){
894		ctx.save();
895		ctx.beginPath();
896		ctx.rect(x1,y1, w1, h1);
897		ctx.clip();
898	}
899
900	if(gridEnable){
901		ctx.fillStyle = "#000";
902		for(var ix = Math.ceil(x1 / gridSize); ix < (x1 + w1) / gridSize; ix++){
903			for(var iy = Math.ceil(y1 / gridSize); iy < (y1 + h1) / gridSize; iy++){
904				ctx.fillRect(ix * gridSize, iy * gridSize, 1, 1);
905			}
906		}
907	}
908
909	// Translate and scale the coordinates by applying matrices before invoking drawCanvas().
910	ctx.save();
911	ctx.translate(offset.x, offset.y);
912	ctx.scale(scale, scale);
913	for (var i=0; i<pt.length; i++)
914		drawCanvas(pt[i]);
915
916	if(cur_shape)
917		drawCanvas(cur_shape);
918
919	ctx.restore();
920
921	if(boxselecting){
922		ctx.beginPath();
923		ctx.lineWidth = 1;
924		ctx.strokeStyle = '#000';
925		ctx.setLineDash([5]);
926		ctx.rect(dragstart[0], dragstart[1], dragend[0] - dragstart[0], dragend[1] - dragstart[1]);
927		ctx.stroke();
928		ctx.setLineDash([]);
929	}
930
931	for(var n = 0; n < selectobj.length; n++)
932		cur_tool.selectDraw(selectobj[n]);
933
934	if(editmode)
935		ctx.restore();
936}
937
938// When pathedit tool is selected, draw handles on the shape's control points
939// and the path that connecting control points, but not the
940// bounding boxes and scaling handles, which could be annoying
941// because they're nothing to do with path editing.
942function pathEditSelectDraw(shape){
943	var pts = shape.points;
944	ctx.beginPath();
945	ctx.lineWidth = 1;
946	ctx.strokeStyle = '#000';
947	if(toolmap[shape.tool].isArc){
948		ctx.setLineDash([5]);
949		ctx.moveTo(pts[0].x + offset.x, pts[0].y + offset.y);
950		for(var i = 1; i < pts.length; i++)
951			ctx.lineTo(pts[i].x + offset.x, pts[i].y + offset.y);
952		ctx.stroke();
953		ctx.setLineDash([]);
954	}
955
956	// Draws dashed line that connects a control point and its associated vertex
957	function drawGuidingLine(pt0, name){
958		ctx.setLineDash([5]);
959		ctx.beginPath();
960		ctx.moveTo(pt0.x + offset.x, pt0.y + offset.y);
961		ctx.lineTo(pt[name + "x"] + offset.x, pt[name + "y"] + offset.y);
962		ctx.stroke();
963		ctx.setLineDash([]);
964	}
965
966	for(var i = 0; i < pts.length; i++){
967		var pt = pts[i];
968		// Indicate currently selected vertex with blue handle, otherwise gray
969		drawHandle(pt.x + offset.x, pt.y + offset.y,
970			shape === this.selectPointShape && i === this.selectPointIdx ? '#7f7fff' : '#7f7f7f');
971
972		// Draw handles for control point only around the selected vertex
973		if(shape !== this.selectPointShape || i < this.selectPointIdx || this.selectPointIdx + 1 < i)
974			continue;
975
976		// Handles and guiding lines for the control points
977		if(0 < i && "cx" in pt && "cy" in pt){
978			drawHandle(pt.cx + offset.x, pt.cy + offset.y, '#ff7f7f', true);
979			drawGuidingLine(pts[i-1], "c");
980		}
981		if("dx" in pt && "dy" in pt){
982			drawHandle(pt.dx + offset.x, pt.dy + offset.y, '#ff7f7f', true);
983			drawGuidingLine(pt, "d");
984		}
985	}
986}
987
988// ==================== Shape class definition ================================= //
989function Shape(){
990	this.tool = "line";
991	this.color = "black";
992	this.width = 1;
993	this.points = [];
994}
995//inherit(Shape, Object);
996
997Shape.prototype.serialize = function(){
998	function set_default(t,k,v,def){
999		if(v !== def)
1000			t[k] = v;
1001	}
1002
1003	// send parts to server
1004	var dat = "";
1005	for (var i=0; i<this.points.length; i++){
1006		if(i !== 0) dat += ":";
1007		// Prepend control points if this vertex has them.
1008		if("cx" in this.points[i] && "cy" in this.points[i])
1009			dat += this.points[i].cx+","+this.points[i].cy+",";
1010		if("dx" in this.points[i] && "dy" in this.points[i])
1011			dat += this.points[i].dx+","+this.points[i].dy+",";
1012		dat += this.points[i].x+","+this.points[i].y;
1013	}
1014	var alldat = {
1015		type: this.tool,
1016		points: dat
1017	};
1018	// Values with defaults needs not assigned a value when saved.
1019	// This will save space if the drawn element properties use many default values.
1020	set_default(alldat, "color", this.color, "black");
1021	set_default(alldat, "width", this.width, 1);
1022	return alldat;
1023};
1024
1025Shape.prototype.isSizeable = function(){
1026	return true;
1027}
1028
1029Shape.prototype.deserialize = function(obj){
1030	this.color = obj.color || "black";
1031	this.width = obj.width || 1;
1032	if("points" in obj){
1033		var pt1 = obj.points.split(":");
1034		var arr = [];
1035		for(var j = 0; j < pt1.length; j++){
1036			var pt2 = pt1[j].split(",");
1037			var pt = {};
1038			if(6 <= pt2.length){
1039				pt.cx = parseFloat(pt2[0]);
1040				pt.cy = parseFloat(pt2[1]);
1041			}
1042			if(4 <= pt2.length){
1043				pt.dx = parseFloat(pt2[pt2.length-4]);
1044				pt.dy = parseFloat(pt2[pt2.length-3]);
1045			}
1046			pt.x = parseFloat(pt2[pt2.length-2]);
1047			pt.y = parseFloat(pt2[pt2.length-1]);
1048			arr.push(pt);
1049		}
1050		this.points = arr;
1051	}
1052};
1053
1054Shape.prototype.getBoundingRect = function(){
1055	// Get bounding box of the object
1056	var maxx, maxy, minx, miny;
1057	for(var j = 0; j < this.points.length; j++){
1058		var x = this.points[j].x;
1059		if(maxx === undefined || maxx < x)
1060			maxx = x;
1061		if(minx === undefined || x < minx)
1062			minx = x;
1063		var y = this.points[j].y;
1064		if(maxy === undefined || maxy < y)
1065			maxy = y;
1066		if(miny === undefined || y < miny)
1067			miny = y;
1068	}
1069	return {minx: minx, miny: miny, maxx: maxx, maxy: maxy};
1070};
1071// ==================== Shape class definition end ================================= //
1072
1073// ==================== PointShape class definition ================================= //
1074function PointShape(){
1075	Shape.call(this);
1076}
1077inherit(PointShape, Shape);
1078
1079PointShape.prototype.isSizeable = function(){
1080	return false;
1081}
1082
1083PointShape.prototype.getBoundingRect = function(){
1084	var height = 20;
1085	var width = 20;
1086	return {minx: this.points[0].x, miny: this.points[0].y,
1087		maxx: this.points[0].x + width, maxy: this.points[0].y + height};
1088}
1089// ==================== PointOject class definition end ================================= //
1090
1091// ==================== TextShape class definition ================================= //
1092function TextShape(){
1093	Shape.call(this);
1094	this.text = "";
1095	this.link = "";
1096}
1097inherit(TextShape, Shape);
1098
1099TextShape.prototype.serialize = function(){
1100	var alldat = Shape.prototype.serialize.call(this);
1101	alldat.text = this.text;
1102	if(this.link) alldat.link = this.link; // Do not serialize blank URL
1103	return alldat;
1104}
1105
1106TextShape.prototype.deserialize = function(obj){
1107	Shape.prototype.deserialize.call(this, obj);
1108	if (undefined !== obj.text) this.text = obj.text;
1109	if(undefined !== obj.link) this.link = obj.link;
1110};
1111
1112TextShape.prototype.isSizeable = function(){
1113	return false;
1114}
1115
1116TextShape.prototype.getBoundingRect = function(){
1117	var height = this.width === 1 ? 14 : this.width === 2 ? 16 : 20;
1118	var oldfont = ctx.font;
1119	ctx.font = setFont(height);
1120	var width = ctx.measureText(this.text).width;
1121	ctx.font = oldfont;
1122	return {minx: this.points[0].x, miny: this.points[0].y - height,
1123		maxx: this.points[0].x + width, maxy: this.points[0].y};
1124}
1125
1126/// Click event in view mode
1127TextShape.prototype.viewModeClick = function(e){
1128	if("link" in this && this.link){
1129		location.href = this.link;
1130		return true;
1131	}
1132	return false;
1133}
1134// ==================== TextShape class definition end ================================= //
1135
1136// ==================== PathShape class definition ================================= //
1137function PathShape(){
1138	Shape.call(this);
1139}
1140inherit(PathShape, Shape);
1141
1142/// Custom serializer for SVG-compatible path data
1143PathShape.prototype.serialize = function(){
1144	var alldat = Shape.prototype.serialize.call(this);
1145	var src = "";
1146	for (var i=0; i<this.points.length; i++){
1147		var pt = this.points[i];
1148		if(i !== 0 && ("cx" in pt && "cy" in pt || "dx" in pt && "dy" in pt)){
1149			src += "C"; // Bezier curve command. 'S' command is not yet supported.
1150			// Prepend control points if this vertex has them.
1151			if("cx" in pt && "cy" in pt)
1152				src += pt.cx+","+pt.cy+" ";
1153			else
1154				src += this.points[i-1].x+","+this.points[i-1].y+" ";
1155			if("dx" in pt && "dy" in pt)
1156				src += pt.dx+","+pt.dy+" ";
1157			else
1158				src += pt.x+","+pt.y+" ";
1159		}
1160		else if(i === 0)
1161			src += "M"; // Moveto command
1162		else
1163			src += "L"; // Lineto command
1164		src += pt.x+","+pt.y;
1165	}
1166	alldat.d = src;
1167	// Set point data source to src and forget about old field
1168	delete alldat.points;
1169	if("arrow" in this)
1170		alldat.arrow = set2seq(this.arrow);
1171	return alldat;
1172}
1173
1174/// Custom deserializer for SVG-compatible path data
1175PathShape.prototype.deserialize = function(obj){
1176	Shape.prototype.deserialize.call(this, obj);
1177	if("arrow" in obj)
1178		this.arrow = seq2set(obj.arrow);
1179	if(!("d" in obj))
1180		return;
1181	var arr = [];
1182	var src = obj.d;
1183	var pt2 = [];
1184	var last = 0;
1185
1186	function process(){
1187		if(k <= last)
1188			return;
1189		var ssrc = src.slice(last+1, k);
1190		pt2 = ssrc.split(/[, \t]/);
1191		var pt = {};
1192		if(6 <= pt2.length){
1193			pt.cx = parseFloat(pt2[0]);
1194			pt.cy = parseFloat(pt2[1]);
1195		}
1196		if(4 <= pt2.length){
1197			pt.dx = parseFloat(pt2[pt2.length-4]);
1198			pt.dy = parseFloat(pt2[pt2.length-3]);
1199		}
1200		pt.x = parseFloat(pt2[pt2.length-2]);
1201		pt.y = parseFloat(pt2[pt2.length-1]);
1202		arr.push(pt);
1203		last = k;
1204	}
1205
1206	for(var k = 0; k < src.length; k++){
1207		if("MCLS".indexOf(src.charAt(k)) >= 0)
1208			process();
1209	}
1210	process();
1211
1212	this.points = arr;
1213};
1214// ==================== PathShape class definition end ================================= //
1215
1216
1217function serialize(dobjs){
1218	var ret = [metaObj];
1219	for(var i = 0; i < dobjs.length; i++)
1220		ret.push(dobjs[i].serialize());
1221	return ret;
1222}
1223
1224function deserialize(dat){
1225	// Reset the metaObj before deserialization
1226	metaObj = cloneObject(defaultMetaObj);
1227	var ret = [];
1228	for (var i=0; i<dat.length; i++) {
1229		var obj = dat[i];
1230		if(obj.type === 'meta'){
1231			metaObj = obj;
1232			continue;
1233		}
1234		if(!(obj.type in toolmap))
1235			continue;
1236		var robj = new toolmap[obj.type].objctor();
1237		robj.tool = obj.type;
1238		robj.deserialize(obj);
1239		ret.push(robj);
1240	}
1241	return ret;
1242}
1243
1244this.loadData = function(value){
1245	try{
1246		dobjs = deserialize(jsyaml.load(value));
1247		selectobj = []; // Clear the selection explicitly
1248		resizeCanvas();
1249		draw();
1250		redraw(dobjs);
1251	} catch(e){
1252		console.log(e);
1253	}
1254}
1255
1256this.saveAsImage = function(img){
1257	var reset = editmode;
1258	if(editmode){
1259		editmode = false;
1260		resizeCanvas();
1261		redraw(dobjs);
1262	}
1263	img.src = canvas.toDataURL();
1264	if(reset){
1265		editmode = true;
1266		resizeCanvas();
1267		draw();
1268		redraw(dobjs);
1269	}
1270}
1271
1272/// @brief Loads a data from local storage of the browser
1273/// @param name The name of the sketch in the local storage to load.
1274this.loadLocal = function(name){
1275	try{
1276		var origData = localStorage.getItem("canvasDrawData");
1277		if(origData === null)
1278			return;
1279		var selData = jsyaml.load(origData);
1280		dobjs = deserialize(jsyaml.load(selData[name]));
1281		selectobj = []; // Clear the selection explicitly
1282		updateDrawData();
1283		redraw(dobjs);
1284	} catch(e){
1285		debug(e);
1286	}
1287}
1288
1289// Downloads the file list from the server.
1290function downloadList(){
1291	// Asynchronous request for getting figure data in the server.
1292	var xmlHttp = createXMLHttpRequest();
1293	if(xmlHttp){
1294		// The event handler is assigned here because xmlHttp is a free variable
1295		// implicitly passed to the anonymous function without polluting the
1296		// global namespace.
1297		xmlHttp.onreadystatechange = function(){
1298			if(xmlHttp.readyState !== 4 || xmlHttp.status !== 200)
1299				return;
1300			try{
1301				var selData = xmlHttp.responseText;
1302				if(!selData)
1303					return;
1304				if(self.onUpdateServerList)
1305					self.onUpdateServerList(selData.split("\n"));
1306			}
1307			catch(e){
1308				console.log(e);
1309			}
1310		};
1311		xmlHttp.open("GET", "list.php", true);
1312		xmlHttp.send();
1313	}
1314}
1315
1316this.listServer = downloadList;
1317
1318this.requestServerFile = function(item, hash){
1319	// Asynchronous request for getting figure data in the server.
1320	var xmlHttp = createXMLHttpRequest();
1321	if(xmlHttp){
1322		var request;
1323		// The event handler is assigned here because xmlHttp is a free variable
1324		// implicitly passed to the anonymous function without polluting the
1325		// global namespace.
1326		xmlHttp.onreadystatechange = function(){
1327			if(xmlHttp.readyState !== 4 || xmlHttp.status !== 200)
1328				return;
1329			try{
1330				var selData = xmlHttp.responseText;
1331				if(!selData)
1332					return;
1333				if(hash){
1334					var firstLine = selData.split("\n", 1)[0];
1335					if(firstLine !== "succeeded")
1336						throw "Failed to obtain revision " + selData;
1337					selData = selData.substr(selData.indexOf("\n")+1);
1338				}
1339				dobjs = deserialize(jsyaml.load(selData));
1340				selectobj = [];
1341				updateDrawData();
1342				resizeCanvas();
1343				draw();
1344				redraw(dobjs);
1345			}
1346			catch(e){
1347				debug(e);
1348			}
1349		};
1350		if(!hash)
1351			request = datadir + "/" + encodeURI(item);
1352		else
1353			request = "history.php?fname=" + encodeURI(item) + "&hash=" + encodeURI(hash);
1354		xmlHttp.open("GET", request, true);
1355		xmlHttp.send();
1356	}
1357}
1358
1359this.requestServerFileHistory = function(item, responseCallback){
1360	var historyQuery = createXMLHttpRequest();
1361	if(historyQuery){
1362		historyQuery.onreadystatechange = function(){
1363			if(historyQuery.readyState !== 4 || historyQuery.status !== 200)
1364				return;
1365			try{
1366				var res = historyQuery.responseText;
1367				if(!res)
1368					return;
1369				var historyData = res.split("\n");
1370				if(historyData[0] !== "succeeded")
1371					return;
1372				historyData = historyData.splice(1);
1373				responseCallback(historyData);
1374			}
1375			catch(e){
1376				console.log(e);
1377			}
1378		};
1379		historyQuery.open("GET", "history.php?fname=" + encodeURI(item), true);
1380		historyQuery.send();
1381	}
1382}
1383
1384// clear canvas
1385function clearCanvas() {
1386	if(editmode){
1387		// Fill outside of valid figure area defined by metaObj.size with gray color.
1388		ctx.fillStyle = '#7f7f7f';
1389		ctx.fillRect(x1,y1, w1, h1);
1390	}
1391	ctx.fillStyle = white;
1392	ctx.fillRect(x1, y1, Math.min(w1, metaObj.size[0]), Math.min(h1, metaObj.size[1]));
1393}
1394
1395// Sets the size of the canvas
1396function setSize(sx, sy){
1397	metaObj.size[0] = sx;
1398	metaObj.size[1] = sy;
1399	updateDrawData();
1400	redraw(dobjs);
1401}
1402
1403// check all menu
1404function checkMenu(x, y) {
1405	var no = choiceMenu(x, y);
1406	if (no >= 0) return no;
1407	no = choiceCBox(x, y);
1408	if (no > 0) return no;
1409	no = choiceHBox(x, y);
1410	if (no > 0) return no;
1411
1412	if (x > w0 || y > h0) no = -1;
1413	return no;
1414}
1415
1416function choiceMenu(x, y) {
1417	// menu
1418	if (y < my0 || y > my0+mh0) return -1;
1419	for(var i=0;i<menus.length;i++) {
1420		if (x >= mx1+(mw1+10)*i && x <= mx1+mw0+(mw1+10)*i) return i;
1421	}
1422
1423	return -1;
1424}
1425
1426/// Selects a toolbar button. Returns true if the coordinates hit a button.
1427function choiceTBox(x, y) {
1428	// ToolBox
1429	if (x < mx0 || x > mx0+mw0) return false;
1430	for(var i=0;i<toolbar.length;i++) {
1431		var r = getTBox(i);
1432		if(hitRect(r, x, y)){
1433			cur_tool = toolbar[i];
1434			drawTBox();
1435			cur_shape = null;
1436			redraw(dobjs);
1437			return true;
1438		}
1439	}
1440
1441	return false;
1442}
1443
1444	// Color Parett
1445function choiceCBox(x, y) {
1446	if (y < my0 || y > my0+mh0) return -1;
1447	for(var i=0;i<5;i++) {
1448		if (x >= mx2+(mw2+10)*i && x <= mx2+mw2+(mw2+10)*i) return i+31;
1449	}
1450
1451	return -1;
1452}
1453	// Thin Box
1454function choiceHBox(x, y) {
1455	if (y < my0 || y > my0+mh0) return -1;
1456	for(var i=0;i<3;i++) {
1457		if (x >= mx3+(mw2+10)*i && x <= mx3+mw2+(mw2+10)*i) return i+41;
1458	}
1459
1460	return -1;
1461}
1462
1463/// @brief Save a sketch data to a local storage entry with name
1464/// @param name Name of the sketch which can be used in loadLocal() to restore
1465this.saveLocal = function(name){
1466	if(typeof(Storage) !== "undefined"){
1467		var str = localStorage.getItem("canvasDrawData");
1468		var origData = str === null ? {} : jsyaml.load(str);
1469		var newEntry = !(name in origData);
1470		origData[name] = jsyaml.dump(serialize(dobjs));
1471		localStorage.setItem("canvasDrawData", jsyaml.dump(origData));
1472		// If the named sketch didn't exist, fire up the event of local storage change.
1473		if(newEntry && ('onLocalChange' in this) && this.onLocalChange)
1474			this.onLocalChange();
1475		return true;
1476	}
1477	return false;
1478};
1479
1480/// @brief Returns list of sketches saved in local storage
1481/// @returns An array with sketch names in each element
1482this.listLocal = function() {
1483
1484	if(typeof(Storage) !== "undefined"){
1485		var str = localStorage.getItem("canvasDrawData");
1486		var origData = str === null ? {} : jsyaml.load(str);
1487
1488		// Enumerating keys array would be simpler if we could use Object.keys(),
1489		// but the method won't work for IE6, 7, 8.
1490		// If we'd repeat this procedure, we may be able to use a shim in:
1491		// https://github/com/es-shims/es5-shim/blob/v2.0.5/es5-shim.js
1492		var keys = [];
1493		for(var name in origData)
1494			keys.push(name);
1495
1496		return keys;
1497	}
1498
1499}
1500
1501/// Custom inheritance function that prevents the super class's constructor
1502/// from being called on inehritance.
1503/// Also assigns constructor property of the subclass properly.
1504/// Inheriting is closely related to cloneObject()
1505function inherit(subclass,base){
1506	// If the browser or ECMAScript supports Object.create, use it
1507	// (but don't remember to redirect constructor pointer to subclass)
1508	if(Object.create){
1509		subclass.prototype = Object.create(base.prototype);
1510	}
1511	else{
1512		var sub = function(){};
1513		sub.prototype = base.prototype;
1514		subclass.prototype = new sub;
1515	}
1516	subclass.prototype.constructor = subclass;
1517}
1518
1519/// Copy all properties in src to target, retaining target's existing properties
1520/// that have different names from src's.
1521function mixin(target, src){
1522	for(var k in src){
1523		target[k] = src[k];
1524	}
1525}
1526
1527// Create a deep clone of objects
1528function cloneObject(obj) {
1529	if (obj === null || typeof obj !== 'object') {
1530		return obj;
1531	}
1532
1533	// give temp the original obj's constructor
1534	// this 'new' is important in case obj is user-defined class which was created by
1535	// 'new ClassName()', no matter inherited or not.
1536	// I wonder why Array and Object (built-in classes) don't need new operator.
1537	var temp = new obj.constructor();
1538	for (var key in obj) {
1539		temp[key] = cloneObject(obj[key]);
1540	}
1541
1542	return temp;
1543}
1544
1545function updateDrawData(){
1546	try{
1547		var text = jsyaml.dump(serialize(dobjs), {flowLevel: 2});
1548		if(('onUpdateData' in self) && self.onUpdateData)
1549			self.onUpdateData(text);
1550	} catch(e){
1551		console.log(e);
1552	}
1553}
1554
1555// clear data
1556function ajaxclear() {
1557	dobjs = [];
1558	selectobj = [];
1559	updateDrawData();
1560	clearCanvas();
1561}
1562
1563// undo
1564function ajaxundo() {
1565	if(dhistory.length < 1)
1566		return;
1567	dobjs = dhistory[dhistory.length-1];
1568	dhistory.pop();
1569	selectobj = [];
1570	updateDrawData();
1571	redraw(dobjs);
1572}
1573
1574var createXMLHttpRequest = this.createXMLHttpRequest;
1575
1576/// @brief Posts a sketch data to the server
1577/// @param fname The file name of the added sketch.
1578/// @param target The target URL for posting.
1579/// @param requestDelete If true, it will post delete request instead of new data.
1580this.postData = function(fname, target, requestDelete){
1581	var data = jsyaml.dump(serialize(dobjs), {flowLevel: 2});
1582	// Asynchronous request for getting figure data in the server.
1583	var xmlHttp = createXMLHttpRequest();
1584	if(xmlHttp){
1585		// The event handler is assigned here because xmlHttp is a free variable
1586		// implicitly passed to the anonymous function without polluting the
1587		// global namespace.
1588		xmlHttp.onreadystatechange = function(){
1589			if(xmlHttp.readyState !== 4 || xmlHttp.status !== 200)
1590				return;
1591			debug(xmlHttp.responseText);
1592			downloadList();
1593		};
1594		xmlHttp.open("POST", target, true);
1595		xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
1596		var request = "fname=" + encodeURI(fname);
1597		if(requestDelete)
1598			request += "&action=delete";
1599		else
1600			request += "&drawdata=" + encodeURI(data);
1601		xmlHttp.send(request);
1602	}
1603};
1604
1605/// @brief Posts a request to server to pull from remote (respective to the server)
1606/// @param remoteName The remote name (defined in the server)
1607this.pull = function(remoteName){
1608	// Asynchronous request for pulling.
1609	var xmlHttp = createXMLHttpRequest();
1610	if(xmlHttp){
1611		// The event handler is assigned here because xmlHttp is a free variable
1612		// implicitly passed to the anonymous function without polluting the
1613		// global namespace.
1614		xmlHttp.onreadystatechange = function(){
1615			if(xmlHttp.readyState !== 4 || xmlHttp.status !== 200)
1616				return;
1617			debug(xmlHttp.responseText);
1618			downloadList();
1619		};
1620		xmlHttp.open("GET", "pull.php?remote=" + encodeURI(remoteName), true);
1621		xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
1622		xmlHttp.send();
1623	}
1624}
1625
1626/// @brief Posts a request to server to push to remote (respective to the server)
1627/// @param remoteName The remote name (defined in the server)
1628this.push = function(remoteName){
1629	// Asynchronous request for pulling.
1630	var xmlHttp = createXMLHttpRequest();
1631	if(xmlHttp){
1632		// The event handler is assigned here because xmlHttp is a free variable
1633		// implicitly passed to the anonymous function without polluting the
1634		// global namespace.
1635		xmlHttp.onreadystatechange = function(){
1636			if(xmlHttp.readyState !== 4 || xmlHttp.status !== 200)
1637				return;
1638			debug(xmlHttp.responseText);
1639			downloadList();
1640		};
1641		xmlHttp.open("GET", "push.php?remote=" + encodeURI(remoteName), true);
1642		xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
1643		xmlHttp.send();
1644	}
1645}
1646
1647//------------------------ debug ------------------------
1648function debug(msg) {
1649	if(options && options.debug)
1650		options.debug(msg);
1651}
1652
1653// ==================== Button class definition ================================= //
1654/// An abstract button class.
1655/// SVG or canvas graphics libraries like EaselJS would do this sort of thing
1656/// much better, but I don't feel like to port to it at once.
1657function Button(minx, miny, maxx, maxy, ondraw, onclick){
1658	this.minx = minx;
1659	this.miny = miny;
1660	this.maxx = maxx;
1661	this.maxy = maxy;
1662	this.ondraw = ondraw;
1663	this.onclick = onclick;
1664}
1665
1666// ==================== Menu class definition ================================= //
1667function MenuItem(text, onclick){
1668	this.text = i18n.t(text);
1669	this.onclick = onclick;
1670}
1671
1672// init --------------------------------------------------
1673//window.captureEvents(Event.click);
1674//onclick=mouseLeftClick;
1675var ctx;
1676var menus = [
1677	new MenuItem("Grid", function(){
1678		gridEnable = !gridEnable;
1679		redraw(dobjs);
1680	}),
1681	new MenuItem("Grid+", function(){
1682		if(gridSize < 32)
1683			gridSize *= 2;
1684		if(gridEnable)
1685			redraw(dobjs);
1686	}),
1687	new MenuItem("Grid-", function(){
1688		if(4 < gridSize)
1689			gridSize /= 2;
1690		if(gridEnable)
1691			redraw(dobjs);
1692	}),
1693	new MenuItem("Clear", function(){	// clear
1694		clearCanvas();
1695		ajaxclear();
1696	}),
1697	new MenuItem("Redraw", function(){redraw(dobjs);}),// redraw
1698	new MenuItem("Undo", function(){	// undo
1699		clearCanvas();
1700		ajaxundo();
1701	}),
1702	new MenuItem("Size", function(){
1703		// Show size input layer on top of the canvas because the canvas cannot have
1704		// a text input element.
1705		if(!sizeLayer){
1706			sizeLayer = document.createElement('div');
1707			var lay = sizeLayer;
1708			lay.id = 'bookingLayer';
1709			lay.style.position = 'absolute';
1710			lay.style.padding = '5px 5px 5px 5px';
1711			lay.style.borderStyle = 'solid';
1712			lay.style.borderColor = '#cf0000';
1713			lay.style.borderWidth = '2px';
1714			// Drop shadow to make it distinguishable from the figure contents.
1715			lay.style.boxShadow = '0px 0px 20px grey';
1716			lay.style.background = '#cfffcf';
1717			lay.innerHTML = i18n.t('Input image size in pixels') + ':<br>'
1718				+ 'x:<input id="sizeinputx" type="text">'
1719				+ 'y:<input id="sizeinputy" type="text">';
1720			var okbutton = document.createElement('input');
1721			okbutton.type = 'button';
1722			okbutton.value = 'OK';
1723			okbutton.onclick = function(s){
1724				lay.style.display = 'none';
1725				setSize(parseFloat(document.getElementById('sizeinputx').value),
1726					parseFloat(document.getElementById('sizeinputy').value));
1727			}
1728			var cancelbutton = document.createElement('input');
1729			cancelbutton.type = 'button';
1730			cancelbutton.value = 'Cancel';
1731			cancelbutton.onclick = function(s){
1732				lay.style.display = 'none';
1733			}
1734			lay.appendChild(document.createElement('br'));
1735			lay.appendChild(okbutton);
1736			lay.appendChild(cancelbutton);
1737			// Append as the body element's child because style.position = "absolute" would
1738			// screw up in deeply nested DOM tree (which may have a positioned ancestor).
1739			document.body.appendChild(lay);
1740		}
1741		else // Just show the created layer in the second invocation.
1742			sizeLayer.style.display = 'block';
1743		var canvasRect = canvas.getBoundingClientRect();
1744		// Cross-browser scroll position query
1745		var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
1746		var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
1747		// getBoundingClientRect() returns coordinates relative to view, which means we have to
1748		// add scroll position into them.
1749		sizeLayer.style.left = (canvasRect.left + scrollX + 150) + 'px';
1750		sizeLayer.style.top = (canvasRect.top + scrollY + 50) + 'px';
1751		document.getElementById('sizeinputx').value = metaObj.size[0];
1752		document.getElementById('sizeinputy').value = metaObj.size[1];
1753	}), // size
1754];
1755
1756var buttons = [];
1757
1758
1759/// @brief Converts a sequence (array) to a set (object)
1760///
1761/// We wish if we could use YAML's !!set type (which is described in
1762/// http://yaml.org/type/set.html ), but JavaScript doesn't natively
1763/// support set type and js-yaml library converts YAML's !!set type
1764/// into an object with property names as keys in the set.
1765/// The values associated with the keys are not meaningful and
1766/// null is assigned by the library.
1767/// If we serialize the object, it would be an !!map instead of a !!set.
1768/// For example, a !!set {head, tail} would be serialized as
1769/// "{head: null, tail: null}", which is far from beautiful.
1770///
1771/// So we had to pretend as if !!set is encoded as !!seq in the serialized
1772/// YAML document by converting JavaScript objects into arrays.
1773/// It would be "[head, tail]" in the case of the former example.
1774///
1775/// The seq2set function converts sequence from YAML parser into
1776/// a set-like object, while set2seq does the opposite.
1777///
1778/// Note that these functions work only for sets of strings.
1779///
1780/// @sa set2seq
1781function seq2set(seq){
1782	var ret = {};
1783	for(var i = 0; i < seq.length; i++){
1784		if(seq[i] === "")
1785			continue;
1786		ret[seq[i]] = null;
1787	}
1788	return ret;
1789}
1790
1791/// @brief Converts a set (object) to a sequence (array)
1792/// @sa seq2set
1793function set2seq(set){
1794	var ret = [];
1795	for(var i in set){
1796		if(i === "")
1797			continue;
1798		ret.push(i);
1799	}
1800	return ret;
1801}
1802
1803// ==================== Tool class definition ================================= //
1804
1805// A mapping of tool names and tool objects. Automatically updated in the Tool class's constructor.
1806var toolmap = {};
1807
1808/// @brief A class that represents a tool in the toolbar.
1809/// @param name Name of the tool, used in serialized text
1810/// @param points Number of points which are used to describe points
1811/// @param params A table of initialization parameters:
1812///         objctor: The constructor function that is used to create Shape, stands for OBJect ConsTructOR
1813///         drawTool: A function(x, y) to draw icon on the toolbar.
1814///         setColor: A function(color) that is called before drawing.
1815///         setWidth: A function(width) that is called before drawing.
1816///         draw: A function(mode,str) to actually draw a shape.
1817function Tool(name, points, params){
1818	this.name = name;
1819	this.points = points || 1;
1820	this.objctor = params && params.objctor || Shape;
1821	this.drawTool = params && params.drawTool;
1822	mixin(this, params);
1823	// The path tool shares the same shape type identifier among multiple
1824	// tools, so duplicate assignments should be avoided.
1825	if(!(name in toolmap))
1826		toolmap[name] = this;
1827}
1828
1829Tool.prototype.setColor = function(color){
1830	ctx.strokeStyle = color;
1831};
1832
1833function setColorFill(color){
1834	ctx.fillStyle = color;
1835}
1836
1837Tool.prototype.setWidth = function(width){
1838	ctx.lineWidth = width;
1839};
1840
1841function nop(){}
1842
1843Tool.prototype.draw = nop;
1844
1845/// Append a point to the shape by mouse click
1846Tool.prototype.appendPoint = function(x, y) {
1847	function addPoint(x, y){
1848		if (cur_shape.points.length === cur_tool.points){
1849			dhistory.push(cloneObject(dobjs));
1850			dobjs.push(cur_shape);
1851			updateDrawData();
1852			cur_shape = null;
1853			redraw(dobjs);
1854			return true;
1855		}
1856		else{
1857			cur_shape.points.push(canvasToSrc(constrainCoord({x:x, y:y})));
1858			return false;
1859		}
1860	}
1861
1862	if(!cur_shape){
1863		var obj = new cur_tool.objctor();
1864		obj.tool = cur_tool.name;
1865		obj.color = cur_col;
1866		obj.width = cur_thin;
1867		cur_shape = obj;
1868		// Add two points for previewing shapes that consist of multiple points,
1869		// but single-point shapes will finish with just single click.
1870		if(!addPoint(x, y))
1871			addPoint(x, y);
1872	}
1873	else{
1874		addPoint(x, y);
1875	}
1876}
1877
1878Tool.prototype.mouseDown = function(e){
1879	// Do nothing by default
1880}
1881
1882Tool.prototype.mouseMove = function(e){
1883	if(cur_shape && 0 < cur_shape.points.length){
1884		var clrect = canvas.getBoundingClientRect();
1885		var mx = (gridEnable ? Math.round(e.clientX / gridSize) * gridSize : e.clientX) - clrect.left;
1886		var my = (gridEnable ? Math.round(e.clientY / gridSize) * gridSize : e.clientY) - clrect.top;
1887		// Live preview of the shape being added.
1888		var coord = {x: mx, y: my};
1889		cur_shape.points[cur_shape.points.length-1] = canvasToSrc(constrainCoord(coord));
1890		redraw(dobjs);
1891	}
1892}
1893
1894Tool.prototype.mouseUp = function(e){
1895	moving = false;
1896	sizing = null;
1897	pointMoving = null;
1898	var needsRedraw = boxselecting;
1899	boxselecting = false;
1900	if(needsRedraw) // Redraw to clear selection box
1901		redraw(dobjs);
1902}
1903
1904Tool.prototype.keyDown = function(e){
1905	var code = e.keyCode || e.which;
1906	if(code === 46){ // Delete key
1907		dhistory.push(cloneObject(dobjs)); // Push undo buffer
1908		// Delete all selected objects
1909		deleteShapes(selectobj);
1910		updateDrawData();
1911		redraw(dobjs);
1912	}
1913}
1914
1915// All other tools than the pathedit, draw scaling handles and
1916// bounding boxes around selected objects.
1917// Aside from the select tool, the user cannot grab the scaling handles,
1918// but they're useful to visually appeal that the object is selected.
1919Tool.prototype.selectDraw = function(shape){
1920	var bounds = objBounds(shape);
1921
1922	ctx.beginPath();
1923	ctx.lineWidth = 1;
1924	ctx.strokeStyle = '#000';
1925	ctx.setLineDash([5]);
1926	ctx.rect(bounds.minx, bounds.miny, bounds.maxx-bounds.minx, bounds.maxy-bounds.miny);
1927	ctx.stroke();
1928	ctx.setLineDash([]);
1929
1930	ctx.beginPath();
1931	ctx.strokeStyle = '#000';
1932	for(var i = 0; i < 8; i++){
1933		var r = getHandleRect(bounds, i);
1934		ctx.fillStyle = sizing === shape && i === sizedir ? '#7fff7f' : '#ffff7f';
1935		ctx.fillRect(r.minx, r.miny, r.maxx - r.minx, r.maxy-r.miny);
1936		ctx.rect(r.minx, r.miny, r.maxx - r.minx, r.maxy-r.miny);
1937	}
1938	ctx.stroke();
1939}
1940
1941// ==================== Tool class definition end ============================= //
1942
1943
1944// List of tools in the toolbar.
1945var toolbar = [
1946	new Tool("select", 1, {drawTool: function(x, y){
1947			ctx.beginPath();
1948			ctx.moveTo(x, y-5);
1949			ctx.lineTo(x, y+10);
1950			ctx.lineTo(x+4, y+7);
1951			ctx.lineTo(x+6, y+11);
1952			ctx.lineTo(x+8, y+9);
1953			ctx.lineTo(x+6, y+5);
1954			ctx.lineTo(x+10, y+3);
1955			ctx.closePath();
1956			ctx.stroke();
1957			ctx.strokeText('1', x+45, y+10);
1958		},
1959		mouseDown: selectMouseDown,
1960		mouseMove: selectMouseMove,
1961		mouseUp: function(e){
1962			if(0 < selectobj.length && (moving || sizing))
1963				updateDrawData();
1964			Tool.prototype.mouseUp.call(this, e);
1965		}
1966	}),
1967	new Tool("pathedit", 1, {
1968		// The path editing tool. This is especially useful when editing
1969		// existing curves.  The icon is identical to that of select tool,
1970		// except that the mouse cursor graphic is filled.
1971		drawTool: function(x, y){
1972			ctx.fillStyle = 'rgb(250, 250, 250)';
1973			ctx.beginPath();
1974			ctx.moveTo(x, y-5);
1975			ctx.lineTo(x, y+10);
1976			ctx.lineTo(x+4, y+7);
1977			ctx.lineTo(x+6, y+11);
1978			ctx.lineTo(x+8, y+9);
1979			ctx.lineTo(x+6, y+5);
1980			ctx.lineTo(x+10, y+3);
1981			ctx.closePath();
1982			ctx.fill();
1983			ctx.strokeText('1', x+45, y+10);
1984		},
1985		mouseDown: pathEditMouseDown,
1986		mouseMove: pathEditMouseMove,
1987		mouseUp: function(e){
1988			if(0 < selectobj.length && pointMoving)
1989				updateDrawData();
1990			Tool.prototype.mouseUp.call(this, e);
1991		},
1992		keyDown: function(e){
1993			var code = e.keyCode || e.which;
1994			if(code === 46){ // Delete key
1995				// Pathedit tool's delete key just delete single vertex in a path
1996				if(this.selectPointShape && this.selectPointShape.tool === "path" && 0 <= this.selectPointIdx){
1997					dhistory.push(cloneObject(dobjs)); // Push undo buffer
1998					if(this.selectPointShape.points.length <= 2){
1999						deleteShapes([this.selectPointShape]);
2000						this.selectPointShape = null;
2001					}
2002					else
2003						this.selectPointShape.points.splice(this.selectPointIdx, 1);
2004					updateDrawData();
2005					redraw(dobjs);
2006				}
2007			}
2008		},
2009		selectDraw: pathEditSelectDraw,
2010		selectPointShape: null, // Default value for the variable
2011		selectPointIdx: -1, // Default value for the variable
2012	}),
2013	new Tool("line", 2, {
2014		drawTool: function(x, y){
2015			ctx.beginPath();
2016			ctx.moveTo(x, y);
2017			ctx.lineTo(x+40, y+10);
2018			ctx.stroke();
2019			ctx.strokeText('2', x+45, y+10);
2020		},
2021		draw: function(obj){
2022			var arr = obj.points;
2023			ctx.beginPath();
2024			ctx.moveTo(arr[0].x, arr[0].y);
2025			ctx.lineTo(arr[1].x, arr[1].y);
2026			ctx.stroke();
2027			ctx.lineWidth = 1;
2028		},
2029	}),
2030	new Tool("arrow", 2, {
2031		drawTool: function(x, y){
2032			ctx.beginPath();
2033			l_arrow(ctx, [{x:x, y:y+5}, {x:x+40, y:y+5}]);
2034			ctx.strokeText('2', x+45, y+10);
2035		},
2036		draw: function(obj){
2037			var arr = obj.points;
2038			ctx.beginPath();
2039			l_arrow(ctx, arr);
2040			ctx.lineWidth = 1;
2041		}
2042	}),
2043	new Tool("barrow", 2, {
2044		drawTool: function(x, y){
2045			ctx.beginPath();
2046			l_tarrow(ctx, [{x:x, y:y+5}, {x:x+40, y:y+5}]);
2047			ctx.strokeText('2', x+45, y+10);
2048		},
2049		draw: function(obj){
2050			var arr = obj.points;
2051			ctx.beginPath();
2052			l_tarrow(ctx, arr);
2053			ctx.lineWidth = 1;
2054		}
2055	}),
2056	new Tool("darrow", 2, {
2057		drawTool: function(x, y){
2058			ctx.beginPath();
2059			ctx.moveTo(x, y+3);
2060			ctx.lineTo(x+39, y+3);
2061			ctx.moveTo(x, y+7);
2062			ctx.lineTo(x+39, y+7);
2063			ctx.moveTo(x+35, y);
2064			ctx.lineTo(x+40, y+5);
2065			ctx.lineTo(x+35, y+10);
2066			ctx.stroke();
2067			ctx.strokeText('2', x+45, y+10);
2068		},
2069		draw: function(obj){
2070			var arr = obj.points;
2071			ctx.beginPath();
2072			l_darrow(ctx, arr);
2073			ctx.lineWidth = 1;
2074		}
2075	}),
2076	new Tool("arc", 3, {
2077		isArc: true,
2078		drawTool: function(x, y){
2079			ctx.beginPath();
2080			ctx.moveTo(x, y);
2081			ctx.quadraticCurveTo(x+20, y+20, x+40, y);
2082			ctx.stroke();
2083			ctx.strokeText('3', x+45, y+10);
2084		},
2085		draw: function(obj){
2086			var arr = obj.points;
2087			ctx.beginPath();
2088			ctx.moveTo(arr[0].x, arr[0].y);
2089			ctx.quadraticCurveTo(arr[1].x, arr[1].y, arr[2].x, arr[2].y);
2090			ctx.stroke();
2091			ctx.lineWidth = 1;
2092		}
2093	}),
2094	new Tool("arcarrow", 3, {
2095		isArc: true,
2096		drawTool: function(x, y){
2097			ctx.beginPath();
2098			ctx.moveTo(x, y);
2099			ctx.quadraticCurveTo(x+20, y+20, x+40, y);
2100			l_hige(ctx, [{x:x+20, y:y+20}, {x:x+40, y:y}]);
2101			ctx.strokeText('3', x+45, y+10);
2102		},
2103		draw: function(obj){
2104			var arr = obj.points;
2105			ctx.beginPath();
2106			ctx.moveTo(arr[0].x, arr[0].y);
2107			ctx.quadraticCurveTo(arr[1].x, arr[1].y, arr[2].x, arr[2].y);
2108			l_hige(ctx, [arr[1], arr[2]]);
2109			ctx.lineWidth = 1;
2110		}
2111	}),
2112	new Tool("arcbarrow", 3, {
2113		isArc: true,
2114		drawTool: function(x, y){
2115			ctx.beginPath();
2116			ctx.moveTo(x, y);
2117			ctx.quadraticCurveTo(x+20, y+20, x+40, y);
2118			var a = [{x:x+20, y:y+20}, {x:x+40, y:y}];
2119			l_hige(ctx, a);
2120			//a[0] = {x:x+10, y:y+10};
2121			a[1] = {x:x, y:y};
2122			l_hige(ctx, a);
2123			ctx.strokeText('3', x+45, y+10);
2124		},
2125		draw: function(obj){
2126			var arr = obj.points;
2127			ctx.beginPath();
2128			ctx.moveTo(arr[0].x, arr[0].y);
2129			ctx.quadraticCurveTo(arr[1].x, arr[1].y, arr[2].x, arr[2].y);
2130			var a = new Array(2);
2131			a[0] = arr[1];
2132			a[1] = arr[2];
2133			l_hige(ctx, a);
2134			a[1] = arr[0];
2135			l_hige(ctx, a);
2136			ctx.lineWidth = 1;
2137		}
2138	}),
2139	new Tool("rect", 2, {
2140		drawTool: function(x, y){
2141			ctx.beginPath();
2142			ctx.rect(x, y, 40, 10);
2143			ctx.stroke();
2144			ctx.strokeText('2', x+45, y+10);
2145		},
2146		draw: function(obj){
2147			var arr = obj.points;
2148			ctx.beginPath();
2149			ctx.rect(arr[0].x, arr[0].y, arr[1].x-arr[0].x, arr[1].y-arr[0].y);
2150			ctx.stroke();
2151			ctx.lineWidth = 1;
2152		}
2153	}),
2154	new Tool("ellipse", 2, {
2155		drawTool: function(x, y){
2156			ctx.beginPath();
2157			ctx.scale(1.0, 0.5);		// vertically half
2158			ctx.arc(x+20, (y+5)*2, 20, 0, 2 * Math.PI, false);
2159			ctx.stroke();
2160			ctx.scale(1.0, 2.0);
2161			ctx.strokeText('2', x+45, y+10);
2162		},
2163		draw: function(obj){
2164			var arr = obj.points;
2165			ctx.beginPath();
2166			l_elipse(ctx, arr);
2167			ctx.lineWidth = 1;
2168		}
2169	}),
2170	new Tool("rectfill", 2, {
2171		drawTool: function(x, y){
2172			ctx.beginPath();
2173			ctx.fillStyle = 'rgb(250, 250, 250)';
2174			ctx.fillRect(x, y, 40, 10);
2175			ctx.strokeText('2', x+45, y+10);
2176		},
2177		setColor: setColorFill,
2178		setWidth: nop,
2179		draw: function(obj){
2180			var arr = obj.points;
2181			ctx.beginPath();
2182			ctx.fillRect(arr[0].x, arr[0].y, arr[1].x-arr[0].x, arr[1].y-arr[0].y);
2183		}
2184	}),
2185	new Tool("ellipsefill", 2, {drawTool: function(x, y){
2186			ctx.beginPath();
2187			ctx.fillStyle = 'rgb(250, 250, 250)';
2188			ctx.scale(1.0, 0.5);		// vertically half
2189			ctx.arc(x+20, (y+5)*2, 20, 0, 2 * Math.PI, false);
2190			ctx.fill();
2191			ctx.scale(1.0, 2.0);
2192			ctx.strokeText('2', x+45, y+10);
2193		},
2194		setColor: setColorFill,
2195		setWidth: nop,
2196		draw: function(obj){
2197			var arr = obj.points;
2198			ctx.beginPath();
2199			l_elipsef(ctx, arr);
2200			ctx.lineWidth = 1;
2201		}
2202	}),
2203	new Tool("star", 1, {objctor: PointShape,
2204		drawTool: function(x, y){
2205			ctx.beginPath();
2206			ctx.moveTo(x+8, y-3);
2207			ctx.lineTo(x+14, y+13);
2208			ctx.lineTo(x, y+2);
2209			ctx.lineTo(x+16, y+2);
2210			ctx.lineTo(x+2, y+13);
2211			ctx.closePath();
2212			ctx.stroke();
2213			ctx.strokeText('1', x+45, y+10);
2214		},
2215		draw: function(obj){
2216			var arr = obj.points;
2217			ctx.beginPath();
2218			//ctx.lineWidth = cur_thin - 40;
2219			l_star(ctx, arr);
2220			ctx.lineWidth = 1;
2221		}
2222	}),
2223	new Tool("check", 1, {objctor: PointShape,
2224		drawTool: function(x, y){
2225			ctx.beginPath();
2226			ctx.moveTo(x, y);
2227			ctx.lineTo(x+5, y+7);
2228			ctx.lineTo(x+20, y);
2229			ctx.stroke();
2230			ctx.strokeText('1', x+45, y+10);
2231		},
2232		setWidth: nop,
2233		draw: function(obj){
2234			var arr = obj.points;
2235			ctx.beginPath();
2236			l_check(ctx, arr);
2237		}
2238	}),
2239	new Tool("text", 1, {objctor: TextShape,
2240		drawTool: function(x, y){
2241			ctx.beginPath();
2242			ctx.strokeText(i18n.t('Text'), x+3, y+10);
2243			ctx.strokeText('1', x+45, y+10);
2244		},
2245		setColor: setColorFill,
2246		draw: function(obj){
2247			var str = obj.text;
2248			if (null == str) {		// cancel
2249//					idx = 0;
2250				return;
2251			}
2252			ctx.beginPath();
2253			if (1 == obj.width) setFont(14);
2254			else if (2 == obj.width) setFont(16);
2255			else setFont(20);
2256			ctx.fillText(str, obj.points[0].x, obj.points[0].y);
2257
2258			// Draw blue underline for linked text
2259			if(obj.link){
2260				ctx.strokeStyle = '#0000ff';
2261				ctx.beginPath();
2262				ctx.moveTo(obj.points[0].x, obj.points[0].y + 4);
2263				ctx.lineTo(obj.points[0].x + ctx.measureText(str).width, obj.points[0].y + 4);
2264				ctx.stroke();
2265			}
2266
2267			ctx.font = setFont(14);
2268		},
2269		appendPoint: function(x, y){
2270			var textTool = this;
2271			// Show size input layer on top of the canvas because the canvas cannot have
2272			// a text input element.
2273			if(!textLayer){
2274				textLayer = document.createElement('div');
2275				// Create field for remembering position of text being inserted.
2276				// Free variables won't work well.
2277				textLayer.canvasPos = {x:0, y:0};
2278				var lay = textLayer;
2279				lay.id = 'textLayer';
2280				lay.style.position = 'absolute';
2281				lay.style.padding = '5px 5px 5px 5px';
2282				lay.style.borderStyle = 'solid';
2283				lay.style.borderColor = '#cf0000';
2284				lay.style.borderWidth = '2px';
2285				// Drop shadow to make it distinguishable from the figure contents.
2286				lay.style.boxShadow = '0px 0px 20px grey';
2287				lay.style.background = '#cfffcf';
2288
2289				// Create and assign the input element to a field of the textLayer object
2290				// to keep track of the input element after this function is exited.
2291				lay.textInput = document.createElement('input');
2292				lay.textInput.id = "textinput";
2293				lay.textInput.type = "text";
2294				lay.textInput.style.width = "30em";
2295				lay.textInput.onkeyup = function(e){
2296					// Convert enter key event to OK button click
2297					if(e.keyCode === 13)
2298						okbutton.onclick();
2299				};
2300				lay.appendChild(lay.textInput);
2301
2302				// Add a text area for link
2303				lay.appendChild(document.createElement('br'));
2304				lay.linkInput = document.createElement('input');
2305				lay.linkInput.id = "linkinput";
2306				lay.linkInput.type = "text";
2307				lay.linkInput.onkeyup = lay.textInput.onkeyup;
2308				var linkdiv = document.createElement('div');
2309				linkdiv.innerHTML = "Link:";
2310				linkdiv.appendChild(lay.linkInput);
2311				lay.appendChild(linkdiv);
2312
2313				var okbutton = document.createElement('input');
2314				okbutton.type = 'button';
2315				okbutton.value = 'OK';
2316				okbutton.onclick = function(e){
2317					lay.style.display = 'none';
2318					// Ignore blank text
2319					if(lay.textInput.value == '')
2320						return;
2321					dhistory.push(cloneObject(dobjs));
2322					// If a shape is clicked, alter its value instead of adding a new one.
2323					if(lay.dobj){
2324						lay.dobj.text = lay.textInput.value;
2325						lay.dobj.link = lay.linkInput.value;
2326					}
2327					else{
2328						var obj = new textTool.objctor();
2329						obj.tool = cur_tool.name;
2330						obj.color = cur_col;
2331						obj.width = cur_thin;
2332						obj.points.push({x: lay.canvasPos.x, y: lay.canvasPos.y});
2333						obj.text = lay.textInput.value;
2334						obj.link = lay.linkInput.value;
2335						dobjs.push(obj);
2336					}
2337					updateDrawData();
2338					redraw(dobjs);
2339				}
2340				var cancelbutton = document.createElement('input');
2341				cancelbutton.type = 'button';
2342				cancelbutton.value = 'Cancel';
2343				cancelbutton.onclick = function(s){
2344					lay.style.display = 'none';
2345				}
2346//				lay.appendChild(document.createElement('br')); // It seems to add an extra line break
2347				lay.appendChild(okbutton);
2348				lay.appendChild(cancelbutton);
2349				// Append as the body element's child because style.position = "absolute" would
2350				// screw up in deeply nested DOM tree (which may have a positioned ancestor).
2351				document.body.appendChild(lay);
2352			}
2353			else
2354				textLayer.style.display = 'block';
2355			var coord = canvasToSrc(constrainCoord({x:x, y:y}));
2356			textLayer.canvasPos.x = coord.x;
2357			textLayer.canvasPos.y = coord.y;
2358
2359			// Find if any TextShape is under the mouse cursor.
2360			textLayer.dobj = null;
2361			textLayer.textInput.value = "";
2362			for (var i = 0; i < dobjs.length; i++) {
2363				if(dobjs[i] instanceof TextShape && hitRect(objBounds(dobjs[i], true), coord.x, coord.y)){
2364					textLayer.dobj = dobjs[i]; // Remember the shape being clicked on.
2365					textLayer.textInput.value = dobjs[i].text; // Initialized the input buffer with the previous content.
2366					textLayer.linkInput.value = dobjs[i].link;
2367					break;
2368				}
2369			}
2370
2371			// Adjust the text area's width so that two of them uses the div element's space efficiently.
2372			var textInputRect = textLayer.textInput.getBoundingClientRect();
2373			var linkInputRect = textLayer.linkInput.getBoundingClientRect();
2374			textLayer.linkInput.style.width = textInputRect.width - (linkInputRect.left - textInputRect.left) + "px";
2375
2376			var canvasRect = canvas.getBoundingClientRect();
2377			// Cross-browser scroll position query
2378			var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
2379			var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
2380			// getBoundingClientRect() returns coordinates relative to view, which means we have to
2381			// add scroll position into them.
2382			textLayer.style.left = (canvasRect.left + scrollX + offset.x + coord.x) + 'px';
2383			textLayer.style.top = (canvasRect.top + scrollY + offset.y + coord.y) + 'px';
2384			// focus() should be called after textLayer is positioned, otherwise the page may
2385			// unexpectedly scroll to somewhere.
2386			textLayer.textInput.focus();
2387
2388			// Reset the point buffer
2389//				idx = 0;
2390			return true; // Skip registration
2391		}
2392	})
2393];
2394
2395var toolbars = [toolbar,
2396	[
2397		toolmap.select,
2398		toolmap.pathedit,
2399		new Tool("delete", 1, {
2400			drawTool: function(x, y){
2401				ctx.beginPath();
2402				ctx.moveTo(x, y);
2403				ctx.lineTo(x+10, y+10);
2404				ctx.moveTo(x, y+10);
2405				ctx.lineTo(x+10, y);
2406				ctx.stroke();
2407				ctx.strokeText('1', x+45, y+10);
2408			}
2409		}),
2410		new Tool("done", 1, {objctor: PointShape,
2411			drawTool: function(x, y){
2412				ctx.beginPath();
2413				ctx.strokeText(i18n.t('Done'), x+3, y+10);
2414				ctx.beginPath();
2415				ctx.arc(x+9, y+5, 8, 0, 6.28, false);
2416				ctx.stroke();
2417				ctx.strokeText('1', x+45, y+10);
2418			},
2419			setWidth: nop,
2420			draw: function(obj){
2421				var arr = obj.points;
2422				ctx.beginPath();
2423				l_complete(ctx, arr);
2424			}
2425		}),
2426		new Tool("path", 2, {
2427			objctor: PathShape,
2428			drawTool: function(x, y){
2429				ctx.beginPath();
2430				ctx.moveTo(x, y);
2431				ctx.bezierCurveTo(x+10, y+20, x+20, y-10, x+30, y+10);
2432				ctx.stroke();
2433				ctx.strokeText('n', x+45, y+10);
2434			},
2435			draw: function(obj){
2436				var arr = obj.points;
2437				if(arr.length < 2)
2438					return;
2439				ctx.beginPath();
2440				ctx.moveTo(arr[0].x, arr[0].y);
2441				for(var i = 1; i < arr.length; i++){
2442					if("cx" in arr[i] && "cy" in arr[i]){
2443						if("dx" in arr[i] && "dy" in arr[i])
2444							ctx.bezierCurveTo(arr[i].cx, arr[i].cy, arr[i].dx, arr[i].dy, arr[i].x, arr[i].y);
2445						else
2446							ctx.bezierCurveTo(arr[i].cx, arr[i].cy, arr[i].x, arr[i].y, arr[i].x, arr[i].y);
2447					}
2448					else if("dx" in arr[i] && "dy" in arr[i])
2449						ctx.bezierCurveTo(arr[i-1].x, arr[i-1].y, arr[i].dx, arr[i].dy, arr[i].x, arr[i].y);
2450					else
2451						ctx.lineTo(arr[i].x, arr[i].y);
2452				}
2453				ctx.stroke();
2454				if("arrow" in obj && "head" in obj.arrow && 1 < arr.length){
2455					var first = arr[0], first2 = arr[1], a = [];
2456					if("cx" in first2 && "cy" in first2 && (first2.cx !== first.x || first2.cy !== first.y))
2457						a[0] = {x: first2.cx, y: first2.cy};
2458					else if("dx" in first2 && "dy" in first2 && (first2.dx !== first.x || first2.dy !== first.y))
2459						a[0] = {x: first2.dx, y: first2.dy};
2460					else
2461						a[0] = first2;
2462					a[1] = first;
2463					ctx.beginPath();
2464					l_hige(ctx, a);
2465				}
2466				if("arrow" in obj && "tail" in obj.arrow && 1 < arr.length){
2467					var last = arr[arr.length-1], last2 = arr[arr.length-2], a = [];
2468					if("dx" in last && "dy" in last && (last.dx !== last.x || last.dy !== last.y))
2469						a[0] = {x: last.dx, y: last.dy};
2470					else if("cx" in last && "cy" in last && (last.cx !== last.x || last.cy !== last.y))
2471						a[0] = {x: last.cx, y: last.cy};
2472					else
2473						a[0] = last2;
2474					a[1] = last;
2475					ctx.beginPath();
2476					l_hige(ctx, a);
2477				}
2478				ctx.lineWidth = 1;
2479
2480				// Draws dashed line that connects a control point and its associated vertex
2481				// This one does not take offset into account since the transformation is done
2482				// before this function.
2483				function drawGuidingLineNoOffset(pt0, pt, name){
2484					ctx.setLineDash([5]);
2485					ctx.beginPath();
2486					ctx.moveTo(pt0.x, pt0.y);
2487					ctx.lineTo(pt[name + "x"], pt[name + "y"]);
2488					ctx.stroke();
2489					ctx.setLineDash([]);
2490				}
2491
2492				if(obj === cur_shape){
2493					var last = arr[arr.length-1];
2494					drawGuidingLineNoOffset(last ,last, "d");
2495					drawHandle(last.dx, last.dy, "#ff7f7f", true);
2496					var next = {x: 2 * last.x - last.dx, y: 2 * last.y - last.dy};
2497					drawGuidingLineNoOffset(last, next, "");
2498					drawHandle(next.x, next.y, "#ff7f7f", true);
2499				}
2500			},
2501			onNewShape: function(shape){}, /// Virtual event handler on creation of a new shape
2502			appendPoint: function(x, y){
2503				function addPoint(){
2504					var pts = cur_shape.points;
2505					pts.push(cloneObject(d));
2506				}
2507
2508				var d = canvasToSrc(constrainCoord({x:x, y:y}));
2509				if(!cur_shape){
2510					var obj = new cur_tool.objctor();
2511					obj.tool = cur_tool.name;
2512					obj.color = cur_col;
2513					obj.width = cur_thin;
2514					this.onNewShape(obj);
2515					cur_shape = obj;
2516					addPoint();
2517					addPoint();
2518				}
2519				else{
2520					var pts = cur_shape.points;
2521					if(pts.length !== 1){
2522						var prev = pts[pts.length-2];
2523						if(d.x === prev.x && d.y === prev.y){
2524							cur_shape.points.pop();
2525							dhistory.push(cloneObject(dobjs));
2526							dobjs.push(cur_shape);
2527							updateDrawData();
2528							cur_shape = null;
2529							redraw(dobjs);
2530							return true;
2531						}
2532					}
2533					if(0 < pts.length){
2534						var prev = pts[pts.length-1];
2535						// Mirror position of control point for smooth curve
2536						if("dx" in prev && "dy" in prev){
2537							d.cx = 2 * prev.x - prev.dx;
2538							d.cy = 2 * prev.y - prev.dy;
2539						}
2540					}
2541					addPoint();
2542				}
2543			},
2544			mouseDown: function(e){
2545				if(cur_shape){
2546					var clrect = canvas.getBoundingClientRect();
2547					var mx = e.clientX - clrect.left;
2548					var my = e.clientY - clrect.top;
2549					// Remember mouse stat to this (Path tool) object for later use
2550					// in mouseMove().
2551					this.lastx = mx;
2552					this.lasty = my;
2553					this.lastPoint = cur_shape.points[cur_shape.points.length-1];
2554				}
2555			},
2556			mouseMove: function(e){
2557				if(!cur_shape)
2558					return;
2559				var clrect = canvas.getBoundingClientRect();
2560				var mx = e.clientX - clrect.left;
2561				var my = e.clientY - clrect.top;
2562				if(this.lastPoint){
2563					var pt = this.lastPoint;
2564					var d = canvasToSrc(constrainCoord({x:mx, y:my}));
2565					if(0 < cur_shape.points.length && (pt.x !== d.x && pt.y !== d.y)){
2566						var prev = cur_shape.points[cur_shape.points.length-2];
2567						// Extend control point by mouse dragging.
2568						pt.dx = 2 * pt.x - d.x;
2569						pt.dy = 2 * pt.y - d.y;
2570					}
2571					redraw(dobjs);
2572				}
2573				else if(0 < cur_shape.points.length){
2574					// Live preview of the shape being added.
2575					var coord = canvasToSrc(constrainCoord({x: mx, y: my}));
2576					var pt = cur_shape.points[cur_shape.points.length-1];
2577					var dx = coord.x - pt.x;
2578					var dy = coord.y - pt.y;
2579					pt.x = coord.x;
2580					pt.y = coord.y;
2581					// Only move dx and dy along with x, y.
2582					// cx and cy should be moved with the previous vertex.
2583					if("dx" in pt && "dy" in pt)
2584						pt.dx += dx, pt.dy += dy;
2585					redraw(dobjs);
2586				}
2587			},
2588			mouseUp: function(e){
2589				this.lastPoint = null;
2590			},
2591		}),
2592		new Tool("path", 2, {
2593			objctor: PathShape,
2594			drawTool: function(x, y){
2595				ctx.beginPath();
2596				ctx.moveTo(x, y);
2597				ctx.bezierCurveTo(x+10, y+20, x+20, y-10, x+30, y+10);
2598				ctx.stroke();
2599				l_hige(ctx, [{x: x+20, y: y-10}, {x: x+30, y: y+10}]);
2600				ctx.strokeText('n', x+45, y+10);
2601			},
2602			draw: toolmap.path.draw,
2603			onNewShape: function(shape){shape.arrow = {"tail": null}},
2604			appendPoint: toolmap.path.appendPoint,
2605			mouseDown: toolmap.path.mouseDown,
2606			mouseMove: toolmap.path.mouseMove,
2607			mouseUp: toolmap.path.mouseUp,
2608		}),
2609		new Tool("path", 2, {
2610			objctor: PathShape,
2611			drawTool: function(x, y){
2612				ctx.beginPath();
2613				ctx.moveTo(x, y);
2614				ctx.bezierCurveTo(x+10, y+20, x+20, y-10, x+30, y+10);
2615				ctx.stroke();
2616				l_hige(ctx, [{x: x+10, y: y+20}, {x: x, y: y}]);
2617				l_hige(ctx, [{x: x+20, y: y-10}, {x: x+30, y: y+10}]);
2618				ctx.strokeText('n', x+45, y+10);
2619			},
2620			draw: toolmap.path.draw,
2621			onNewShape: function(shape){shape.arrow = {head: null, "tail": null}},
2622			appendPoint: toolmap.path.appendPoint,
2623			mouseDown: toolmap.path.mouseDown,
2624			mouseMove: toolmap.path.mouseMove,
2625			mouseUp: toolmap.path.mouseUp,
2626		})
2627	]
2628];
2629var toolbarPage = 0;
2630
2631var white = "rgb(255, 255, 255)";
2632var black = "rgb(0, 0, 0)";
2633var blue = "rgb(0, 100, 255)";
2634var green = "rgb(0, 255, 0)";
2635var red = "rgb(255, 0, 0)";
2636var gray = "rgb(150, 150, 150)";
2637var colstr = new Array(black,blue,red,green,white);
2638var colnames = ["black", "blue", "red", "green", "white"];
2639var coltable = {"black": black, "blue": blue, "red": red, "green": green, "white": white};
2640var x0 = 0, y0 = 0, w0 = 1024, h0 = 640;
2641var x1 = 90, y1 = 50, w1 = 930, h1 = 580;
2642var mx0 = 10, mx1 = x1, mx2 = 600, mx3 = 820;
2643var mw0 = 70, mw1 = 60, mw2 = 30, my0 = 20, mh0 = 28;
2644var cur_tool = toolmap.select, cur_col = "black", cur_thin = 1;
2645var cur_shape = null;
2646var offset = editmode ? {x:x1, y:y1} : {x:0, y:0};
2647
2648// The layer to show input controls for width and height sizes of the figure.
2649// It's kept as a member variable in order to reuse in the second and later invocations.
2650var sizeLayer = null;
2651
2652// The layer to input text.
2653// It used to be prompt() function, but it's not beautiful.
2654var textLayer = null;
2655
2656// The default metaObj values used for resetting.
2657var defaultMetaObj = {type: "meta", size: [1024-x1, 640-y1]};
2658
2659// The meta object is always the first element in the serialized figure text,
2660// but is not an element of dobjs array.
2661// It's automatically loaded when deserialized and included when serialized.
2662var metaObj = cloneObject(defaultMetaObj);
2663
2664// A pseudo-this pointer that can be used in private methods.
2665// Private methods mean local functions in this constructor, which
2666// don't share this pointer with the constructor itself unless the
2667// form "function.call(this)" is used.
2668var self = this;
2669
2670onload();
2671}
2672
2673// Create and return a XMLHttpRequest object or ActiveXObject for IE6-
2674SketchCanvas.prototype.createXMLHttpRequest = function(){
2675	var xmlHttp = null;
2676	try{
2677		// for IE7+, Fireforx, Chrome, Opera, Safari
2678		xmlHttp = new XMLHttpRequest();
2679	}
2680	catch(e){
2681		try{
2682			// for IE6, IE5 (canvas element wouldn't work from the start, though)
2683			xmlHttp = new ActiveXObject("Msxml2.XMLHTTP");
2684		}
2685		catch(e){
2686			try{
2687				xmlHttp = new ActiveXObject("Microsoft.XMLHttp");
2688			}
2689			catch(e){
2690				return null;
2691			}
2692		}
2693	}
2694	return xmlHttp;
2695}
2696