1 /* 2 * Copyright (C) 2008, 2009 Mihai Şucan 3 * 4 * This file is part of PaintWeb. 5 * 6 * PaintWeb is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * PaintWeb is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>. 18 * 19 * $URL: http://code.google.com/p/paintweb $ 20 * $Date: 2009-07-02 16:07:14 +0300 $ 21 */ 22 23 /** 24 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a> 25 * @fileOverview Holds the selection tool implementation. 26 */ 27 28 /** 29 * @class The selection tool. 30 * 31 * @param {PaintWeb} app Reference to the main paint application object. 32 */ 33 pwlib.tools.selection = function (app) { 34 var _self = this, 35 appEvent = pwlib.appEvent, 36 bufferContext = app.buffer.context, 37 clearInterval = app.win.clearInterval, 38 config = app.config.selection, 39 gui = app.gui, 40 image = app.image, 41 lang = app.lang, 42 layerCanvas = app.layer.canvas, 43 layerContext = app.layer.context, 44 marqueeStyle = null, 45 MathAbs = Math.abs, 46 MathMin = Math.min, 47 MathRound = Math.round, 48 mouse = app.mouse, 49 setInterval = app.win.setInterval, 50 snapXY = app.toolSnapXY; 51 52 /** 53 * The interval ID used for invoking the drawing operation every few 54 * milliseconds. 55 * 56 * @private 57 * @see PaintWeb.config.toolDrawDelay 58 */ 59 var timer = null; 60 61 /** 62 * Tells if the drawing canvas needs to be updated or not. 63 * 64 * @private 65 * @type Boolean 66 * @default false 67 */ 68 var needsRedraw = false; 69 70 /** 71 * The selection has been dropped, and the mouse button is down. The user has 72 * two choices: he releases the mouse button, thus the selection is dropped 73 * and the tool switches to STATE_NONE, or he moves the mouse in order to 74 * start a new selection (STATE_DRAWING). 75 * @constant 76 */ 77 this.STATE_PENDING = -1; 78 79 /** 80 * No selection is available. 81 * @constant 82 */ 83 this.STATE_NONE = 0; 84 85 /** 86 * The user is drawing a selection. 87 * @constant 88 */ 89 this.STATE_DRAWING = 1; 90 91 /** 92 * The selection rectangle is available. 93 * @constant 94 */ 95 this.STATE_SELECTED = 2; 96 97 /** 98 * The user is dragging/moving the selection rectangle. 99 * @constant 100 */ 101 this.STATE_DRAGGING = 3; 102 103 /** 104 * The user is resizing the selection rectangle. 105 * @constant 106 */ 107 this.STATE_RESIZING = 4; 108 109 /** 110 * Selection state. Known states: 111 * 112 * <ul> 113 * <li>{@link pwlib.tools.selection#STATE_PENDING} - Selection dropped after 114 * the <code>mousedown</code> event is fired. The script can switch to 115 * STATE_DRAWING if the mouse moves, or to STATE_NONE if it does not 116 * (allowing the user to drop the selection). 117 * 118 * <li>{@link pwlib.tools.selection#STATE_NONE} - No selection is available. 119 * 120 * <li>{@link pwlib.tools.selection#STATE_DRAWING} - The user is drawing the 121 * selection rectangle. 122 * 123 * <li>{@link pwlib.tools.selection#STATE_SELECTED} - The selection 124 * rectangle is available. 125 * 126 * <li>{@link pwlib.tools.selection#STATE_DRAGGING} - The user is 127 * dragging/moving the current selection. 128 * 129 * <li>{@link pwlib.tools.selection#STATE_RESIZING} - The user is resizing 130 * the current selection. 131 * </ul> 132 * 133 * @type Number 134 * @default STATE_NONE 135 */ 136 this.state = this.STATE_NONE; 137 138 /** 139 * Holds the starting point on the <var>x</var> axis of the image, for any 140 * ongoing operation. 141 * 142 * @private 143 * @type Number 144 */ 145 var x0 = 0; 146 147 /** 148 * Holds the starting point on the <var>y</var> axis of the image, for the any 149 * ongoing operation. 150 * 151 * @private 152 * @type Number 153 */ 154 var y0 = 0; 155 156 /** 157 * Holds selection information and image. 158 * @type Object 159 */ 160 this.selection = { 161 /** 162 * Selection start point, on the <var>x</var> axis. 163 * @type Number 164 */ 165 x: 0, 166 167 /** 168 * Selection start point, on the <var>y</var> axis. 169 * @type Number 170 */ 171 y: 0, 172 173 /** 174 * Selection width. 175 * @type Number 176 */ 177 width: 0, 178 179 /** 180 * Selection height. 181 * @type Number 182 */ 183 height: 0, 184 185 /** 186 * Selection original width. The user can make a selection rectangle of 187 * a given width and height, but after that he/she can resize the selection. 188 * @type Number 189 */ 190 widthOriginal: 0, 191 192 /** 193 * Selection original height. The user can make a selection rectangle of 194 * a given width and height, but after that he/she can resize the selection. 195 * @type Number 196 */ 197 heightOriginal: 0, 198 199 /** 200 * Tells if the selected ImageData has been cut out or not from the 201 * layerContext. 202 * 203 * @type Boolean 204 * @default false 205 */ 206 layerCleared: false, 207 208 /** 209 * Selection marquee/border element. 210 * @type HTMLElement 211 */ 212 marquee: null, 213 214 /** 215 * Selection buffer context which holds the selected pixels. 216 * @type CanvasRenderingContext2D 217 */ 218 context: null, 219 220 /** 221 * Selection buffer canvas which holds the selected pixels. 222 * @type HTMLCanvasElement 223 */ 224 canvas: null 225 }; 226 227 /** 228 * The area type under the current mouse location. 229 * 230 * <p>When the selection is available the mouse location can be on top/inside 231 * the selection rectangle, on the border of the selection, or outside the 232 * selection. 233 * 234 * <p>Possible values: 'in', 'out', 'border'. 235 * 236 * @private 237 * @type String 238 * @default 'out' 239 */ 240 var mouseArea = 'out'; 241 242 /** 243 * The resize type. If the mouse is on top of the selection border, then the 244 * selection can be resized. The direction of the resize operation is 245 * determined by the location of the mouse. 246 * 247 * <p>While the user resizes the selection this variable can hold the 248 * following values: 'n' (North), 'ne' (North-East), 'e' (East), 'se' 249 * (South-East), 's' (South), 'sw' (South-West), 'w' (West), 'nw' 250 * (North-West). 251 * 252 * @private 253 * @type String 254 * @default null 255 */ 256 var mouseResize = null; 257 258 // shorthands / private variables 259 var sel = this.selection, 260 borderDouble = config.borderWidth * 2, 261 ev_canvasSizeChangeId = null, 262 ev_configChangeId = null, 263 ctrlKey = false, 264 shiftKey = false; 265 266 /** 267 * The last selection rectangle that was drawn. This is used by the selection 268 * drawing functions. 269 * 270 * @private 271 * @type Object 272 */ 273 // We avoid retrieving the mouse coordinates during the mouseup event, due to 274 // the Opera bug DSK-232264. 275 var lastSel = null; 276 277 /** 278 * The tool preactivation code. This function prepares the selection canvas 279 * element. 280 * 281 * @returns {Boolean} True if the activation did not fail, or false otherwise. 282 * If false is returned, the selection tool cannot be activated. 283 */ 284 this.preActivate = function () { 285 if (!('canvasContainer' in gui.elems)) { 286 alert(lang.errorToolActivate); 287 return false; 288 } 289 290 // The selection image buffer. 291 sel.canvas = app.doc.createElement('canvas'); 292 if (!sel.canvas) { 293 alert(lang.errorToolActivate); 294 return false; 295 } 296 297 sel.canvas.width = 5; 298 sel.canvas.height = 5; 299 300 sel.context = sel.canvas.getContext('2d'); 301 if (!sel.context) { 302 alert(lang.errorToolActivate); 303 return false; 304 } 305 306 sel.marquee = app.doc.createElement('div'); 307 if (!sel.marquee) { 308 alert(lang.errorToolActivate); 309 return false; 310 } 311 sel.marquee.className = gui.classPrefix + 'selectionMarquee'; 312 marqueeStyle = sel.marquee.style; 313 314 return true; 315 }; 316 317 /** 318 * The tool activation code. This method sets-up multiple event listeners for 319 * several target objects. 320 */ 321 this.activate = function () { 322 // Older browsers do not support get/putImageData, thus non-transparent 323 // selections cannot be used. 324 if (!layerContext.putImageData || !layerContext.getImageData) { 325 config.transparent = true; 326 } 327 328 marqueeHide(); 329 330 marqueeStyle.borderWidth = config.borderWidth + 'px'; 331 sel.marquee.addEventListener('mousedown', marqueeMousedown, false); 332 sel.marquee.addEventListener('mousemove', marqueeMousemove, false); 333 sel.marquee.addEventListener('mouseup', marqueeMouseup, false); 334 335 gui.elems.canvasContainer.appendChild(sel.marquee); 336 337 // Disable the Canvas shadow. 338 app.shadowDisallow(); 339 340 // Application event listeners. 341 ev_canvasSizeChangeId = app.events.add('canvasSizeChange', 342 ev_canvasSizeChange); 343 ev_configChangeId = app.events.add('configChange', ev_configChange); 344 345 // Register selection-related commands 346 app.commandRegister('selectionCrop', _self.selectionCrop); 347 app.commandRegister('selectionDelete', _self.selectionDelete); 348 app.commandRegister('selectionFill', _self.selectionFill); 349 350 if (!timer) { 351 timer = setInterval(timerFn, app.config.toolDrawDelay); 352 } 353 354 return true; 355 }; 356 357 /** 358 * The tool deactivation code. This removes all event listeners and cleans up 359 * the document. 360 */ 361 this.deactivate = function () { 362 if (timer) { 363 clearInterval(timer); 364 timer = null; 365 } 366 367 _self.selectionMerge(); 368 369 sel.marquee.removeEventListener('mousedown', marqueeMousedown, false); 370 sel.marquee.removeEventListener('mousemove', marqueeMousemove, false); 371 sel.marquee.removeEventListener('mouseup', marqueeMouseup, false); 372 373 marqueeStyle = null; 374 gui.elems.canvasContainer.removeChild(sel.marquee); 375 376 delete sel.context, sel.canvas, sel.marquee; 377 378 // Re-enable canvas shadow. 379 app.shadowAllow(); 380 381 // Remove the application event listeners. 382 if (ev_canvasSizeChangeId) { 383 app.events.remove('canvasSizeChange', ev_canvasSizeChangeId); 384 } 385 if (ev_configChangeId) { 386 app.events.remove('configChange', ev_configChangeId); 387 } 388 389 // Unregister selection-related commands 390 app.commandUnregister('selectionCrop'); 391 app.commandUnregister('selectionDelete'); 392 app.commandUnregister('selectionFill'); 393 394 return true; 395 }; 396 397 /** 398 * The <code>mousedown</code> event handler. Depending on the mouse location, 399 * this method does initiate different selection operations: drawing, 400 * dropping, dragging or resizing. 401 * 402 * <p>Hold the <kbd>Control</kbd> key down to temporarily toggle the 403 * transformation mode. 404 * 405 * @param {Event} ev The DOM Event object. 406 */ 407 this.mousedown = function (ev) { 408 if (_self.state !== _self.STATE_NONE && 409 _self.state !== _self.STATE_SELECTED) { 410 return false; 411 } 412 413 // Update the current mouse position, this is used as the start position for most of the operations. 414 x0 = mouse.x; 415 y0 = mouse.y; 416 417 shiftKey = ev.shiftKey; 418 ctrlKey = ev.ctrlKey; 419 lastSel = null; 420 421 // No selection is available, then start drawing a selection. 422 if (_self.state === _self.STATE_NONE) { 423 _self.state = _self.STATE_DRAWING; 424 marqueeStyle.display = ''; 425 gui.statusShow('selectionDraw'); 426 427 return true; 428 } 429 430 // STATE_SELECTED: selection available. 431 mouseAreaUpdate(); 432 433 /* 434 * Check if the user clicked outside the selection: drop the selection, 435 * switch to STATE_PENDING, clear the image buffer and put the current 436 * selection buffer in the image layer. 437 * 438 * If the user moves the mouse without taking the finger off the mouse 439 * button, then a new selection rectangle will start to be drawn: the script 440 * will switch to STATE_DRAWING. 441 * 442 * If the user simply takes the finger off the mouse button (mouseup), then 443 * the script will switch to STATE_NONE (no selection available). 444 */ 445 switch (mouseArea) { 446 case 'out': 447 _self.state = _self.STATE_PENDING; 448 marqueeHide(); 449 gui.statusShow('selectionActive'); 450 selectionMergeStrict(); 451 452 return true; 453 454 case 'in': 455 // The mouse area: 'in' for drag. 456 _self.state = _self.STATE_DRAGGING; 457 gui.statusShow('selectionDrag'); 458 break; 459 460 case 'border': 461 // 'border' for resize (the user is clicking on the borders). 462 _self.state = _self.STATE_RESIZING; 463 gui.statusShow('selectionResize'); 464 } 465 466 // Temporarily toggle the transformation mode if the user holds the Control 467 // key down. 468 if (ev.ctrlKey) { 469 config.transform = !config.transform; 470 } 471 472 // If there's any ImageData currently in memory, which was "cut" out from 473 // the current layer, then put it back on the layer. This needs to be done 474 // only when the selection.transform mode is not active - that's when the 475 // drag/resize operation only changes the selection, not the pixels 476 // themselves. 477 if (sel.layerCleared && !config.transform) { 478 selectionMergeStrict(); 479 480 } else if (!sel.layerCleared && config.transform) { 481 // When the user starts dragging/resizing the ImageData we must cut out 482 // the current selection from the image layer. 483 selectionBufferInit(); 484 } 485 486 return true; 487 }; 488 489 /** 490 * The <code>mousemove</code> event handler. 491 * 492 * @param {Event} ev The DOM Event object. 493 */ 494 this.mousemove = function (ev) { 495 shiftKey = ev.shiftKey; 496 needsRedraw = true; 497 }; 498 499 /** 500 * The timer function. When the mouse button is down, this method performs the 501 * dragging/resizing operation. When the mouse button is not down, this method 502 * simply tracks the mouse location for the purpose of determining the area 503 * being pointed at: the selection, the borders, or if the mouse is outside 504 * the selection. 505 * @private 506 */ 507 function timerFn () { 508 if (!needsRedraw) { 509 return; 510 } 511 512 switch (_self.state) { 513 case _self.STATE_PENDING: 514 // selection dropped, switch to draw selection 515 _self.state = _self.STATE_DRAWING; 516 marqueeStyle.display = ''; 517 gui.statusShow('selectionDraw'); 518 519 case _self.STATE_DRAWING: 520 selectionDraw(); 521 break; 522 523 case _self.STATE_SELECTED: 524 mouseAreaUpdate(); 525 break; 526 527 case _self.STATE_DRAGGING: 528 selectionDrag(); 529 break; 530 531 case _self.STATE_RESIZING: 532 selectionResize(); 533 } 534 535 needsRedraw = false; 536 }; 537 538 /** 539 * The <code>mouseup</code> event handler. This method ends any selection 540 * operation. 541 * 542 * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 543 * application event when the selection state is changed or when the selection 544 * size/location is updated. 545 * 546 * @param {Event} ev The DOM Event object. 547 */ 548 this.mouseup = function (ev) { 549 // Allow click+mousemove+click, not only mousedown+move+up 550 if (_self.state !== _self.STATE_PENDING && 551 mouse.x === x0 && mouse.y === y0) { 552 return true; 553 } 554 555 needsRedraw = false; 556 557 shiftKey = ev.shiftKey; 558 if (ctrlKey) { 559 config.transform = !config.transform; 560 } 561 562 if (_self.state === _self.STATE_PENDING) { 563 // Selection dropped? If yes, switch to the no selection state. 564 _self.state = _self.STATE_NONE; 565 app.events.dispatch(new appEvent.selectionChange(_self.state)); 566 567 return true; 568 569 } else if (!lastSel) { 570 _self.state = _self.STATE_NONE; 571 marqueeHide(); 572 gui.statusShow('selectionActive'); 573 app.events.dispatch(new appEvent.selectionChange(_self.state)); 574 575 return true; 576 } 577 578 sel.x = lastSel.x; 579 sel.y = lastSel.y; 580 581 if ('width' in lastSel) { 582 sel.width = lastSel.width; 583 sel.height = lastSel.height; 584 } 585 586 _self.state = _self.STATE_SELECTED; 587 588 app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 589 sel.width, sel.height)); 590 591 gui.statusShow('selectionAvailable'); 592 593 return true; 594 }; 595 596 /** 597 * The <code>mousedown</code> event handler for the selection marquee element. 598 * 599 * @private 600 * @param {Event} ev The DOM Event object. 601 */ 602 function marqueeMousedown (ev) { 603 if (mouse.buttonDown) { 604 return; 605 } 606 mouse.buttonDown = true; 607 608 ev.preventDefault(); 609 610 _self.mousedown(ev); 611 }; 612 613 /** 614 * The <code>mousemove</code> event handler for the selection marquee element. 615 * 616 * @private 617 * @param {Event} ev The DOM Event object. 618 */ 619 function marqueeMousemove (ev) { 620 if ('layerX' in ev) { 621 mouse.x = MathRound((this.offsetLeft + ev.layerX) / image.canvasScale); 622 mouse.y = MathRound((this.offsetTop + ev.layerY) / image.canvasScale); 623 } else if ('offsetX' in ev) { 624 mouse.x = MathRound((this.offsetLeft + ev.offsetX) / image.canvasScale); 625 mouse.y = MathRound((this.offsetTop + ev.offsetY) / image.canvasScale); 626 } 627 628 shiftKey = ev.shiftKey; 629 needsRedraw = true; 630 }; 631 632 /** 633 * The <code>mouseup</code> event handler for the selection marquee element. 634 * 635 * @private 636 * @param {Event} ev The DOM Event object. 637 */ 638 function marqueeMouseup (ev) { 639 if (!mouse.buttonDown) { 640 return; 641 } 642 mouse.buttonDown = false; 643 644 ev.preventDefault(); 645 646 _self.mouseup(ev); 647 }; 648 649 /** 650 * Hide the selection marquee element. 651 * @private 652 */ 653 function marqueeHide () { 654 marqueeStyle.display = 'none'; 655 marqueeStyle.top = '-' + (borderDouble + 50) + 'px'; 656 marqueeStyle.left = '-' + (borderDouble + 50) + 'px'; 657 marqueeStyle.width = '1px'; 658 marqueeStyle.height = '1px'; 659 marqueeStyle.cursor = ''; 660 }; 661 662 /** 663 * Perform the selection rectangle drawing operation. 664 * 665 * @private 666 */ 667 function selectionDraw () { 668 var x = MathMin(mouse.x, x0), 669 y = MathMin(mouse.y, y0), 670 w = MathAbs(mouse.x - x0), 671 h = MathAbs(mouse.y - y0); 672 673 // Constrain the shape to a square. 674 if (shiftKey) { 675 if (w > h) { 676 if (y === mouse.y) { 677 y -= w-h; 678 } 679 h = w; 680 } else { 681 if (x === mouse.x) { 682 x -= h-w; 683 } 684 w = h; 685 } 686 } 687 688 var mw = w * image.canvasScale - borderDouble, 689 mh = h * image.canvasScale - borderDouble; 690 691 if (mw < 1 || mh < 1) { 692 lastSel = null; 693 return; 694 } 695 696 marqueeStyle.top = (y * image.canvasScale) + 'px'; 697 marqueeStyle.left = (x * image.canvasScale) + 'px'; 698 marqueeStyle.width = mw + 'px'; 699 marqueeStyle.height = mh + 'px'; 700 701 lastSel = {'x': x, 'y': y, 'width': w, 'height': h}; 702 }; 703 704 /** 705 * Perform the selection drag operation. 706 * 707 * @private 708 * 709 * @returns {false|Array} False is returned if the selection is too small, 710 * otherwise an array of two elements is returned. The array holds the 711 * selection coordinates, x and y. 712 */ 713 function selectionDrag () { 714 // Snapping on the X/Y axis 715 if (shiftKey) { 716 snapXY(x0, y0); 717 } 718 719 var x = sel.x + mouse.x - x0, 720 y = sel.y + mouse.y - y0; 721 722 // Dragging the ImageData 723 if (config.transform) { 724 bufferContext.clearRect(0, 0, image.width, image.height); 725 726 if (!config.transparent) { 727 bufferContext.fillRect(x, y, sel.width, sel.height); 728 } 729 730 // Parameters: 731 // source image, dest x, dest y, dest width, dest height 732 bufferContext.drawImage(sel.canvas, x, y, sel.width, sel.height); 733 } 734 735 marqueeStyle.top = (y * image.canvasScale) + 'px'; 736 marqueeStyle.left = (x * image.canvasScale) + 'px'; 737 738 lastSel = {'x': x, 'y': y}; 739 }; 740 741 /** 742 * Perform the selection resize operation. 743 * 744 * @private 745 * 746 * @returns {false|Array} False is returned if the selection is too small, 747 * otherwise an array of four elements is returned. The array holds the 748 * selection information: x, y, width and height. 749 */ 750 function selectionResize () { 751 var diffx = mouse.x - x0, 752 diffy = mouse.y - y0, 753 x = sel.x, 754 y = sel.y, 755 w = sel.width, 756 h = sel.height; 757 758 switch (mouseResize) { 759 case 'nw': 760 x += diffx; 761 y += diffy; 762 w -= diffx; 763 h -= diffy; 764 break; 765 case 'n': 766 y += diffy; 767 h -= diffy; 768 break; 769 case 'ne': 770 y += diffy; 771 w += diffx; 772 h -= diffy; 773 break; 774 case 'e': 775 w += diffx; 776 break; 777 case 'se': 778 w += diffx; 779 h += diffy; 780 break; 781 case 's': 782 h += diffy; 783 break; 784 case 'sw': 785 x += diffx; 786 w -= diffx; 787 h += diffy; 788 break; 789 case 'w': 790 x += diffx; 791 w -= diffx; 792 break; 793 default: 794 lastSel = null; 795 return; 796 } 797 798 if (!w || !h) { 799 lastSel = null; 800 return; 801 } 802 803 // Constrain the rectangle to have the same aspect ratio as the initial 804 // rectangle. 805 if (shiftKey) { 806 var p = sel.width / sel.height, 807 w2 = w, 808 h2 = h; 809 810 switch (mouseResize.charAt(0)) { 811 case 'n': 812 case 's': 813 w2 = MathRound(h*p); 814 break; 815 default: 816 h2 = MathRound(w/p); 817 } 818 819 switch (mouseResize) { 820 case 'nw': 821 case 'sw': 822 x -= w2 - w; 823 y -= h2 - h; 824 } 825 826 w = w2; 827 h = h2; 828 } 829 830 if (w < 0) { 831 x += w; 832 w *= -1; 833 } 834 if (h < 0) { 835 y += h; 836 h *= -1; 837 } 838 839 var mw = w * image.canvasScale - borderDouble, 840 mh = h * image.canvasScale - borderDouble; 841 842 if (mw < 1 || mh < 1) { 843 lastSel = null; 844 return; 845 } 846 847 // Resizing the ImageData 848 if (config.transform) { 849 bufferContext.clearRect(0, 0, image.width, image.height); 850 851 if (!config.transparent) { 852 bufferContext.fillRect(x, y, w, h); 853 } 854 855 // Parameters: 856 // source image, dest x, dest y, dest width, dest height 857 bufferContext.drawImage(sel.canvas, x, y, w, h); 858 } 859 860 marqueeStyle.top = (y * image.canvasScale) + 'px'; 861 marqueeStyle.left = (x * image.canvasScale) + 'px'; 862 marqueeStyle.width = mw + 'px'; 863 marqueeStyle.height = mh + 'px'; 864 865 lastSel = {'x': x, 'y': y, 'width': w, 'height': h}; 866 }; 867 868 /** 869 * Determine the are where the mouse is located: if it is inside or outside of 870 * the selection rectangle, or on the selection border. 871 * @private 872 */ 873 function mouseAreaUpdate () { 874 var border = config.borderWidth / image.canvasScale, 875 cursor = '', 876 x1_out = sel.x + sel.width, 877 y1_out = sel.y + sel.height, 878 x1_in = x1_out - border, 879 y1_in = y1_out - border, 880 x0_out = sel.x, 881 y0_out = sel.y, 882 x0_in = sel.x + border, 883 y0_in = sel.y + border; 884 885 mouseArea = 'out'; 886 887 // Inside the rectangle 888 if (mouse.x < x1_in && mouse.y < y1_in && 889 mouse.x > x0_in && mouse.y > y0_in) { 890 cursor = 'move'; 891 mouseArea = 'in'; 892 893 } else { 894 // On one of the borders (north/south) 895 if (mouse.x >= x0_out && mouse.x <= x1_out && 896 mouse.y >= y0_out && mouse.y <= y0_in) { 897 cursor = 'n'; 898 899 } else if (mouse.x >= x0_out && mouse.x <= x1_out && 900 mouse.y >= y1_in && mouse.y <= y1_out) { 901 cursor = 's'; 902 } 903 904 // West/east 905 if (mouse.y >= y0_out && mouse.y <= y1_out && 906 mouse.x >= x0_out && mouse.x <= x0_in) { 907 cursor += 'w'; 908 909 } else if (mouse.y >= y0_out && mouse.y <= y1_out && 910 mouse.x >= x1_in && mouse.x <= x1_out) { 911 cursor += 'e'; 912 } 913 914 if (cursor !== '') { 915 mouseResize = cursor; 916 cursor += '-resize'; 917 mouseArea = 'border'; 918 } 919 } 920 921 // Due to bug 126457 Opera will not automatically update the cursor, 922 // therefore they will not see any visual feedback. 923 if (cursor !== marqueeStyle.cursor) { 924 marqueeStyle.cursor = cursor; 925 } 926 }; 927 928 /** 929 * The <code>canvasSizeChange</code> application event handler. This method 930 * makes sure the selection size stays in sync. 931 * 932 * @private 933 * @param {pwlib.appEvent.canvasSizeChange} ev The application event object. 934 */ 935 function ev_canvasSizeChange (ev) { 936 if (_self.state !== _self.STATE_SELECTED) { 937 return; 938 } 939 940 marqueeStyle.top = (sel.y * ev.scale) + 'px'; 941 marqueeStyle.left = (sel.x * ev.scale) + 'px'; 942 marqueeStyle.width = (sel.width * ev.scale - borderDouble) + 'px'; 943 marqueeStyle.height = (sel.height * ev.scale - borderDouble) + 'px'; 944 }; 945 946 /** 947 * The <code>configChange</code> application event handler. This method makes 948 * sure that changes to the selection transparency configuration option are 949 * applied. 950 * 951 * @private 952 * @param {pwlib.appEvent.configChange} ev The application event object. 953 */ 954 function ev_configChange (ev) { 955 // Continue only if the selection rectangle is available. 956 if (ev.group !== 'selection' || ev.config !== 'transparent' || 957 !config.transform || _self.state !== _self.STATE_SELECTED) { 958 return; 959 } 960 961 if (!sel.layerCleared) { 962 selectionBufferInit(); 963 } 964 965 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); 966 967 if (!ev.value) { 968 bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height); 969 } 970 971 // Draw the updated selection 972 bufferContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height); 973 }; 974 975 /** 976 * Initialize the selection buffer, when the user starts dragging or resizing 977 * the selected pixels. 978 * 979 * @private 980 */ 981 function selectionBufferInit () { 982 var x = sel.x, 983 y = sel.y, 984 w = sel.width, 985 h = sel.height, 986 sumX = sel.x + sel.width, 987 sumY = sel.y + sel.height, 988 dx = 0, dy = 0; 989 990 sel.widthOriginal = w; 991 sel.heightOriginal = h; 992 993 if (x < 0) { 994 w += x; 995 dx -= x; 996 x = 0; 997 } 998 if (y < 0) { 999 h += y; 1000 dy -= y; 1001 y = 0; 1002 } 1003 1004 if (sumX > image.width) { 1005 w = image.width - sel.x; 1006 } 1007 if (sumY > image.height) { 1008 h = image.height - sel.y; 1009 } 1010 1011 if (!config.transparent) { 1012 bufferContext.fillRect(x, y, w, h); 1013 } 1014 1015 // Parameters: 1016 // source image, src x, src y, src w, src h, dest x, dest y, dest w, dest h 1017 bufferContext.drawImage(layerCanvas, x, y, w, h, x, y, w, h); 1018 1019 sel.canvas.width = sel.widthOriginal; 1020 sel.canvas.height = sel.heightOriginal; 1021 1022 // Also put the selected pixels into the selection buffer. 1023 sel.context.drawImage(layerCanvas, x, y, w, h, dx, dy, w, h); 1024 1025 // Clear the selected pixels from the image 1026 layerContext.clearRect(x, y, w, h); 1027 sel.layerCleared = true; 1028 1029 app.historyAdd(); 1030 }; 1031 1032 /** 1033 * Perform the selection buffer merge onto the current image layer. 1034 * @private 1035 */ 1036 function selectionMergeStrict () { 1037 if (!sel.layerCleared) { 1038 return; 1039 } 1040 1041 if (!config.transparent) { 1042 layerContext.fillRect(sel.x, sel.y, sel.width, sel.height); 1043 } 1044 1045 layerContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height); 1046 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); 1047 1048 sel.layerCleared = false; 1049 sel.canvas.width = 5; 1050 sel.canvas.height = 5; 1051 1052 app.historyAdd(); 1053 }; 1054 1055 /** 1056 * Merge the selection buffer onto the current image layer. 1057 * 1058 * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 1059 * application event. 1060 * 1061 * @returns {Boolean} True if the operation was successful, or false if not. 1062 */ 1063 this.selectionMerge = function () { 1064 if (_self.state !== _self.STATE_SELECTED) { 1065 return false; 1066 } 1067 1068 selectionMergeStrict(); 1069 1070 _self.state = _self.STATE_NONE; 1071 marqueeHide(); 1072 gui.statusShow('selectionActive'); 1073 1074 app.events.dispatch(new appEvent.selectionChange(_self.state)); 1075 1076 return true; 1077 }; 1078 1079 /** 1080 * Select all the entire image. 1081 * 1082 * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 1083 * application event. 1084 * 1085 * @returns {Boolean} True if the operation was successful, or false if not. 1086 */ 1087 this.selectAll = function () { 1088 if (_self.state !== _self.STATE_NONE && _self.state !== 1089 _self.STATE_SELECTED) { 1090 return false; 1091 } 1092 1093 if (_self.state === _self.STATE_SELECTED) { 1094 selectionMergeStrict(); 1095 } else { 1096 _self.state = _self.STATE_SELECTED; 1097 marqueeStyle.display = ''; 1098 } 1099 1100 sel.x = 0; 1101 sel.y = 0; 1102 sel.width = image.width; 1103 sel.height = image.height; 1104 1105 marqueeStyle.top = '0px'; 1106 marqueeStyle.left = '0px'; 1107 marqueeStyle.width = (sel.width*image.canvasScale - borderDouble) + 'px'; 1108 marqueeStyle.height = (sel.height*image.canvasScale - borderDouble) + 'px'; 1109 1110 mouseAreaUpdate(); 1111 1112 app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 1113 sel.width, sel.height)); 1114 1115 return true; 1116 }; 1117 1118 /** 1119 * Cut the selected pixels. The associated ImageData is stored in {@link 1120 * PaintWeb#clipboard}. 1121 * 1122 * <p>This method dispatches two application events: {@link 1123 * pwlib.appEvent.clipboardUpdate} and {@link pwlib.appEvent.selectionChange}. 1124 * 1125 * @returns {Boolean} True if the operation was successful, or false if not. 1126 */ 1127 this.selectionCut = function () { 1128 if (!_self.selectionCopy()) { 1129 return false; 1130 } 1131 1132 if (sel.layerCleared) { 1133 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); 1134 1135 sel.canvas.width = 5; 1136 sel.canvas.height = 5; 1137 sel.layerCleared = false; 1138 1139 } else { 1140 layerContext.clearRect(sel.x, sel.y, sel.width, sel.height); 1141 app.historyAdd(); 1142 } 1143 1144 _self.state = _self.STATE_NONE; 1145 marqueeHide(); 1146 1147 app.events.dispatch(new appEvent.selectionChange(_self.state)); 1148 gui.statusShow('selectionActive'); 1149 1150 return true; 1151 }; 1152 1153 /** 1154 * Copy the selected pixels. The associated ImageData is stored in {@link 1155 * PaintWeb#clipboard}. 1156 * 1157 * <p>This method dispatches the {@link pwlib.appEvent.clipboardUpdate} 1158 * application event. 1159 * 1160 * @returns {Boolean} True if the operation was successful, or false if not. 1161 */ 1162 this.selectionCopy = function () { 1163 if (_self.state !== _self.STATE_SELECTED) { 1164 return false; 1165 } 1166 1167 if (!layerContext.getImageData || !layerContext.putImageData) { 1168 alert(lang.errorClipboardUnsupported); 1169 return false; 1170 } 1171 1172 if (!sel.layerCleared) { 1173 var w = sel.width, 1174 h = sel.height, 1175 sumX = sel.width + sel.x; 1176 sumY = sel.height + sel.y; 1177 1178 if (sumX > image.width) { 1179 w = image.width - sel.x; 1180 } 1181 if (sumY > image.height) { 1182 h = image.height - sel.y; 1183 } 1184 1185 try { 1186 app.clipboard = layerContext.getImageData(sel.x, sel.y, w, h); 1187 } catch (err) { 1188 alert(lang.failedSelectionCopy); 1189 return false; 1190 } 1191 1192 } else { 1193 try { 1194 app.clipboard = sel.context.getImageData(0, 0, sel.widthOriginal, 1195 sel.heightOriginal); 1196 } catch (err) { 1197 alert(lang.failedSelectionCopy); 1198 return false; 1199 } 1200 } 1201 1202 app.events.dispatch(new appEvent.clipboardUpdate(app.clipboard)); 1203 1204 return true; 1205 }; 1206 1207 /** 1208 * Paste an image from the "clipboard". The {@link PaintWeb#clipboard} object 1209 * must be an ImageData. This method will generate a new selection which will 1210 * hold the pasted image. 1211 * 1212 * <p>The {@link pwlib.appEvent.selectionChange} application event is 1213 * dispatched. 1214 * 1215 * <p>If the {@link PaintWeb.config.selection.transform} value is false, then 1216 * it becomes true. The {@link pwlib.appEvent.configChange} application is 1217 * then dispatched. 1218 * 1219 * @returns {Boolean} True if the operation was successful, or false if not. 1220 */ 1221 this.clipboardPaste = function () { 1222 if (!app.clipboard || _self.state !== _self.STATE_NONE && _self.state !== 1223 _self.STATE_SELECTED) { 1224 return false; 1225 } 1226 1227 if (!layerContext.getImageData || !layerContext.putImageData) { 1228 alert(lang.errorClipboardUnsupported); 1229 return false; 1230 } 1231 1232 // The default position for the pasted image is the top left corner of the 1233 // visible area, taking into consideration the zoom level. 1234 var x = MathRound(gui.elems.viewport.scrollLeft / image.canvasScale), 1235 y = MathRound(gui.elems.viewport.scrollTop / image.canvasScale), 1236 w = app.clipboard.width, 1237 h = app.clipboard.height; 1238 1239 sel.canvas.width = w; 1240 sel.canvas.height = h; 1241 sel.context.putImageData(app.clipboard, 0, 0); 1242 1243 if (_self.state === _self.STATE_SELECTED) { 1244 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); 1245 } else { 1246 _self.state = _self.STATE_SELECTED; 1247 } 1248 1249 if (!config.transparent) { 1250 bufferContext.fillRect(x, y, w, h); 1251 } 1252 bufferContext.drawImage(sel.canvas, x, y, w, h); 1253 1254 sel.widthOriginal = sel.width = w; 1255 sel.heightOriginal = sel.height = h; 1256 sel.x = x; 1257 sel.y = y; 1258 sel.layerCleared = true; 1259 1260 marqueeStyle.top = (y * image.canvasScale) + 'px'; 1261 marqueeStyle.left = (x * image.canvasScale) + 'px'; 1262 marqueeStyle.width = (w * image.canvasScale - borderDouble) + 'px'; 1263 marqueeStyle.height = (h * image.canvasScale - borderDouble) + 'px'; 1264 marqueeStyle.display = ''; 1265 1266 if (!config.transform) { 1267 config.transform = true; 1268 app.events.dispatch(new appEvent.configChange(true, false, 'transform', 1269 'selection', config)); 1270 } 1271 1272 mouseAreaUpdate(); 1273 1274 app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 1275 sel.width, sel.height)); 1276 1277 gui.statusShow('selectionAvailable'); 1278 1279 return true; 1280 }; 1281 1282 /** 1283 * Perform selection delete. 1284 * 1285 * <p>This method changes the {@link PaintWeb.config.selection.transform} 1286 * value to false if the current selection has pixels that are currently being 1287 * manipulated. In such cases, the {@link pwlib.appEvent.configChange} 1288 * application event is also dispatched. 1289 * 1290 * @returns {Boolean} True if the operation was successful, or false if not. 1291 */ 1292 this.selectionDelete = function () { 1293 // Delete the pixels from the image if they are not deleted already. 1294 if (_self.state !== _self.STATE_SELECTED) { 1295 return false; 1296 } 1297 1298 if (!sel.layerCleared) { 1299 layerContext.clearRect(sel.x, sel.y, sel.width, sel.height); 1300 app.historyAdd(); 1301 1302 } else { 1303 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); 1304 sel.layerCleared = false; 1305 sel.canvas.width = 5; 1306 sel.canvas.height = 5; 1307 1308 if (config.transform) { 1309 config.transform = false; 1310 app.events.dispatch(new appEvent.configChange(false, true, 'transform', 1311 'selection', config)); 1312 } 1313 } 1314 1315 return true; 1316 }; 1317 1318 /** 1319 * Drop the current selection. 1320 * 1321 * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 1322 * application event. 1323 * 1324 * @returns {Boolean} True if the operation was successful, or false if not. 1325 */ 1326 this.selectionDrop = function () { 1327 if (_self.state !== _self.STATE_SELECTED) { 1328 return false; 1329 } 1330 1331 if (sel.layerCleared) { 1332 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); 1333 sel.canvas.width = 5; 1334 sel.canvas.height = 5; 1335 sel.layerCleared = false; 1336 } 1337 1338 _self.state = _self.STATE_NONE; 1339 1340 marqueeHide(); 1341 gui.statusShow('selectionActive'); 1342 1343 app.events.dispatch(new appEvent.selectionChange(_self.state)); 1344 1345 return true; 1346 }; 1347 1348 /** 1349 * Fill the available selection with the current 1350 * <var>bufferContext.fillStyle</var>. 1351 * 1352 * @returns {Boolean} True if the operation was successful, or false if not. 1353 */ 1354 this.selectionFill = function () { 1355 if (_self.state !== _self.STATE_SELECTED) { 1356 return false; 1357 } 1358 1359 if (sel.layerCleared) { 1360 sel.context.fillStyle = bufferContext.fillStyle; 1361 sel.context.fillRect(0, 0, sel.widthOriginal, sel.heightOriginal); 1362 bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height); 1363 1364 } else { 1365 layerContext.fillStyle = bufferContext.fillStyle; 1366 layerContext.fillRect(sel.x, sel.y, sel.width, sel.height); 1367 app.historyAdd(); 1368 } 1369 1370 return true; 1371 }; 1372 1373 /** 1374 * Crop the image to selection width and height. The selected pixels become 1375 * the image itself. 1376 * 1377 * <p>This method invokes the {@link this#selectionMerge} and {@link 1378 * PaintWeb#imageCrop} methods. 1379 * 1380 * @returns {Boolean} True if the operation was successful, or false if not. 1381 */ 1382 this.selectionCrop = function () { 1383 if (_self.state !== _self.STATE_SELECTED) { 1384 return false; 1385 } 1386 1387 _self.selectionMerge(); 1388 1389 var w = sel.width, 1390 h = sel.height, 1391 sumX = sel.x + w, 1392 sumY = sel.y + h; 1393 1394 if (sumX > image.width) { 1395 w -= sumX - image.width; 1396 } 1397 if (sumY > image.height) { 1398 h -= sumY - image.height; 1399 } 1400 1401 app.imageCrop(sel.x, sel.y, w, h); 1402 1403 return true; 1404 }; 1405 1406 /** 1407 * The <code>keydown</code> event handler. This method calls selection-related 1408 * commands associated to keyboard shortcuts. 1409 * 1410 * @param {Event} ev The DOM Event object. 1411 * 1412 * @returns {Boolean} True if the keyboard shortcut was recognized, or false 1413 * if not. 1414 * 1415 * @see PaintWeb.config.selection.keys holds the keyboard shortcuts 1416 * configuration. 1417 */ 1418 this.keydown = function (ev) { 1419 switch (ev.kid_) { 1420 case config.keys.transformToggle: 1421 // Toggle the selection transformation mode. 1422 config.transform = !config.transform; 1423 app.events.dispatch(new appEvent.configChange(config.transform, 1424 !config.transform, 'transform', 'selection', config)); 1425 break; 1426 1427 case config.keys.selectionCrop: 1428 return _self.selectionCrop(ev); 1429 1430 case config.keys.selectionDelete: 1431 return _self.selectionDelete(ev); 1432 1433 case config.keys.selectionDrop: 1434 return _self.selectionDrop(ev); 1435 1436 case config.keys.selectionFill: 1437 return _self.selectionFill(ev); 1438 1439 default: 1440 return false; 1441 } 1442 1443 return true; 1444 }; 1445 }; 1446 1447 /** 1448 * @class Selection change event. This event is not cancelable. 1449 * 1450 * @augments pwlib.appEvent 1451 * 1452 * @param {Number} state Tells the new state of the selection. 1453 * @param {Number} [x] Selection start position on the x-axis of the image. 1454 * @param {Number} [y] Selection start position on the y-axis of the image. 1455 * @param {Number} [width] Selection width. 1456 * @param {Number} [height] Selection height. 1457 */ 1458 pwlib.appEvent.selectionChange = function (state, x, y, width, height) { 1459 /** 1460 * No selection is available. 1461 * @constant 1462 */ 1463 this.STATE_NONE = 0; 1464 1465 /** 1466 * Selection available. 1467 * @constant 1468 */ 1469 this.STATE_SELECTED = 2; 1470 1471 /** 1472 * Selection state. 1473 * @type Number 1474 */ 1475 this.state = state; 1476 1477 /** 1478 * Selection location on the x-axis of the image. 1479 * @type Number 1480 */ 1481 this.x = x; 1482 1483 /** 1484 * Selection location on the y-axis of the image. 1485 * @type Number 1486 */ 1487 this.y = y; 1488 1489 /** 1490 * Selection width. 1491 * @type Number 1492 */ 1493 this.width = width; 1494 1495 /** 1496 * Selection height. 1497 * @type Number 1498 */ 1499 this.height = height; 1500 1501 pwlib.appEvent.call(this, 'selectionChange'); 1502 }; 1503 1504 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: 1505 1506