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