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