/// @brief The SketchCanvas class accepts a canvas object to draw on. /// @param canvas The canvas to draw the figure to. /// @param options An initialization parameters table that contains following items: /// editmode: The canvas is used for editing when true. /// debug: A function with one argument to output debug string. /// /// Make sure to invoke this class's constructor with "new" prepended /// and keep the returned object to some variable. /// /// It has following event handlers that can be assigned as this object's method. /// /// function onLocalChange(); /// This event is invoked when the canvas saves its contents into localStorage of the /// browser. Use this event to update list of locally saved figures. /// /// function onUpdateServerList(list); /// This event is invoked when the object requests the server to refresh figure list /// and receives response. /// /// function onUpdateData(data); /// This event is invoked when the contents of figure data is modified. function SketchCanvas(canvas, options){ 'use strict'; var editmode = options && options.editmode; var scale = options && options.scale ? options.scale : 1; // Obtain the browser's preferred language. var currentLanguage = (window.navigator.language || window.navigator.userLanguage || window.navigator.userLanguage); currentLanguage = currentLanguage.substr(0, 2); i18n.init({lng: currentLanguage, fallbackLng: 'en', resStore: resources, getAsync: false}); var dobjs; // Drawing objects var dhistory; // Drawing object history (for undoing) var selectobj = []; var handleSize = 4; var gridEnable = false; var gridSize = 8; var toolButtonInterval = 36; // Called at the end of the constructor function onload(){ // Check existence of canvas element and treating not compatible browsers if ( ! canvas || ! canvas.getContext ) { return false; } // Ignore mouse events if it's non-edit mode. if(editmode){ canvas.onclick = mouseLeftClick; canvas.onmousedown = mouseDown; canvas.onmouseup = mouseUp; canvas.onmousemove = mouseMove; canvas.onmouseout = mouseleave; canvas.setAttribute("tabindex", 0); // Make sure the canvas can have a key focus canvas.onkeydown = keyDown; } else canvas.onclick = viewModeClick; // 2D context ctx = canvas.getContext('2d'); // Set a placeholder function to ignore setLineDash method for browsers that don't support it. // The method is not compatible with many browsers, but we don't care if it's not supported // because it's only used for selected object designation. if(!ctx.setLineDash) { ctx.setLineDash = function(){}; } var aw = 20; // Arrow width // Previous toolbar page button buttons.push(new Button(mx0, my0, mx0 + aw, my0 + mh0, function(){ ctx.fillStyle = 'rgb(127,127,127)'; ctx.beginPath(); ctx.moveTo(mx0, my0 + mh0 / 2); ctx.lineTo(mx0 + aw, my0); ctx.lineTo(mx0 + aw, my0 + mh0); ctx.closePath(); ctx.fill(); }, function(){ debug("clicked left"); toolbarPage = Math.max(toolbarPage - 1, 0); toolbar = toolbars[toolbarPage]; cur_tool = toolbar[0]; draw(); })); // Next toolbar page button buttons.push(new Button(mx0 + mw0 - aw, my0, mx0 + mw0, my0 + mh0, function(){ ctx.beginPath(); ctx.moveTo(mx0 + mw0, my0 + mh0 / 2); ctx.lineTo(mx0 + mw0 - aw, my0); ctx.lineTo(mx0 + mw0 - aw, my0 + mh0); ctx.closePath(); ctx.fill(); }, function(){ debug("clicked right"); toolbarPage = Math.min(toolbarPage + 1, toolbars.length-1); toolbar = toolbars[toolbarPage]; cur_tool = toolbar[0]; draw(); })); resizeCanvas(); draw(); // Draw objects dobjs = []; // And the history of operations dhistory = []; } var datadir = "data"; function draw() { if(!editmode) return; // Draw a rectangle ctx.beginPath(); ctx.strokeStyle = 'rgb(192, 192, 77)'; // yellow ctx.font = i18n.t("14px 'Courier'"); ctx.strokeText('SketchCanvas Editor v0.1.3', 420, 10); ctx.rect(x0, y0, w0, h0); ctx.rect(x1, y1, w1, h1); ctx.closePath(); ctx.stroke(); // Background for the page text ctx.fillStyle = 'rgb(255,255,255)'; ctx.fillRect(x0 + 1, y0 + 1, x1 - 2, y0 + h0 - 2); // Text indicating current toolbar page ctx.fillStyle = 'rgb(0,0,0)'; var text = (toolbarPage + 1) + "/" + (toolbars.length); var width = ctx.measureText(text).width; ctx.fillText(text, mx0 + mw0 / 2 - width / 2, my0 + mh0 / 2); // menu drawMenu(); drawTBox(); drawButtons(); drawCBox(cur_col); drawHBox(cur_thin); } // draw coord(for Debug) function drawPos(x, y) { ctx.strokeText('X='+x+' Y='+y, x, y); } // Menu function drawMenu() { for(var i=0;i= 0) process(); } process(); this.points = arr; }; // ==================== PathShape class definition end ================================= // function serialize(dobjs){ var ret = [metaObj]; for(var i = 0; i < dobjs.length; i++) ret.push(dobjs[i].serialize()); return ret; } function deserialize(dat){ // Reset the metaObj before deserialization metaObj = cloneObject(defaultMetaObj); var ret = []; for (var i=0; i' + 'x:' + 'y:'; var okbutton = document.createElement('input'); okbutton.type = 'button'; okbutton.value = 'OK'; okbutton.onclick = function(s){ lay.style.display = 'none'; setSize(parseFloat(document.getElementById('sizeinputx').value), parseFloat(document.getElementById('sizeinputy').value)); } var cancelbutton = document.createElement('input'); cancelbutton.type = 'button'; cancelbutton.value = 'Cancel'; cancelbutton.onclick = function(s){ lay.style.display = 'none'; } lay.appendChild(document.createElement('br')); lay.appendChild(okbutton); lay.appendChild(cancelbutton); // Append as the body element's child because style.position = "absolute" would // screw up in deeply nested DOM tree (which may have a positioned ancestor). document.body.appendChild(lay); } else // Just show the created layer in the second invocation. sizeLayer.style.display = 'block'; var canvasRect = canvas.getBoundingClientRect(); // Cross-browser scroll position query var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; var scrollY = document.documentElement.scrollTop || document.body.scrollTop; // getBoundingClientRect() returns coordinates relative to view, which means we have to // add scroll position into them. sizeLayer.style.left = (canvasRect.left + scrollX + 150) + 'px'; sizeLayer.style.top = (canvasRect.top + scrollY + 50) + 'px'; document.getElementById('sizeinputx').value = metaObj.size[0]; document.getElementById('sizeinputy').value = metaObj.size[1]; }), // size ]; var buttons = []; /// @brief Converts a sequence (array) to a set (object) /// /// We wish if we could use YAML's !!set type (which is described in /// http://yaml.org/type/set.html ), but JavaScript doesn't natively /// support set type and js-yaml library converts YAML's !!set type /// into an object with property names as keys in the set. /// The values associated with the keys are not meaningful and /// null is assigned by the library. /// If we serialize the object, it would be an !!map instead of a !!set. /// For example, a !!set {head, tail} would be serialized as /// "{head: null, tail: null}", which is far from beautiful. /// /// So we had to pretend as if !!set is encoded as !!seq in the serialized /// YAML document by converting JavaScript objects into arrays. /// It would be "[head, tail]" in the case of the former example. /// /// The seq2set function converts sequence from YAML parser into /// a set-like object, while set2seq does the opposite. /// /// Note that these functions work only for sets of strings. /// /// @sa set2seq function seq2set(seq){ var ret = {}; for(var i = 0; i < seq.length; i++){ if(seq[i] === "") continue; ret[seq[i]] = null; } return ret; } /// @brief Converts a set (object) to a sequence (array) /// @sa seq2set function set2seq(set){ var ret = []; for(var i in set){ if(i === "") continue; ret.push(i); } return ret; } // ==================== Tool class definition ================================= // // A mapping of tool names and tool objects. Automatically updated in the Tool class's constructor. var toolmap = {}; /// @brief A class that represents a tool in the toolbar. /// @param name Name of the tool, used in serialized text /// @param points Number of points which are used to describe points /// @param params A table of initialization parameters: /// objctor: The constructor function that is used to create Shape, stands for OBJect ConsTructOR /// drawTool: A function(x, y) to draw icon on the toolbar. /// setColor: A function(color) that is called before drawing. /// setWidth: A function(width) that is called before drawing. /// draw: A function(mode,str) to actually draw a shape. function Tool(name, points, params){ this.name = name; this.points = points || 1; this.objctor = params && params.objctor || Shape; this.drawTool = params && params.drawTool; mixin(this, params); // The path tool shares the same shape type identifier among multiple // tools, so duplicate assignments should be avoided. if(!(name in toolmap)) toolmap[name] = this; } Tool.prototype.setColor = function(color){ ctx.strokeStyle = color; }; function setColorFill(color){ ctx.fillStyle = color; } Tool.prototype.setWidth = function(width){ ctx.lineWidth = width; }; function nop(){} Tool.prototype.draw = nop; /// Append a point to the shape by mouse click Tool.prototype.appendPoint = function(x, y) { function addPoint(x, y){ if (cur_shape.points.length === cur_tool.points){ dhistory.push(cloneObject(dobjs)); dobjs.push(cur_shape); updateDrawData(); cur_shape = null; redraw(dobjs); return true; } else{ cur_shape.points.push(canvasToSrc(constrainCoord({x:x, y:y}))); return false; } } if(!cur_shape){ var obj = new cur_tool.objctor(); obj.tool = cur_tool.name; obj.color = cur_col; obj.width = cur_thin; cur_shape = obj; // Add two points for previewing shapes that consist of multiple points, // but single-point shapes will finish with just single click. if(!addPoint(x, y)) addPoint(x, y); } else{ addPoint(x, y); } } Tool.prototype.mouseDown = function(e){ // Do nothing by default } Tool.prototype.mouseMove = function(e){ if(cur_shape && 0 < cur_shape.points.length){ var clrect = canvas.getBoundingClientRect(); var mx = (gridEnable ? Math.round(e.clientX / gridSize) * gridSize : e.clientX) - clrect.left; var my = (gridEnable ? Math.round(e.clientY / gridSize) * gridSize : e.clientY) - clrect.top; // Live preview of the shape being added. var coord = {x: mx, y: my}; cur_shape.points[cur_shape.points.length-1] = canvasToSrc(constrainCoord(coord)); redraw(dobjs); } } Tool.prototype.mouseUp = function(e){ moving = false; sizing = null; pointMoving = null; var needsRedraw = boxselecting; boxselecting = false; if(needsRedraw) // Redraw to clear selection box redraw(dobjs); } Tool.prototype.keyDown = function(e){ var code = e.keyCode || e.which; if(code === 46){ // Delete key dhistory.push(cloneObject(dobjs)); // Push undo buffer // Delete all selected objects deleteShapes(selectobj); updateDrawData(); redraw(dobjs); } } // All other tools than the pathedit, draw scaling handles and // bounding boxes around selected objects. // Aside from the select tool, the user cannot grab the scaling handles, // but they're useful to visually appeal that the object is selected. Tool.prototype.selectDraw = function(shape){ var bounds = objBounds(shape); ctx.beginPath(); ctx.lineWidth = 1; ctx.strokeStyle = '#000'; ctx.setLineDash([5]); ctx.rect(bounds.minx, bounds.miny, bounds.maxx-bounds.minx, bounds.maxy-bounds.miny); ctx.stroke(); ctx.setLineDash([]); ctx.beginPath(); ctx.strokeStyle = '#000'; for(var i = 0; i < 8; i++){ var r = getHandleRect(bounds, i); ctx.fillStyle = sizing === shape && i === sizedir ? '#7fff7f' : '#ffff7f'; ctx.fillRect(r.minx, r.miny, r.maxx - r.minx, r.maxy-r.miny); ctx.rect(r.minx, r.miny, r.maxx - r.minx, r.maxy-r.miny); } ctx.stroke(); } // ==================== Tool class definition end ============================= // // List of tools in the toolbar. var toolbar = [ new Tool("select", 1, {drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y-5); ctx.lineTo(x, y+10); ctx.lineTo(x+4, y+7); ctx.lineTo(x+6, y+11); ctx.lineTo(x+8, y+9); ctx.lineTo(x+6, y+5); ctx.lineTo(x+10, y+3); ctx.closePath(); ctx.stroke(); ctx.strokeText('1', x+45, y+10); }, mouseDown: selectMouseDown, mouseMove: selectMouseMove, mouseUp: function(e){ if(0 < selectobj.length && (moving || sizing)) updateDrawData(); Tool.prototype.mouseUp.call(this, e); } }), new Tool("pathedit", 1, { // The path editing tool. This is especially useful when editing // existing curves. The icon is identical to that of select tool, // except that the mouse cursor graphic is filled. drawTool: function(x, y){ ctx.fillStyle = 'rgb(250, 250, 250)'; ctx.beginPath(); ctx.moveTo(x, y-5); ctx.lineTo(x, y+10); ctx.lineTo(x+4, y+7); ctx.lineTo(x+6, y+11); ctx.lineTo(x+8, y+9); ctx.lineTo(x+6, y+5); ctx.lineTo(x+10, y+3); ctx.closePath(); ctx.fill(); ctx.strokeText('1', x+45, y+10); }, mouseDown: pathEditMouseDown, mouseMove: pathEditMouseMove, mouseUp: function(e){ if(0 < selectobj.length && pointMoving) updateDrawData(); Tool.prototype.mouseUp.call(this, e); }, keyDown: function(e){ var code = e.keyCode || e.which; if(code === 46){ // Delete key // Pathedit tool's delete key just delete single vertex in a path if(this.selectPointShape && this.selectPointShape.tool === "path" && 0 <= this.selectPointIdx){ dhistory.push(cloneObject(dobjs)); // Push undo buffer if(this.selectPointShape.points.length <= 2){ deleteShapes([this.selectPointShape]); this.selectPointShape = null; } else this.selectPointShape.points.splice(this.selectPointIdx, 1); updateDrawData(); redraw(dobjs); } } }, selectDraw: pathEditSelectDraw, selectPointShape: null, // Default value for the variable selectPointIdx: -1, // Default value for the variable }), new Tool("line", 2, { drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x+40, y+10); ctx.stroke(); ctx.strokeText('2', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); ctx.moveTo(arr[0].x, arr[0].y); ctx.lineTo(arr[1].x, arr[1].y); ctx.stroke(); ctx.lineWidth = 1; }, }), new Tool("arrow", 2, { drawTool: function(x, y){ ctx.beginPath(); l_arrow(ctx, [{x:x, y:y+5}, {x:x+40, y:y+5}]); ctx.strokeText('2', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); l_arrow(ctx, arr); ctx.lineWidth = 1; } }), new Tool("barrow", 2, { drawTool: function(x, y){ ctx.beginPath(); l_tarrow(ctx, [{x:x, y:y+5}, {x:x+40, y:y+5}]); ctx.strokeText('2', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); l_tarrow(ctx, arr); ctx.lineWidth = 1; } }), new Tool("darrow", 2, { drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y+3); ctx.lineTo(x+39, y+3); ctx.moveTo(x, y+7); ctx.lineTo(x+39, y+7); ctx.moveTo(x+35, y); ctx.lineTo(x+40, y+5); ctx.lineTo(x+35, y+10); ctx.stroke(); ctx.strokeText('2', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); l_darrow(ctx, arr); ctx.lineWidth = 1; } }), new Tool("arc", 3, { isArc: true, drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.quadraticCurveTo(x+20, y+20, x+40, y); ctx.stroke(); ctx.strokeText('3', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); ctx.moveTo(arr[0].x, arr[0].y); ctx.quadraticCurveTo(arr[1].x, arr[1].y, arr[2].x, arr[2].y); ctx.stroke(); ctx.lineWidth = 1; } }), new Tool("arcarrow", 3, { isArc: true, drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.quadraticCurveTo(x+20, y+20, x+40, y); l_hige(ctx, [{x:x+20, y:y+20}, {x:x+40, y:y}]); ctx.strokeText('3', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); ctx.moveTo(arr[0].x, arr[0].y); ctx.quadraticCurveTo(arr[1].x, arr[1].y, arr[2].x, arr[2].y); l_hige(ctx, [arr[1], arr[2]]); ctx.lineWidth = 1; } }), new Tool("arcbarrow", 3, { isArc: true, drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.quadraticCurveTo(x+20, y+20, x+40, y); var a = [{x:x+20, y:y+20}, {x:x+40, y:y}]; l_hige(ctx, a); //a[0] = {x:x+10, y:y+10}; a[1] = {x:x, y:y}; l_hige(ctx, a); ctx.strokeText('3', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); ctx.moveTo(arr[0].x, arr[0].y); ctx.quadraticCurveTo(arr[1].x, arr[1].y, arr[2].x, arr[2].y); var a = new Array(2); a[0] = arr[1]; a[1] = arr[2]; l_hige(ctx, a); a[1] = arr[0]; l_hige(ctx, a); ctx.lineWidth = 1; } }), new Tool("rect", 2, { drawTool: function(x, y){ ctx.beginPath(); ctx.rect(x, y, 40, 10); ctx.stroke(); ctx.strokeText('2', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); ctx.rect(arr[0].x, arr[0].y, arr[1].x-arr[0].x, arr[1].y-arr[0].y); ctx.stroke(); ctx.lineWidth = 1; } }), new Tool("ellipse", 2, { drawTool: function(x, y){ ctx.beginPath(); ctx.scale(1.0, 0.5); // vertically half ctx.arc(x+20, (y+5)*2, 20, 0, 2 * Math.PI, false); ctx.stroke(); ctx.scale(1.0, 2.0); ctx.strokeText('2', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); l_elipse(ctx, arr); ctx.lineWidth = 1; } }), new Tool("rectfill", 2, { drawTool: function(x, y){ ctx.beginPath(); ctx.fillStyle = 'rgb(250, 250, 250)'; ctx.fillRect(x, y, 40, 10); ctx.strokeText('2', x+45, y+10); }, setColor: setColorFill, setWidth: nop, draw: function(obj){ var arr = obj.points; ctx.beginPath(); ctx.fillRect(arr[0].x, arr[0].y, arr[1].x-arr[0].x, arr[1].y-arr[0].y); } }), new Tool("ellipsefill", 2, {drawTool: function(x, y){ ctx.beginPath(); ctx.fillStyle = 'rgb(250, 250, 250)'; ctx.scale(1.0, 0.5); // vertically half ctx.arc(x+20, (y+5)*2, 20, 0, 2 * Math.PI, false); ctx.fill(); ctx.scale(1.0, 2.0); ctx.strokeText('2', x+45, y+10); }, setColor: setColorFill, setWidth: nop, draw: function(obj){ var arr = obj.points; ctx.beginPath(); l_elipsef(ctx, arr); ctx.lineWidth = 1; } }), new Tool("star", 1, {objctor: PointShape, drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x+8, y-3); ctx.lineTo(x+14, y+13); ctx.lineTo(x, y+2); ctx.lineTo(x+16, y+2); ctx.lineTo(x+2, y+13); ctx.closePath(); ctx.stroke(); ctx.strokeText('1', x+45, y+10); }, draw: function(obj){ var arr = obj.points; ctx.beginPath(); //ctx.lineWidth = cur_thin - 40; l_star(ctx, arr); ctx.lineWidth = 1; } }), new Tool("check", 1, {objctor: PointShape, drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x+5, y+7); ctx.lineTo(x+20, y); ctx.stroke(); ctx.strokeText('1', x+45, y+10); }, setWidth: nop, draw: function(obj){ var arr = obj.points; ctx.beginPath(); l_check(ctx, arr); } }), new Tool("text", 1, {objctor: TextShape, drawTool: function(x, y){ ctx.beginPath(); ctx.strokeText(i18n.t('Text'), x+3, y+10); ctx.strokeText('1', x+45, y+10); }, setColor: setColorFill, draw: function(obj){ var str = obj.text; if (null == str) { // cancel // idx = 0; return; } ctx.beginPath(); if (1 == obj.width) setFont(14); else if (2 == obj.width) setFont(16); else setFont(20); ctx.fillText(str, obj.points[0].x, obj.points[0].y); // Draw blue underline for linked text if(obj.link){ ctx.strokeStyle = '#0000ff'; ctx.beginPath(); ctx.moveTo(obj.points[0].x, obj.points[0].y + 4); ctx.lineTo(obj.points[0].x + ctx.measureText(str).width, obj.points[0].y + 4); ctx.stroke(); } ctx.font = setFont(14); }, appendPoint: function(x, y){ var textTool = this; // Show size input layer on top of the canvas because the canvas cannot have // a text input element. if(!textLayer){ textLayer = document.createElement('div'); // Create field for remembering position of text being inserted. // Free variables won't work well. textLayer.canvasPos = {x:0, y:0}; var lay = textLayer; lay.id = 'textLayer'; lay.style.position = 'absolute'; lay.style.padding = '5px 5px 5px 5px'; lay.style.borderStyle = 'solid'; lay.style.borderColor = '#cf0000'; lay.style.borderWidth = '2px'; // Drop shadow to make it distinguishable from the figure contents. lay.style.boxShadow = '0px 0px 20px grey'; lay.style.background = '#cfffcf'; // Create and assign the input element to a field of the textLayer object // to keep track of the input element after this function is exited. lay.textInput = document.createElement('input'); lay.textInput.id = "textinput"; lay.textInput.type = "text"; lay.textInput.style.width = "30em"; lay.textInput.onkeyup = function(e){ // Convert enter key event to OK button click if(e.keyCode === 13) okbutton.onclick(); }; lay.appendChild(lay.textInput); // Add a text area for link lay.appendChild(document.createElement('br')); lay.linkInput = document.createElement('input'); lay.linkInput.id = "linkinput"; lay.linkInput.type = "text"; lay.linkInput.onkeyup = lay.textInput.onkeyup; var linkdiv = document.createElement('div'); linkdiv.innerHTML = "Link:"; linkdiv.appendChild(lay.linkInput); lay.appendChild(linkdiv); var okbutton = document.createElement('input'); okbutton.type = 'button'; okbutton.value = 'OK'; okbutton.onclick = function(e){ lay.style.display = 'none'; // Ignore blank text if(lay.textInput.value == '') return; dhistory.push(cloneObject(dobjs)); // If a shape is clicked, alter its value instead of adding a new one. if(lay.dobj){ lay.dobj.text = lay.textInput.value; lay.dobj.link = lay.linkInput.value; } else{ var obj = new textTool.objctor(); obj.tool = cur_tool.name; obj.color = cur_col; obj.width = cur_thin; obj.points.push({x: lay.canvasPos.x, y: lay.canvasPos.y}); obj.text = lay.textInput.value; obj.link = lay.linkInput.value; dobjs.push(obj); } updateDrawData(); redraw(dobjs); } var cancelbutton = document.createElement('input'); cancelbutton.type = 'button'; cancelbutton.value = 'Cancel'; cancelbutton.onclick = function(s){ lay.style.display = 'none'; } // lay.appendChild(document.createElement('br')); // It seems to add an extra line break lay.appendChild(okbutton); lay.appendChild(cancelbutton); // Append as the body element's child because style.position = "absolute" would // screw up in deeply nested DOM tree (which may have a positioned ancestor). document.body.appendChild(lay); } else textLayer.style.display = 'block'; var coord = canvasToSrc(constrainCoord({x:x, y:y})); textLayer.canvasPos.x = coord.x; textLayer.canvasPos.y = coord.y; // Find if any TextShape is under the mouse cursor. textLayer.dobj = null; textLayer.textInput.value = ""; for (var i = 0; i < dobjs.length; i++) { if(dobjs[i] instanceof TextShape && hitRect(objBounds(dobjs[i], true), coord.x, coord.y)){ textLayer.dobj = dobjs[i]; // Remember the shape being clicked on. textLayer.textInput.value = dobjs[i].text; // Initialized the input buffer with the previous content. textLayer.linkInput.value = dobjs[i].link; break; } } // Adjust the text area's width so that two of them uses the div element's space efficiently. var textInputRect = textLayer.textInput.getBoundingClientRect(); var linkInputRect = textLayer.linkInput.getBoundingClientRect(); textLayer.linkInput.style.width = textInputRect.width - (linkInputRect.left - textInputRect.left) + "px"; var canvasRect = canvas.getBoundingClientRect(); // Cross-browser scroll position query var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; var scrollY = document.documentElement.scrollTop || document.body.scrollTop; // getBoundingClientRect() returns coordinates relative to view, which means we have to // add scroll position into them. textLayer.style.left = (canvasRect.left + scrollX + offset.x + coord.x) + 'px'; textLayer.style.top = (canvasRect.top + scrollY + offset.y + coord.y) + 'px'; // focus() should be called after textLayer is positioned, otherwise the page may // unexpectedly scroll to somewhere. textLayer.textInput.focus(); // Reset the point buffer // idx = 0; return true; // Skip registration } }) ]; var toolbars = [toolbar, [ toolmap.select, toolmap.pathedit, new Tool("delete", 1, { drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x+10, y+10); ctx.moveTo(x, y+10); ctx.lineTo(x+10, y); ctx.stroke(); ctx.strokeText('1', x+45, y+10); } }), new Tool("done", 1, {objctor: PointShape, drawTool: function(x, y){ ctx.beginPath(); ctx.strokeText(i18n.t('Done'), x+3, y+10); ctx.beginPath(); ctx.arc(x+9, y+5, 8, 0, 6.28, false); ctx.stroke(); ctx.strokeText('1', x+45, y+10); }, setWidth: nop, draw: function(obj){ var arr = obj.points; ctx.beginPath(); l_complete(ctx, arr); } }), new Tool("path", 2, { objctor: PathShape, drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.bezierCurveTo(x+10, y+20, x+20, y-10, x+30, y+10); ctx.stroke(); ctx.strokeText('n', x+45, y+10); }, draw: function(obj){ var arr = obj.points; if(arr.length < 2) return; ctx.beginPath(); ctx.moveTo(arr[0].x, arr[0].y); for(var i = 1; i < arr.length; i++){ if("cx" in arr[i] && "cy" in arr[i]){ if("dx" in arr[i] && "dy" in arr[i]) ctx.bezierCurveTo(arr[i].cx, arr[i].cy, arr[i].dx, arr[i].dy, arr[i].x, arr[i].y); else ctx.bezierCurveTo(arr[i].cx, arr[i].cy, arr[i].x, arr[i].y, arr[i].x, arr[i].y); } else if("dx" in arr[i] && "dy" in arr[i]) ctx.bezierCurveTo(arr[i-1].x, arr[i-1].y, arr[i].dx, arr[i].dy, arr[i].x, arr[i].y); else ctx.lineTo(arr[i].x, arr[i].y); } ctx.stroke(); if("arrow" in obj && "head" in obj.arrow && 1 < arr.length){ var first = arr[0], first2 = arr[1], a = []; if("cx" in first2 && "cy" in first2 && (first2.cx !== first.x || first2.cy !== first.y)) a[0] = {x: first2.cx, y: first2.cy}; else if("dx" in first2 && "dy" in first2 && (first2.dx !== first.x || first2.dy !== first.y)) a[0] = {x: first2.dx, y: first2.dy}; else a[0] = first2; a[1] = first; ctx.beginPath(); l_hige(ctx, a); } if("arrow" in obj && "tail" in obj.arrow && 1 < arr.length){ var last = arr[arr.length-1], last2 = arr[arr.length-2], a = []; if("dx" in last && "dy" in last && (last.dx !== last.x || last.dy !== last.y)) a[0] = {x: last.dx, y: last.dy}; else if("cx" in last && "cy" in last && (last.cx !== last.x || last.cy !== last.y)) a[0] = {x: last.cx, y: last.cy}; else a[0] = last2; a[1] = last; ctx.beginPath(); l_hige(ctx, a); } ctx.lineWidth = 1; // Draws dashed line that connects a control point and its associated vertex // This one does not take offset into account since the transformation is done // before this function. function drawGuidingLineNoOffset(pt0, pt, name){ ctx.setLineDash([5]); ctx.beginPath(); ctx.moveTo(pt0.x, pt0.y); ctx.lineTo(pt[name + "x"], pt[name + "y"]); ctx.stroke(); ctx.setLineDash([]); } if(obj === cur_shape){ var last = arr[arr.length-1]; drawGuidingLineNoOffset(last ,last, "d"); drawHandle(last.dx, last.dy, "#ff7f7f", true); var next = {x: 2 * last.x - last.dx, y: 2 * last.y - last.dy}; drawGuidingLineNoOffset(last, next, ""); drawHandle(next.x, next.y, "#ff7f7f", true); } }, onNewShape: function(shape){}, /// Virtual event handler on creation of a new shape appendPoint: function(x, y){ function addPoint(){ var pts = cur_shape.points; pts.push(cloneObject(d)); } var d = canvasToSrc(constrainCoord({x:x, y:y})); if(!cur_shape){ var obj = new cur_tool.objctor(); obj.tool = cur_tool.name; obj.color = cur_col; obj.width = cur_thin; this.onNewShape(obj); cur_shape = obj; addPoint(); addPoint(); } else{ var pts = cur_shape.points; if(pts.length !== 1){ var prev = pts[pts.length-2]; if(d.x === prev.x && d.y === prev.y){ cur_shape.points.pop(); dhistory.push(cloneObject(dobjs)); dobjs.push(cur_shape); updateDrawData(); cur_shape = null; redraw(dobjs); return true; } } if(0 < pts.length){ var prev = pts[pts.length-1]; // Mirror position of control point for smooth curve if("dx" in prev && "dy" in prev){ d.cx = 2 * prev.x - prev.dx; d.cy = 2 * prev.y - prev.dy; } } addPoint(); } }, mouseDown: function(e){ if(cur_shape){ var clrect = canvas.getBoundingClientRect(); var mx = e.clientX - clrect.left; var my = e.clientY - clrect.top; // Remember mouse stat to this (Path tool) object for later use // in mouseMove(). this.lastx = mx; this.lasty = my; this.lastPoint = cur_shape.points[cur_shape.points.length-1]; } }, mouseMove: function(e){ if(!cur_shape) return; var clrect = canvas.getBoundingClientRect(); var mx = e.clientX - clrect.left; var my = e.clientY - clrect.top; if(this.lastPoint){ var pt = this.lastPoint; var d = canvasToSrc(constrainCoord({x:mx, y:my})); if(0 < cur_shape.points.length && (pt.x !== d.x && pt.y !== d.y)){ var prev = cur_shape.points[cur_shape.points.length-2]; // Extend control point by mouse dragging. pt.dx = 2 * pt.x - d.x; pt.dy = 2 * pt.y - d.y; } redraw(dobjs); } else if(0 < cur_shape.points.length){ // Live preview of the shape being added. var coord = canvasToSrc(constrainCoord({x: mx, y: my})); var pt = cur_shape.points[cur_shape.points.length-1]; var dx = coord.x - pt.x; var dy = coord.y - pt.y; pt.x = coord.x; pt.y = coord.y; // Only move dx and dy along with x, y. // cx and cy should be moved with the previous vertex. if("dx" in pt && "dy" in pt) pt.dx += dx, pt.dy += dy; redraw(dobjs); } }, mouseUp: function(e){ this.lastPoint = null; }, }), new Tool("path", 2, { objctor: PathShape, drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.bezierCurveTo(x+10, y+20, x+20, y-10, x+30, y+10); ctx.stroke(); l_hige(ctx, [{x: x+20, y: y-10}, {x: x+30, y: y+10}]); ctx.strokeText('n', x+45, y+10); }, draw: toolmap.path.draw, onNewShape: function(shape){shape.arrow = {"tail": null}}, appendPoint: toolmap.path.appendPoint, mouseDown: toolmap.path.mouseDown, mouseMove: toolmap.path.mouseMove, mouseUp: toolmap.path.mouseUp, }), new Tool("path", 2, { objctor: PathShape, drawTool: function(x, y){ ctx.beginPath(); ctx.moveTo(x, y); ctx.bezierCurveTo(x+10, y+20, x+20, y-10, x+30, y+10); ctx.stroke(); l_hige(ctx, [{x: x+10, y: y+20}, {x: x, y: y}]); l_hige(ctx, [{x: x+20, y: y-10}, {x: x+30, y: y+10}]); ctx.strokeText('n', x+45, y+10); }, draw: toolmap.path.draw, onNewShape: function(shape){shape.arrow = {head: null, "tail": null}}, appendPoint: toolmap.path.appendPoint, mouseDown: toolmap.path.mouseDown, mouseMove: toolmap.path.mouseMove, mouseUp: toolmap.path.mouseUp, }) ] ]; var toolbarPage = 0; var white = "rgb(255, 255, 255)"; var black = "rgb(0, 0, 0)"; var blue = "rgb(0, 100, 255)"; var green = "rgb(0, 255, 0)"; var red = "rgb(255, 0, 0)"; var gray = "rgb(150, 150, 150)"; var colstr = new Array(black,blue,red,green,white); var colnames = ["black", "blue", "red", "green", "white"]; var coltable = {"black": black, "blue": blue, "red": red, "green": green, "white": white}; var x0 = 0, y0 = 0, w0 = 1024, h0 = 640; var x1 = 90, y1 = 50, w1 = 930, h1 = 580; var mx0 = 10, mx1 = x1, mx2 = 600, mx3 = 820; var mw0 = 70, mw1 = 60, mw2 = 30, my0 = 20, mh0 = 28; var cur_tool = toolmap.select, cur_col = "black", cur_thin = 1; var cur_shape = null; var offset = editmode ? {x:x1, y:y1} : {x:0, y:0}; // The layer to show input controls for width and height sizes of the figure. // It's kept as a member variable in order to reuse in the second and later invocations. var sizeLayer = null; // The layer to input text. // It used to be prompt() function, but it's not beautiful. var textLayer = null; // The default metaObj values used for resetting. var defaultMetaObj = {type: "meta", size: [1024-x1, 640-y1]}; // The meta object is always the first element in the serialized figure text, // but is not an element of dobjs array. // It's automatically loaded when deserialized and included when serialized. var metaObj = cloneObject(defaultMetaObj); // A pseudo-this pointer that can be used in private methods. // Private methods mean local functions in this constructor, which // don't share this pointer with the constructor itself unless the // form "function.call(this)" is used. var self = this; onload(); } // Create and return a XMLHttpRequest object or ActiveXObject for IE6- SketchCanvas.prototype.createXMLHttpRequest = function(){ var xmlHttp = null; try{ // for IE7+, Fireforx, Chrome, Opera, Safari xmlHttp = new XMLHttpRequest(); } catch(e){ try{ // for IE6, IE5 (canvas element wouldn't work from the start, though) xmlHttp = new ActiveXObject("Msxml2.XMLHTTP"); } catch(e){ try{ xmlHttp = new ActiveXObject("Microsoft.XMLHttp"); } catch(e){ return null; } } } return xmlHttp; }