1 /* 2 * Copyright (C) 2008, 2009, 2010 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: 2010-06-26 21:44:56 +0300 $ 21 */ 22 23 /** 24 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a> 25 * @fileOverview The main PaintWeb application code. 26 */ 27 28 /** 29 * @class The PaintWeb application object. 30 * 31 * @param {Window} [win=window] The window object to use. 32 * @param {Document} [doc=document] The document object to use. 33 */ 34 function PaintWeb (win, doc) { 35 var _self = this; 36 37 if (!win) { 38 win = window; 39 } 40 if (!doc) { 41 doc = document; 42 } 43 44 /** 45 * PaintWeb version. 46 * @type Number 47 */ 48 this.version = 0.9; //! 49 50 /** 51 * PaintWeb build date (YYYYMMDD). 52 * @type Number 53 */ 54 this.build = -1; //! 55 56 /** 57 * Holds all the PaintWeb configuration. 58 * @type Object 59 */ 60 this.config = { 61 showErrors: true 62 }; 63 64 /** 65 * Holds all language strings used within PaintWeb. 66 */ 67 // Here we include a minimal set of strings, used in case the language file will 68 // not load. 69 this.lang = { 70 "noComputedStyle": "Error: window.getComputedStyle is not available.", 71 "noXMLHttpRequest": "Error: window.XMLHttpRequest is not available.", 72 "noCanvasSupport": "Error: Your browser does not support Canvas.", 73 "guiPlaceholderWrong": "Error: The config.guiPlaceholder property must " + 74 "reference a DOM element!", 75 "initHandlerMustBeFunction": "The first argument must be a function.", 76 "noConfigFile": "Error: You must point to a configuration file by " + 77 "setting the config.configFile property!", 78 "failedConfigLoad": "Error: Failed loading the configuration file.", 79 "failedLangLoad": "Error: Failed loading the language file." 80 }; 81 82 /** 83 * Holds the buffer canvas and context references. 84 * @type Object 85 */ 86 this.buffer = {canvas: null, context: null}; 87 88 /** 89 * Holds the current layer ID, canvas and context references. 90 * @type Object 91 */ 92 this.layer = {id: null, canvas: null, context: null}; 93 94 /** 95 * The instance of the active tool object. 96 * 97 * @type Object 98 * 99 * @see PaintWeb.config.toolDefault holds the ID of the tool which is 100 * activated when the application loads. 101 * @see PaintWeb#toolActivate Activate a drawing tool by ID. 102 * @see PaintWeb#toolRegister Register a new drawing tool. 103 * @see PaintWeb#toolUnregister Unregister a drawing tool. 104 * @see pwlib.tools holds the drawing tools. 105 */ 106 this.tool = null; 107 108 /** 109 * Holds references to DOM elements. 110 * 111 * @private 112 * @type Object 113 */ 114 this.elems = {}; 115 116 /** 117 * Holds the last recorded mouse coordinates and the button state (if it's 118 * down or not). 119 * 120 * @private 121 * @type Object 122 */ 123 this.mouse = {x: 0, y: 0, buttonDown: false}; 124 125 /** 126 * Holds all the PaintWeb extensions. 127 * 128 * @type Object 129 * @see PaintWeb#extensionRegister Register a new extension. 130 * @see PaintWeb#extensionUnregister Unregister an extension. 131 * @see PaintWeb.config.extensions Holds the list of extensions to be loaded 132 * automatically when PaintWeb is initialized. 133 */ 134 this.extensions = {}; 135 136 /** 137 * Holds all the PaintWeb commands. Each property in this object must 138 * reference a simple function which can be executed by keyboard shortcuts 139 * and/or GUI elements. 140 * 141 * @type Object 142 * @see PaintWeb#commandRegister Register a new command. 143 * @see PaintWeb#commandUnregister Unregister a command. 144 */ 145 this.commands = {}; 146 147 /** 148 * The graphical user interface object instance. 149 * @type pwlib.gui 150 */ 151 this.gui = null; 152 153 /** 154 * The document element PaintWeb is working with. 155 * 156 * @private 157 * @type Document 158 * @default document 159 */ 160 this.doc = doc; 161 162 /** 163 * The window object PaintWeb is working with. 164 * 165 * @private 166 * @type Window 167 * @default window 168 */ 169 this.win = win; 170 171 /** 172 * Holds image information: width, height, zoom and more. 173 * 174 * @type Object 175 */ 176 this.image = { 177 /** 178 * Image width. 179 * 180 * @type Number 181 */ 182 width: 0, 183 184 /** 185 * Image height. 186 * 187 * @type Number 188 */ 189 height: 0, 190 191 /** 192 * Image zoom level. This property holds the current image zoom level used 193 * by the user for viewing the image. 194 * 195 * @type Number 196 * @default 1 197 */ 198 zoom: 1, 199 200 /** 201 * Image scaling. The canvas elements are scaled from CSS using this value 202 * as the scaling factor. This value is dependant on the browser rendering 203 * resolution and on the user-defined image zoom level. 204 * 205 * @type Number 206 * @default 1 207 */ 208 canvasScale: 1, 209 210 /** 211 * Tells if the current image has been modified since the initial load. 212 * 213 * @type Boolean 214 * @default false 215 */ 216 modified: false 217 }; 218 219 /** 220 * Resolution information. 221 * 222 * @type Object 223 */ 224 this.resolution = { 225 /** 226 * The DOM element holding information about the current browser rendering 227 * settings (zoom / DPI). 228 * 229 * @private 230 * @type Element 231 */ 232 elem: null, 233 234 /** 235 * The ID of the DOM element holding information about the current browser 236 * rendering settings (zoom / DPI). 237 * 238 * @private 239 * @type String 240 * @default 'paintweb_resInfo' 241 */ 242 elemId: 'paintweb_resInfo', 243 244 /** 245 * The styling necessary for the DOM element. 246 * 247 * @private 248 * @type String 249 */ 250 cssText: '@media screen and (resolution:96dpi){' + 251 '#paintweb_resInfo{width:96px}}' + 252 '@media screen and (resolution:134dpi){' + 253 '#paintweb_resInfo{width:134px}}' + 254 '@media screen and (resolution:200dpi){' + 255 '#paintweb_resInfo{width:200px}}' + 256 '@media screen and (resolution:300dpi){' + 257 '#paintweb_resInfo{width:300px}}' + 258 '#paintweb_resInfo{' + 259 'display:block;' + 260 'height:100%;' + 261 'left:-3000px;' + 262 'position:fixed;' + 263 'top:0;' + 264 'visibility:hidden;' + 265 'z-index:-32}', 266 267 /** 268 * Optimal DPI for the canvas elements. 269 * 270 * @private 271 * @type Number 272 * @default 96 273 */ 274 dpiOptimal: 96, 275 276 /** 277 * The current DPI used by the browser for rendering the entire page. 278 * 279 * @type Number 280 * @default 96 281 */ 282 dpiLocal: 96, 283 284 /** 285 * The current zoom level used by the browser for rendering the entire page. 286 * 287 * @type Number 288 * @default 1 289 */ 290 browserZoom: 1, 291 292 /** 293 * The scaling factor used by the browser for rendering the entire page. For 294 * example, on Gecko using DPI 200 the scale factor is 2. 295 * 296 * @private 297 * @type Number 298 * @default -1 299 */ 300 scale: -1 301 }; 302 303 /** 304 * The image history. 305 * 306 * @private 307 * @type Object 308 */ 309 this.history = { 310 /** 311 * History position. 312 * 313 * @type Number 314 * @default 0 315 */ 316 pos: 0, 317 318 /** 319 * The ImageDatas for each history state. 320 * 321 * @private 322 * @type Array 323 */ 324 states: [] 325 }; 326 327 /** 328 * Tells if the browser supports the Canvas Shadows API. 329 * 330 * @type Boolean 331 * @default true 332 */ 333 this.shadowSupported = true; 334 335 /** 336 * Tells if the current tool allows the drawing of shadows. 337 * 338 * @type Boolean 339 * @default true 340 */ 341 this.shadowAllowed = true; 342 343 /** 344 * Image in the clipboard. This is used when some selection is copy/pasted. 345 * 346 * @type ImageData 347 */ 348 this.clipboard = false; 349 350 /** 351 * Application initialization state. This property can be in one of the 352 * following states: 353 * 354 * <ul> 355 * <li>{@link PaintWeb.INIT_NOT_STARTED} - The initialization is not 356 * started. 357 * 358 * <li>{@link PaintWeb.INIT_STARTED} - The initialization process is 359 * running. 360 * 361 * <li>{@link PaintWeb.INIT_DONE} - The initialization process has completed 362 * successfully. 363 * 364 * <li>{@link PaintWeb.INIT_ERROR} - The initialization process has failed. 365 * </ul> 366 * 367 * @type Number 368 * @default PaintWeb.INIT_NOT_STARTED 369 */ 370 this.initialized = PaintWeb.INIT_NOT_STARTED; 371 372 /** 373 * Custom application events object. 374 * 375 * @type pwlib.appEvents 376 */ 377 this.events = null; 378 379 /** 380 * Unique ID for the current PaintWeb instance. 381 * 382 * @type Number 383 */ 384 this.UID = 0; 385 386 /** 387 * List of Canvas context properties to save and restore. 388 * 389 * <p>When the Canvas is resized the state is lost. Using context.save/restore 390 * state does work only in Opera. In Firefox/Gecko and WebKit saved states are 391 * lost after resize, so there's no state to restore. As such, PaintWeb has 392 * its own simple state save/restore mechanism. The property values are saved 393 * into a JavaScript object. 394 * 395 * @private 396 * @type Array 397 * 398 * @see PaintWeb#stateSave to save the canvas context state. 399 * @see PaintWeb#stateRestore to restore a canvas context state. 400 */ 401 this.stateProperties = ['strokeStyle', 'fillStyle', 'globalAlpha', 402 'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', 'shadowOffsetX', 403 'shadowOffsetY', 'shadowBlur', 'shadowColor', 'globalCompositeOperation', 404 'font', 'textAlign', 'textBaseline']; 405 406 /** 407 * Holds the keyboard event listener object. 408 * 409 * @private 410 * @type pwlib.dom.KeyboardEventListener 411 * @see pwlib.dom.KeyboardEventListener The class dealing with the 412 * cross-browser differences in the DOM keyboard events. 413 */ 414 var kbListener_ = null; 415 416 /** 417 * Holds temporary state information during PaintWeb initialization. 418 * 419 * @private 420 * @type Object 421 */ 422 var temp_ = {onInit: null, toolsLoadQueue: 0, extensionsLoadQueue: 0}; 423 424 // Avoid global scope lookup. 425 var MathAbs = Math.abs, 426 MathFloor = Math.floor, 427 MathMax = Math.max, 428 MathMin = Math.min, 429 MathRound = Math.round, 430 pwlib = null, 431 appEvent = null, 432 lang = this.lang; 433 434 /** 435 * Element node type constant. 436 * 437 * @constant 438 * @type Number 439 */ 440 this.ELEMENT_NODE = window.Node ? Node.ELEMENT_NODE : 1; 441 442 /** 443 * PaintWeb pre-initialization code. This runs when the PaintWeb instance is 444 * constructed. 445 * @private 446 */ 447 function preInit() { 448 var d = new Date(); 449 450 // If PaintWeb is running directly from the source code, then the build date 451 // is always today. 452 if (_self.build === -1) { 453 var dateArr = [d.getFullYear(), d.getMonth()+1, d.getDate()]; 454 455 if (dateArr[1] < 10) { 456 dateArr[1] = '0' + dateArr[1]; 457 } 458 if (dateArr[2] < 10) { 459 dateArr[2] = '0' + dateArr[2]; 460 } 461 462 _self.build = dateArr.join(''); 463 } 464 465 _self.UID = d.getMilliseconds() * MathRound(Math.random() * 100); 466 _self.elems.head = doc.getElementsByTagName('head')[0] || doc.body; 467 }; 468 469 /** 470 * Initialize PaintWeb. 471 * 472 * <p>This method is asynchronous, meaning that it will return much sooner 473 * before the application initialization is completed. 474 * 475 * @param {Function} [handler] The <code>appInit</code> event handler. Your 476 * event handler will be invoked automatically when PaintWeb completes 477 * loading, or when an error occurs. 478 * 479 * @returns {Boolean} True if the initialization has been started 480 * successfully, or false if not. 481 */ 482 this.init = function (handler) { 483 if (this.initialized === PaintWeb.INIT_DONE) { 484 return true; 485 } 486 487 this.initialized = PaintWeb.INIT_STARTED; 488 489 if (handler && typeof handler !== 'function') { 490 throw new TypeError(lang.initHandlerMustBeFunction); 491 } 492 493 temp_.onInit = handler; 494 495 // Check Canvas support. 496 if (!doc.createElement('canvas').getContext) { 497 this.initError(lang.noCanvasSupport); 498 return false; 499 } 500 501 // Basic functionality used within the Web application. 502 if (!window.getComputedStyle) { 503 try { 504 win.getComputedStyle(doc.createElement('div'), null); 505 } catch (err) { 506 this.initError(lang.noComputedStyle); 507 return false; 508 } 509 } 510 511 if (!window.XMLHttpRequest) { 512 this.initError(lang.noXMLHttpRequest); 513 return false; 514 } 515 516 if (!this.config.configFile) { 517 this.initError(lang.noConfigFile); 518 return false; 519 } 520 521 if (typeof this.config.guiPlaceholder !== 'object' || 522 this.config.guiPlaceholder.nodeType !== this.ELEMENT_NODE) { 523 this.initError(lang.guiPlaceholderWrong); 524 return false; 525 } 526 527 // Silently ignore any wrong value for the config.imageLoad property. 528 if (typeof this.config.imageLoad !== 'object' || 529 this.config.imageLoad.nodeType !== this.ELEMENT_NODE) { 530 this.config.imageLoad = null; 531 } 532 533 // JSON parser and serializer. 534 if (!window.JSON) { 535 this.scriptLoad(PaintWeb.baseFolder + 'includes/json2.js', 536 this.jsonlibReady); 537 } else { 538 this.jsonlibReady(); 539 } 540 541 return true; 542 }; 543 544 /** 545 * The <code>load</code> event handler for the JSON library script. 546 * @private 547 */ 548 this.jsonlibReady = function () { 549 if (window.pwlib) { 550 _self.pwlibReady(); 551 } else { 552 _self.scriptLoad(PaintWeb.baseFolder + 'includes/lib.js', 553 _self.pwlibReady); 554 } 555 }; 556 557 /** 558 * The <code>load</code> event handler for the PaintWeb library script. 559 * @private 560 */ 561 this.pwlibReady = function () { 562 pwlib = window.pwlib; 563 appEvent = pwlib.appEvent; 564 565 // Create the custom application events object. 566 _self.events = new pwlib.appEvents(_self); 567 _self.configLoad(); 568 }; 569 570 /** 571 * Report an initialization error. 572 * 573 * <p>This method dispatches the {@link pwlib.appEvent.appInit} event. 574 * 575 * @private 576 * 577 * @param {String} msg The error message. 578 * 579 * @see pwlib.appEvent.appInit 580 */ 581 this.initError = function (msg) { 582 switch (this.initialized) { 583 case PaintWeb.INIT_ERROR: 584 case PaintWeb.INIT_DONE: 585 case PaintWeb.INIT_NOT_STARTED: 586 return; 587 } 588 589 this.initialized = PaintWeb.INIT_ERROR; 590 591 var ev = null; 592 593 if (this.events && 'dispatch' in this.events && 594 appEvent && 'appInit' in appEvent) { 595 596 ev = new appEvent.appInit(this.initialized, msg); 597 this.events.dispatch(ev); 598 } 599 600 if (typeof temp_.onInit === 'function') { 601 if (!ev) { 602 // fake an event dispatch. 603 ev = {type: 'appInit', state: this.initialized, errorMessage: msg}; 604 } 605 606 temp_.onInit.call(this, ev); 607 } 608 609 if (this.config.showErrors) { 610 alert(msg); 611 } else if (window.console && console.log) { 612 console.log(msg); 613 } 614 }; 615 616 /** 617 * Asynchronously load the configuration file. This method issues an 618 * XMLHttpRequest to load the JSON file. 619 * 620 * @private 621 * 622 * @see PaintWeb.config.configFile The configuration file. 623 * @see pwlib.xhrLoad The library function being used for creating the 624 * XMLHttpRequest object. 625 */ 626 this.configLoad = function () { 627 pwlib.xhrLoad(PaintWeb.baseFolder + this.config.configFile, 628 this.configReady); 629 }; 630 631 /** 632 * The configuration reader. This is the event handler for the XMLHttpRequest 633 * object, for the <code>onreadystatechange</code> event. 634 * 635 * @private 636 * 637 * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled. 638 * 639 * @see PaintWeb#configLoad The method which issues the XMLHttpRequest request 640 * for loading the configuration file. 641 */ 642 this.configReady = function (xhr) { 643 /* 644 * readyState values: 645 * 0 UNINITIALIZED open() has not been called yet. 646 * 1 LOADING send() has not been called yet. 647 * 2 LOADED send() has been called, headers and status are available. 648 * 3 INTERACTIVE Downloading, responseText holds the partial data. 649 * 4 COMPLETED Finished with all operations. 650 */ 651 if (!xhr || xhr.readyState !== 4) { 652 return; 653 } 654 655 if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) { 656 _self.initError(lang.failedConfigLoad); 657 return; 658 } 659 660 var config = pwlib.jsonParse(xhr.responseText); 661 pwlib.extend(_self.config, config); 662 663 _self.langLoad(); 664 }; 665 666 /** 667 * Asynchronously load the language file. This method issues an XMLHttpRequest 668 * to load the JSON file. 669 * 670 * @private 671 * 672 * @see PaintWeb.config.lang The language you want for the PaintWeb user 673 * interface. 674 * @see pwlib.xhrLoad The library function being used for creating the 675 * XMLHttpRequest object. 676 */ 677 this.langLoad = function () { 678 var id = this.config.lang, 679 file = PaintWeb.baseFolder; 680 681 // If the language is not available, always fallback to English. 682 if (!(id in this.config.languages)) { 683 id = this.config.lang = 'en'; 684 } 685 686 if ('file' in this.config.languages[id]) { 687 file += this.config.languages[id].file; 688 } else { 689 file += this.config.langFolder + '/' + id + '.json'; 690 } 691 692 pwlib.xhrLoad(file, this.langReady); 693 }; 694 695 /** 696 * The language file reader. This is the event handler for the XMLHttpRequest 697 * object, for the <code>onreadystatechange</code> event. 698 * 699 * @private 700 * 701 * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled. 702 * 703 * @see PaintWeb#langLoad The method which issues the XMLHttpRequest request 704 * for loading the language file. 705 */ 706 this.langReady = function (xhr) { 707 if (!xhr || xhr.readyState !== 4) { 708 return; 709 } 710 711 if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) { 712 _self.initError(lang.failedLangLoad); 713 return; 714 } 715 716 pwlib.extend(_self.lang, pwlib.jsonParse(xhr.responseText)); 717 718 if (_self.initCanvas() && _self.initContext()) { 719 // Start GUI load now. 720 _self.guiLoad(); 721 } else { 722 _self.initError(lang.errorInitCanvas); 723 } 724 }; 725 726 /** 727 * Initialize the PaintWeb commands. 728 * 729 * @private 730 * @returns {Boolean} True if the initialization was successful, or false if 731 * not. 732 */ 733 this.initCommands = function () { 734 if (this.commandRegister('historyUndo', this.historyUndo) && 735 this.commandRegister('historyRedo', this.historyRedo) && 736 this.commandRegister('selectAll', this.selectAll) && 737 this.commandRegister('selectionCut', this.selectionCut) && 738 this.commandRegister('selectionCopy', this.selectionCopy) && 739 this.commandRegister('clipboardPaste', this.clipboardPaste) && 740 this.commandRegister('imageSave', this.imageSave) && 741 this.commandRegister('imageClear', this.imageClear) && 742 this.commandRegister('swapFillStroke', this.swapFillStroke) && 743 this.commandRegister('imageZoomIn', this.imageZoomIn) && 744 this.commandRegister('imageZoomOut', this.imageZoomOut) && 745 this.commandRegister('imageZoomReset', this.imageZoomReset)) { 746 return true; 747 } else { 748 this.initError(lang.errorInitCommands); 749 return false; 750 } 751 }; 752 753 /** 754 * Load th PaintWeb GUI. This method loads the GUI markup file, the stylesheet 755 * and the script. 756 * 757 * @private 758 * 759 * @see PaintWeb.config.guiStyle The interface style file. 760 * @see PaintWeb.config.guiScript The interface script file. 761 * @see pwlib.gui The interface object. 762 */ 763 this.guiLoad = function () { 764 var cfg = this.config, 765 gui = this.config.gui, 766 base = PaintWeb.baseFolder + cfg.interfacesFolder + '/' + gui + '/', 767 style = base + cfg.guiStyle, 768 script = base + cfg.guiScript; 769 770 this.styleLoad(gui + 'style', style); 771 772 if (pwlib.gui) { 773 this.guiScriptReady(); 774 } else { 775 this.scriptLoad(script, this.guiScriptReady); 776 } 777 }; 778 779 /** 780 * The <code>load</code> event handler for the PaintWeb GUI script. This 781 * method creates an instance of the GUI object that just loaded and starts 782 * loading the GUI markup. 783 * 784 * @private 785 * 786 * @see PaintWeb.config.guiScript The interface script file. 787 * @see PaintWeb.config.guiMarkup The interface markup file. 788 * @see pwlib.gui The interface object. 789 * @see pwlib.xhrLoad The library function being used for creating the 790 * XMLHttpRequest object. 791 */ 792 this.guiScriptReady = function () { 793 var cfg = _self.config, 794 gui = _self.config.gui, 795 base = cfg.interfacesFolder + '/' + gui + '/', 796 markup = base + cfg.guiMarkup; 797 798 _self.gui = new pwlib.gui(_self); 799 800 // Check if the interface markup is cached already. 801 if (markup in pwlib.fileCache) { 802 if (_self.gui.init(pwlib.fileCache[markup])) { 803 _self.initTools(); 804 } else { 805 _self.initError(lang.errorInitGUI); 806 } 807 808 } else { 809 pwlib.xhrLoad(PaintWeb.baseFolder + markup, _self.guiMarkupReady); 810 } 811 }; 812 813 /** 814 * The GUI markup reader. This is the event handler for the XMLHttpRequest 815 * object, for the <code>onreadystatechange</code> event. 816 * 817 * @private 818 * 819 * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled. 820 * 821 * @see PaintWeb#guiScriptReady The method which issues the XMLHttpRequest 822 * request for loading the interface markup file. 823 */ 824 this.guiMarkupReady = function (xhr) { 825 if (!xhr || xhr.readyState !== 4) { 826 return; 827 } 828 829 if (xhr.status !== 304 && xhr.status !== 200) { 830 _self.initError(lang.failedMarkupLoad); 831 return; 832 } 833 834 var param; 835 if (xhr.responseXML && xhr.responseXML.documentElement) { 836 param = xhr.responseXML; 837 } else if (xhr.responseText) { 838 param = xhr.responseText; 839 } else { 840 _self.initError(lang.failedMarkupLoad); 841 return; 842 } 843 844 if (_self.gui.init(param)) { 845 _self.initTools(); 846 } else { 847 _self.initError(lang.errorInitGUI); 848 } 849 }; 850 851 /** 852 * Initialize the Canvas elements. This method creates the elements and 853 * sets-up their dimensions. 854 * 855 * <p>The layer Canvas element will have the background rendered with the 856 * color from {@link PaintWeb.config.backgroundColor}. 857 * 858 * <p>If {@link PaintWeb.config.imageLoad} is defined, then the image element 859 * is inserted into the Canvas image. 860 * 861 * <p>All the Canvas event listeners are also attached to the buffer Canvas 862 * element. 863 * 864 * @private 865 * @returns {Boolean} True if the initialization was successful, or false if 866 * not. 867 * 868 * @see PaintWeb#ev_canvas The global Canvas events handler. 869 */ 870 this.initCanvas = function () { 871 var cfg = this.config, 872 res = this.resolution, 873 resInfo = doc.getElementById(res.elemId), 874 layerCanvas = doc.createElement('canvas'), 875 bufferCanvas = doc.createElement('canvas'), 876 layerContext = layerCanvas.getContext('2d'), 877 bufferContext = bufferCanvas.getContext('2d'), 878 width = cfg.imageWidth, 879 height = cfg.imageHeight, 880 imageLoad = cfg.imageLoad; 881 882 if (!resInfo) { 883 var style = doc.createElement('style'); 884 style.type = 'text/css'; 885 style.appendChild(doc.createTextNode(res.cssText)); 886 _self.elems.head.appendChild(style); 887 888 resInfo = doc.createElement('div'); 889 resInfo.id = res.elemId; 890 doc.body.appendChild(resInfo); 891 } 892 893 if (!resInfo) { 894 this.initError(lang.errorInitCanvas); 895 return false; 896 } 897 if (!layerCanvas || !bufferCanvas || !layerContext || !bufferContext) { 898 this.initError(lang.noCanvasSupport); 899 return false; 900 } 901 902 if (!pwlib.isSameHost(imageLoad.src, win.location.host)) { 903 cfg.imageLoad = imageLoad = null; 904 alert(lang.imageLoadDifferentHost); 905 } 906 907 if (imageLoad) { 908 width = parseInt(imageLoad.width); 909 height = parseInt(imageLoad.height); 910 } 911 912 res.elem = resInfo; 913 914 this.image.width = layerCanvas.width = bufferCanvas.width = width; 915 this.image.height = layerCanvas.height = bufferCanvas.height = height; 916 917 this.layer.canvas = layerCanvas; 918 this.layer.context = layerContext; 919 this.buffer.canvas = bufferCanvas; 920 this.buffer.context = bufferContext; 921 922 if (imageLoad) { 923 layerContext.drawImage(imageLoad, 0, 0); 924 } else { 925 // Set the configured background color. 926 var fillStyle = layerContext.fillStyle; 927 layerContext.fillStyle = cfg.backgroundColor; 928 layerContext.fillRect(0, 0, width, height); 929 layerContext.fillStyle = fillStyle; 930 } 931 932 /* 933 * Setup the event listeners for the canvas element. 934 * 935 * The event handler (ev_canvas) calls the event handlers associated with 936 * the active tool (e.g. tool.mousemove). 937 */ 938 var events = ['dblclick', 'click', 'mousedown', 'mouseup', 'mousemove', 939 'contextmenu'], 940 n = events.length; 941 942 for (var i = 0; i < n; i++) { 943 bufferCanvas.addEventListener(events[i], this.ev_canvas, false); 944 } 945 946 return true; 947 }; 948 949 /** 950 * Initialize the Canvas buffer context. This method updates the context 951 * properties to reflect the values defined in the PaintWeb configuration 952 * file. 953 * 954 * <p>Shadows support is also determined. The {@link PaintWeb#shadowSupported} 955 * value is updated accordingly. 956 * 957 * @private 958 * @returns {Boolean} True if the initialization was successful, or false if 959 * not. 960 */ 961 this.initContext = function () { 962 var bufferContext = this.buffer.context; 963 964 // Opera does not render shadows, at the moment. 965 if (!pwlib.browser.opera && bufferContext.shadowColor && 'shadowOffsetX' in 966 bufferContext && 'shadowOffsetY' in bufferContext && 'shadowBlur' in 967 bufferContext) { 968 this.shadowSupported = true; 969 } else { 970 this.shadowSupported = false; 971 } 972 973 var cfg = this.config, 974 props = { 975 fillStyle: cfg.fillStyle, 976 font: cfg.text.fontSize + 'px ' + cfg.text.fontFamily, 977 lineCap: cfg.line.lineCap, 978 lineJoin: cfg.line.lineJoin, 979 lineWidth: cfg.line.lineWidth, 980 miterLimit: cfg.line.miterLimit, 981 strokeStyle: cfg.strokeStyle, 982 textAlign: cfg.text.textAlign, 983 textBaseline: cfg.text.textBaseline 984 }; 985 986 if (cfg.text.bold) { 987 props.font = 'bold ' + props.font; 988 } 989 990 if (cfg.text.italic) { 991 props.font = 'italic ' + props.font; 992 } 993 994 // Support Gecko 1.9.0 995 if (!bufferContext.fillText && 'mozTextStyle' in bufferContext) { 996 props.mozTextStyle = props.font; 997 } 998 999 for (var prop in props) { 1000 bufferContext[prop] = props[prop]; 1001 } 1002 1003 // shadows are only for the layer context. 1004 if (cfg.shadow.enable && this.shadowSupported) { 1005 var layerContext = this.layer.context; 1006 layerContext.shadowColor = cfg.shadow.shadowColor; 1007 layerContext.shadowBlur = cfg.shadow.shadowBlur; 1008 layerContext.shadowOffsetX = cfg.shadow.shadowOffsetX; 1009 layerContext.shadowOffsetY = cfg.shadow.shadowOffsetY; 1010 } 1011 1012 return true; 1013 }; 1014 1015 /** 1016 * Initialization procedure which runs after the configuration, language and 1017 * GUI files have loaded. 1018 * 1019 * <p>This method dispatches the {@link pwlib.appEvent.appInit} event. 1020 * 1021 * @private 1022 * 1023 * @see pwlib.appEvent.appInit 1024 */ 1025 this.initComplete = function () { 1026 if (!this.initCommands()) { 1027 this.initError(lang.errorInitCommands); 1028 return; 1029 } 1030 1031 // The initial blank state of the image 1032 this.historyAdd(); 1033 this.image.modified = false; 1034 1035 // The global keyboard events handler implements everything needed for 1036 // switching between tools and for accessing any other functionality of the 1037 // Web application. 1038 kbListener_ = new pwlib.dom.KeyboardEventListener(this.config.guiPlaceholder, 1039 {keydown: this.ev_keyboard, 1040 keypress: this.ev_keyboard, 1041 keyup: this.ev_keyboard}); 1042 1043 this.updateCanvasScaling(); 1044 this.win.addEventListener('resize', this.updateCanvasScaling, false); 1045 1046 this.events.add('configChange', this.configChangeHandler); 1047 this.events.add('imageSaveResult', this.imageSaveResultHandler); 1048 1049 // Add the init event handler. 1050 if (typeof temp_.onInit === 'function') { 1051 _self.events.add('appInit', temp_.onInit); 1052 delete temp_.onInit; 1053 } 1054 1055 this.initialized = PaintWeb.INIT_DONE; 1056 1057 this.events.dispatch(new appEvent.appInit(this.initialized)); 1058 }; 1059 1060 /** 1061 * Load all the configured drawing tools. 1062 * @private 1063 */ 1064 this.initTools = function () { 1065 var id = '', 1066 cfg = this.config, 1067 n = cfg.tools.length, 1068 base = PaintWeb.baseFolder + cfg.toolsFolder + '/'; 1069 1070 if (n < 1) { 1071 this.initError(lang.noToolConfigured); 1072 return; 1073 } 1074 1075 temp_.toolsLoadQueue = n; 1076 1077 for (var i = 0; i < n; i++) { 1078 id = cfg.tools[i]; 1079 if (id in pwlib.tools) { 1080 this.toolLoaded(); 1081 } else { 1082 this.scriptLoad(base + id + '.js' , this.toolLoaded); 1083 } 1084 } 1085 }; 1086 1087 /** 1088 * The <code>load</code> event handler for each tool script. 1089 * @private 1090 */ 1091 this.toolLoaded = function () { 1092 temp_.toolsLoadQueue--; 1093 1094 if (temp_.toolsLoadQueue === 0) { 1095 var t = _self.config.tools, 1096 n = t.length; 1097 1098 for (var i = 0; i < n; i++) { 1099 if (!_self.toolRegister(t[i])) { 1100 _self.initError(pwlib.strf(lang.toolRegisterFailed, {id: t[i]})); 1101 return; 1102 } 1103 } 1104 1105 _self.initExtensions(); 1106 } 1107 }; 1108 1109 /** 1110 * Load all the extensions. 1111 * @private 1112 */ 1113 this.initExtensions = function () { 1114 var id = '', 1115 cfg = this.config, 1116 n = cfg.extensions.length, 1117 base = PaintWeb.baseFolder + cfg.extensionsFolder + '/'; 1118 1119 if (n < 1) { 1120 this.initComplete(); 1121 return; 1122 } 1123 1124 temp_.extensionsLoadQueue = n; 1125 1126 for (var i = 0; i < n; i++) { 1127 id = cfg.extensions[i]; 1128 if (id in pwlib.extensions) { 1129 this.extensionLoaded(); 1130 } else { 1131 this.scriptLoad(base + id + '.js', this.extensionLoaded); 1132 } 1133 } 1134 }; 1135 1136 /** 1137 * The <code>load</code> event handler for each extension script. 1138 * @private 1139 */ 1140 this.extensionLoaded = function () { 1141 temp_.extensionsLoadQueue--; 1142 1143 if (temp_.extensionsLoadQueue === 0) { 1144 var e = _self.config.extensions, 1145 n = e.length; 1146 1147 for (var i = 0; i < n; i++) { 1148 if (!_self.extensionRegister(e[i])) { 1149 _self.initError(pwlib.strf(lang.extensionRegisterFailed, {id: e[i]})); 1150 return; 1151 } 1152 } 1153 1154 _self.initComplete(); 1155 } 1156 }; 1157 1158 /** 1159 * Update the canvas scaling. This method determines the DPI and/or zoom level 1160 * used by the browser to render the application. Based on these values, the 1161 * canvas elements are scaled down to cancel any upscaling performed by the 1162 * browser. 1163 * 1164 * <p>The {@link pwlib.appEvent.canvasSizeChange} application event is 1165 * dispatched. 1166 */ 1167 this.updateCanvasScaling = function () { 1168 var res = _self.resolution, 1169 cs = win.getComputedStyle(res.elem, null), 1170 image = _self.image; 1171 bufferStyle = _self.buffer.canvas.style, 1172 layerStyle = _self.layer.canvas.style, 1173 scaleNew = 1, 1174 width = parseInt(cs.width), 1175 height = parseInt(cs.height); 1176 1177 if (pwlib.browser.opera) { 1178 // Opera zoom level detection. 1179 // The scaling factor is sufficiently accurate for zoom levels between 1180 // 100% and 200% (in steps of 10%). 1181 1182 scaleNew = win.innerHeight / height; 1183 scaleNew = MathRound(scaleNew * 10) / 10; 1184 1185 } else if (width && !isNaN(width) && width !== res.dpiOptimal) { 1186 // Page DPI detection. This only works in Gecko 1.9.1. 1187 1188 res.dpiLocal = width; 1189 1190 // The scaling factor is the same as in Gecko. 1191 scaleNew = MathFloor(res.dpiLocal / res.dpiOptimal); 1192 1193 } else if (pwlib.browser.olpcxo && pwlib.browser.gecko) { 1194 // Support for the default Gecko included on the OLPC XO-1 system. 1195 // 1196 // See: 1197 // http://www.robodesign.ro/mihai/blog/paintweb-performance 1198 // http://mxr.mozilla.org/mozilla-central/source/gfx/src/thebes/nsThebesDeviceContext.cpp#725 1199 // dotsArePixels = false on the XO due to a hard-coded patch. 1200 // Thanks go to roc from Mozilla for his feedback on making this work. 1201 1202 res.dpiLocal = 134; // hard-coded value, we cannot determine it 1203 1204 var appUnitsPerCSSPixel = 60, // hard-coded internally in Gecko 1205 devPixelsPerCSSPixel = res.dpiLocal / res.dpiOptimal; // 1.3958333333 1206 appUnitsPerDevPixel = appUnitsPerCSSPixel / devPixelsPerCSSPixel; // 42.9850746278... 1207 1208 scaleNew = appUnitsPerCSSPixel / MathFloor(appUnitsPerDevPixel); // 1.4285714285... 1209 1210 // New in Gecko 1.9.2. 1211 if ('mozImageSmoothingEnabled' in layerStyle) { 1212 layerStyle.mozImageSmoothingEnabled 1213 = bufferStyle.mozImageSmoothingEnabled = false; 1214 } 1215 } 1216 1217 if (scaleNew === res.scale) { 1218 return; 1219 } 1220 1221 res.scale = scaleNew; 1222 1223 var styleWidth = image.width / res.scale * image.zoom, 1224 styleHeight = image.height / res.scale * image.zoom; 1225 1226 image.canvasScale = styleWidth / image.width; 1227 1228 // FIXME: MSIE 9 clears the Canvas element when you change the 1229 // elem.style.width/height... *argh* 1230 bufferStyle.width = layerStyle.width = styleWidth + 'px'; 1231 bufferStyle.height = layerStyle.height = styleHeight + 'px'; 1232 1233 _self.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 1234 image.canvasScale)); 1235 }; 1236 1237 /** 1238 * The Canvas events handler. 1239 * 1240 * <p>This method determines the mouse position relative to the canvas 1241 * element, after which it invokes the method of the currently active tool 1242 * with the same name as the current event type. For example, for the 1243 * <code>mousedown</code> event the <code><var>tool</var>.mousedown()</code> 1244 * method is invoked. 1245 * 1246 * <p>The mouse coordinates are stored in the {@link PaintWeb#mouse} object. 1247 * These properties take into account the current zoom level and the image 1248 * scroll. 1249 * 1250 * @private 1251 * 1252 * @param {Event} ev The DOM Event object. 1253 * 1254 * @returns {Boolean} True if the tool event handler executed, or false 1255 * otherwise. 1256 */ 1257 this.ev_canvas = function (ev) { 1258 if (!_self.tool) { 1259 return false; 1260 } 1261 1262 switch (ev.type) { 1263 case 'mousedown': 1264 /* 1265 * If the mouse is down already, skip the event. 1266 * This is needed to allow the user to go out of the drawing canvas, 1267 * release the mouse button, then come back and click to end the drawing 1268 * operation. 1269 * Additionally, this is needed to allow extensions like MouseKeys to 1270 * perform their actions during a drawing operation, even when a real 1271 * mouse is used. For example, allow the user to start drawing with the 1272 * keyboard (press 0) then use the real mouse to move and click to end 1273 * the drawing operation. 1274 */ 1275 if (_self.mouse.buttonDown) { 1276 return false; 1277 } 1278 _self.mouse.buttonDown = true; 1279 break; 1280 1281 case 'mouseup': 1282 // Skip the event if the mouse button was not down. 1283 if (!_self.mouse.buttonDown) { 1284 return false; 1285 } 1286 _self.mouse.buttonDown = false; 1287 } 1288 1289 /* 1290 * Update the event, to include the mouse position, relative to the canvas 1291 * element. 1292 */ 1293 if ('layerX' in ev) { 1294 if (_self.image.canvasScale === 1) { 1295 _self.mouse.x = ev.layerX; 1296 _self.mouse.y = ev.layerY; 1297 } else { 1298 _self.mouse.x = MathRound(ev.layerX / _self.image.canvasScale); 1299 _self.mouse.y = MathRound(ev.layerY / _self.image.canvasScale); 1300 } 1301 } else if ('offsetX' in ev) { 1302 if (_self.image.canvasScale === 1) { 1303 _self.mouse.x = ev.offsetX; 1304 _self.mouse.y = ev.offsetY; 1305 } else { 1306 _self.mouse.x = MathRound(ev.offsetX / _self.image.canvasScale); 1307 _self.mouse.y = MathRound(ev.offsetY / _self.image.canvasScale); 1308 } 1309 } 1310 1311 // The event handler of the current tool. 1312 if (ev.type in _self.tool && _self.tool[ev.type](ev)) { 1313 ev.preventDefault(); 1314 return true; 1315 } else { 1316 return false; 1317 } 1318 }; 1319 1320 /** 1321 * The global keyboard events handler. This makes all the keyboard shortcuts 1322 * work in the web application. 1323 * 1324 * <p>This method determines the key the user pressed, based on the 1325 * <var>ev</var> DOM Event object, taking into consideration any browser 1326 * differences. Two new properties are added to the <var>ev</var> object: 1327 * 1328 * <ul> 1329 * <li><var>ev.kid_</var> is a string holding the key and the modifiers list 1330 * (<kbd>Control</kbd>, <kbd>Alt</kbd> and/or <kbd>Shift</kbd>). For 1331 * example, if the user would press the key <kbd>A</kbd> while holding down 1332 * <kbd>Control</kbd>, then <var>ev.kid_</var> would be "Control A". If the 1333 * user would press "9" while holding down <kbd>Shift</kbd>, then 1334 * <var>ev.kid_</var> would be "Shift 9". 1335 * 1336 * <li><var>ev.kobj_</var> holds a reference to the keyboard shortcut 1337 * definition object from the configuration. This is useful for reuse, for 1338 * passing parameters from the keyboard shortcut configuration object to the 1339 * event handler. 1340 * </ul> 1341 * 1342 * <p>In {@link PaintWeb.config.keys} one can setup the keyboard shortcuts. 1343 * If the keyboard combination is found in that list, then the associated tool 1344 * is activated. 1345 * 1346 * <p>Note: this method includes some work-around for making the image zoom 1347 * keys work well both in Opera and Firefox. 1348 * 1349 * @private 1350 * 1351 * @param {Event} ev The DOM Event object. 1352 * 1353 * @see PaintWeb.config.keys The keyboard shortcuts configuration. 1354 * @see pwlib.dom.KeyboardEventListener The class dealing with the 1355 * cross-browser differences in the DOM keyboard events. 1356 */ 1357 this.ev_keyboard = function (ev) { 1358 // Do not continue if the key was not recognized by the lib. 1359 if (!ev.key_) { 1360 return; 1361 } 1362 1363 if (ev.target && ev.target.nodeName) { 1364 switch (ev.target.nodeName.toLowerCase()) { 1365 case 'input': 1366 if (ev.type === 'keypress' && (ev.key_ === 'Up' || ev.key_ === 'Down') 1367 && ev.target.getAttribute('type') === 'number') { 1368 _self.ev_numberInput(ev); 1369 } 1370 case 'select': 1371 case 'textarea': 1372 case 'button': 1373 return; 1374 } 1375 } 1376 1377 // Rather ugly, but the only way, at the moment, to detect these keys in 1378 // Opera and Firefox. 1379 if (ev.type === 'keypress' && ev.char_) { 1380 var isZoomKey = true, 1381 imageZoomKeys = _self.config.imageZoomKeys; 1382 1383 // Check if this is a zoom key and execute the commands as needed. 1384 switch (ev.char_) { 1385 case imageZoomKeys['in']: 1386 _self.imageZoomIn(ev); 1387 break; 1388 1389 case imageZoomKeys['out']: 1390 _self.imageZoomOut(ev); 1391 break; 1392 case imageZoomKeys['reset']: 1393 _self.imageZoomReset(ev); 1394 break; 1395 default: 1396 isZoomKey = false; 1397 } 1398 1399 if (isZoomKey) { 1400 ev.preventDefault(); 1401 return; 1402 } 1403 } 1404 1405 // Determine the key ID. 1406 ev.kid_ = ''; 1407 var i, kmods = {altKey: 'Alt', ctrlKey: 'Control', shiftKey: 'Shift'}; 1408 for (i in kmods) { 1409 if (ev[i] && ev.key_ !== kmods[i]) { 1410 ev.kid_ += kmods[i] + ' '; 1411 } 1412 } 1413 ev.kid_ += ev.key_; 1414 1415 // Send the keyboard event to the event handler of the active tool. If it 1416 // returns true, we consider it recognized the keyboard shortcut. 1417 if (_self.tool && ev.type in _self.tool && _self.tool[ev.type](ev)) { 1418 return true; 1419 } 1420 1421 // If there's no event handler within the active tool, or if the event 1422 // handler does otherwise return false, then we continue with the global 1423 // keyboard shortcuts. 1424 1425 var gkey = _self.config.keys[ev.kid_]; 1426 if (!gkey) { 1427 return false; 1428 } 1429 1430 ev.kobj_ = gkey; 1431 1432 // Check if the keyboard shortcut has some extension associated. 1433 if ('extension' in gkey) { 1434 var extension = _self.extensions[gkey.extension], 1435 method = gkey.method || ev.type; 1436 1437 // Call the extension method. 1438 if (method in extension) { 1439 extension[method].call(this, ev); 1440 } 1441 1442 } else if ('command' in gkey && gkey.command in _self.commands) { 1443 // Invoke the command associated with the key. 1444 _self.commands[gkey.command].call(this, ev); 1445 1446 } else if (ev.type === 'keydown' && 'toolActivate' in gkey) { 1447 1448 // Active the tool associated to the key. 1449 _self.toolActivate(gkey.toolActivate, ev); 1450 1451 } 1452 1453 if (ev.type === 'keypress') { 1454 ev.preventDefault(); 1455 } 1456 }; 1457 1458 /** 1459 * This is the <code>keypress</code> event handler for inputs of type=number. 1460 * This function only handles cases when the key is <kbd>Up</kbd> or 1461 * <kbd>Down</kbd>. For the <kbd>Up</kbd> key the input value is increased, 1462 * and for the <kbd>Down</kbd> the value is decreased. 1463 * 1464 * @private 1465 * @param {Event} ev The DOM Event object. 1466 * @see PaintWeb#ev_keyboard 1467 */ 1468 this.ev_numberInput = function (ev) { 1469 var target = ev.target; 1470 1471 // Process the value. 1472 var val, 1473 max = parseFloat(target.getAttribute('max')), 1474 min = parseFloat(target.getAttribute('min')), 1475 step = parseFloat(target.getAttribute('step')); 1476 1477 if (target.value === '' || target.value === null) { 1478 val = !isNaN(min) ? min : 0; 1479 } else { 1480 val = parseFloat(target.value.replace(/[,.]+/g, '.'). 1481 replace(/[^0-9.\-]/g, '')); 1482 } 1483 1484 // If target is not a number, then set the old value, or the minimum value. If all fails, set 0. 1485 if (isNaN(val)) { 1486 val = min || 0; 1487 } 1488 1489 if (isNaN(step)) { 1490 step = 1; 1491 } 1492 1493 if (ev.shiftKey) { 1494 step *= 2; 1495 } 1496 1497 if (ev.key_ === 'Down') { 1498 step *= -1; 1499 } 1500 1501 val += step; 1502 1503 if (!isNaN(max) && val > max) { 1504 val = max; 1505 } else if (!isNaN(min) && val < min) { 1506 val = min; 1507 } 1508 1509 if (val == target.value) { 1510 return; 1511 } 1512 1513 target.value = val; 1514 1515 // Dispatch the 'change' events to make sure that any associated event 1516 // handlers pick up the changes. 1517 if (doc.createEvent && target.dispatchEvent) { 1518 var ev_change = doc.createEvent('HTMLEvents'); 1519 ev_change.initEvent('change', true, true); 1520 target.dispatchEvent(ev_change); 1521 } 1522 }; 1523 1524 /** 1525 * Zoom into the image. 1526 * 1527 * @param {mixed} ev An event object which might have the <var>shiftKey</var> 1528 * property. If the property evaluates to true, then the zoom level will 1529 * increase twice more than normal. 1530 * 1531 * @returns {Boolean} True if the operation was successful, or false if not. 1532 * 1533 * @see PaintWeb#imageZoomTo The method used for changing the zoom level. 1534 * @see PaintWeb.config.zoomStep The value used for increasing the zoom level. 1535 */ 1536 this.imageZoomIn = function (ev) { 1537 if (ev && ev.shiftKey) { 1538 _self.config.imageZoomStep *= 2; 1539 } 1540 1541 var res = _self.imageZoomTo('+'); 1542 1543 if (ev && ev.shiftKey) { 1544 _self.config.imageZoomStep /= 2; 1545 } 1546 1547 return res; 1548 }; 1549 1550 /** 1551 * Zoom out of the image. 1552 * 1553 * @param {mixed} ev An event object which might have the <var>shiftKey</var> 1554 * property. If the property evaluates to true, then the zoom level will 1555 * decrease twice more than normal. 1556 * 1557 * @returns {Boolean} True if the operation was successful, or false if not. 1558 * 1559 * @see PaintWeb#imageZoomTo The method used for changing the zoom level. 1560 * @see PaintWeb.config.zoomStep The value used for decreasing the zoom level. 1561 */ 1562 this.imageZoomOut = function (ev) { 1563 if (ev && ev.shiftKey) { 1564 _self.config.imageZoomStep *= 2; 1565 } 1566 1567 var res = _self.imageZoomTo('-'); 1568 1569 if (ev && ev.shiftKey) { 1570 _self.config.imageZoomStep /= 2; 1571 } 1572 1573 return res; 1574 }; 1575 1576 /** 1577 * Reset the image zoom level to normal. 1578 * 1579 * @returns {Boolean} True if the operation was successful, or false if not. 1580 * 1581 * @see PaintWeb#imageZoomTo The method used for changing the zoom level. 1582 */ 1583 this.imageZoomReset = function (ev) { 1584 return _self.imageZoomTo(1); 1585 }; 1586 1587 /** 1588 * Change the image zoom level. 1589 * 1590 * <p>This method dispatches the {@link pwlib.appEvent.imageZoom} application 1591 * event before zooming the image. Once the image zoom is applied, the {@link 1592 * pwlib.appEvent.canvasSizeChange} event is dispatched. 1593 * 1594 * @param {Number|String} level The level you want to zoom the image to. 1595 * 1596 * <p>If the value is a number, it must be a floating point positive number, 1597 * where 0.5 means 50%, 1 means 100% (normal) zoom, 4 means 400% and so on. 1598 * 1599 * <p>If the value is a string it must be "+" or "-". This means that the zoom 1600 * level will increase/decrease using the configured {@link 1601 * PaintWeb.config.zoomStep}. 1602 * 1603 * @returns {Boolean} True if the image zoom level changed successfully, or 1604 * false if not. 1605 */ 1606 this.imageZoomTo = function (level) { 1607 var image = this.image, 1608 config = this.config, 1609 res = this.resolution; 1610 1611 if (!level) { 1612 return false; 1613 } else if (level === '+') { 1614 level = image.zoom + config.imageZoomStep; 1615 } else if (level === '-') { 1616 level = image.zoom - config.imageZoomStep; 1617 } else if (typeof level !== 'number') { 1618 return false; 1619 } 1620 1621 if (level > config.imageZoomMax) { 1622 level = config.imageZoomMax; 1623 } else if (level < config.imageZoomMin) { 1624 level = config.imageZoomMin; 1625 } 1626 1627 if (level === image.zoom) { 1628 return true; 1629 } 1630 1631 var cancel = this.events.dispatch(new appEvent.imageZoom(level)); 1632 if (cancel) { 1633 return false; 1634 } 1635 1636 var styleWidth = image.width / res.scale * level, 1637 styleHeight = image.height / res.scale * level, 1638 bufferStyle = this.buffer.canvas.style, 1639 layerStyle = this.layer.canvas.style; 1640 1641 image.canvasScale = styleWidth / image.width; 1642 1643 // FIXME: MSIE 9 clears the Canvas element when you change the 1644 // elem.style.width/height... *argh* 1645 bufferStyle.width = layerStyle.width = styleWidth + 'px'; 1646 bufferStyle.height = layerStyle.height = styleHeight + 'px'; 1647 1648 image.zoom = level; 1649 1650 this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 1651 image.canvasScale)); 1652 1653 return true; 1654 }; 1655 1656 /** 1657 * Crop the image. 1658 * 1659 * <p>The content of the image is retained only if the browser implements the 1660 * <code>getImageData</code> and <code>putImageData</code> methods. 1661 * 1662 * <p>This method dispatches three application events: {@link 1663 * pwlib.appEvent.imageSizeChange}, {@link pwlib.appEvent.canvasSizeChange} 1664 * and {@link pwlib.appEvent.imageCrop}. The <code>imageCrop</code> event is 1665 * dispatched before the image is cropped. The <code>imageSizeChange</code> 1666 * and <code>canvasSizeChange</code> events are dispatched after the image is 1667 * cropped. 1668 * 1669 * @param {Number} cropX Image cropping start position on the x-axis. 1670 * @param {Number} cropY Image cropping start position on the y-axis. 1671 * @param {Number} cropWidth Image crop width. 1672 * @param {Number} cropHeight Image crop height. 1673 * 1674 * @returns {Boolean} True if the image was cropped successfully, or false if 1675 * not. 1676 */ 1677 this.imageCrop = function (cropX, cropY, cropWidth, cropHeight) { 1678 var bufferCanvas = this.buffer.canvas, 1679 bufferContext = this.buffer.context, 1680 image = this.image, 1681 layerCanvas = this.layer.canvas, 1682 layerContext = this.layer.context; 1683 1684 cropX = parseInt(cropX); 1685 cropY = parseInt(cropY); 1686 cropWidth = parseInt(cropWidth); 1687 cropHeight = parseInt(cropHeight); 1688 1689 if (!cropWidth || !cropHeight || isNaN(cropX) || isNaN(cropY) || 1690 isNaN(cropWidth) || isNaN(cropHeight) || cropX >= image.width || cropY 1691 >= image.height) { 1692 return false; 1693 } 1694 1695 var cancel = this.events.dispatch(new appEvent.imageCrop(cropX, cropY, 1696 cropWidth, cropHeight)); 1697 if (cancel) { 1698 return false; 1699 } 1700 1701 if (cropWidth > this.config.imageWidthMax) { 1702 cropWidth = this.config.imageWidthMax; 1703 } 1704 1705 if (cropHeight > this.config.imageHeightMax) { 1706 cropHeight = this.config.imageHeightMax; 1707 } 1708 1709 if (cropX === 0 && cropY === 0 && image.width === cropWidth && image.height 1710 === cropHeight) { 1711 return true; 1712 } 1713 1714 var layerData = null, 1715 bufferData = null, 1716 layerState = this.stateSave(layerContext), 1717 bufferState = this.stateSave(bufferContext), 1718 scaledWidth = cropWidth * image.canvasScale, 1719 scaledHeight = cropHeight * image.canvasScale, 1720 dataWidth = MathMin(image.width, cropWidth), 1721 dataHeight = MathMin(image.height, cropHeight), 1722 sumX = cropX + dataWidth, 1723 sumY = cropY + dataHeight; 1724 1725 if (sumX > image.width) { 1726 dataWidth -= sumX - image.width; 1727 } 1728 if (sumY > image.height) { 1729 dataHeight -= sumY - image.height; 1730 } 1731 1732 if (layerContext.getImageData) { 1733 // TODO: handle "out of memory" errors. 1734 try { 1735 layerData = layerContext.getImageData(cropX, cropY, dataWidth, 1736 dataHeight); 1737 } catch (err) { } 1738 } 1739 1740 if (bufferContext.getImageData) { 1741 try { 1742 bufferData = bufferContext.getImageData(cropX, cropY, dataWidth, 1743 dataHeight); 1744 } catch (err) { } 1745 } 1746 1747 bufferCanvas.style.width = layerCanvas.style.width = scaledWidth + 'px'; 1748 bufferCanvas.style.height = layerCanvas.style.height = scaledHeight + 'px'; 1749 1750 layerCanvas.width = cropWidth; 1751 layerCanvas.height = cropHeight; 1752 1753 if (layerData && layerContext.putImageData) { 1754 layerContext.putImageData(layerData, 0, 0); 1755 } 1756 1757 this.stateRestore(layerContext, layerState); 1758 state = this.stateSave(bufferContext); 1759 1760 bufferCanvas.width = cropWidth; 1761 bufferCanvas.height = cropHeight; 1762 1763 if (bufferData && bufferContext.putImageData) { 1764 bufferContext.putImageData(bufferData, 0, 0); 1765 } 1766 1767 this.stateRestore(bufferContext, bufferState); 1768 1769 image.width = cropWidth; 1770 image.height = cropHeight; 1771 1772 bufferState = layerState = layerData = bufferData = null; 1773 1774 this.events.dispatch(new appEvent.imageSizeChange(cropWidth, cropHeight)); 1775 this.events.dispatch(new appEvent.canvasSizeChange(scaledWidth, 1776 scaledHeight, image.canvasScale)); 1777 1778 return true; 1779 }; 1780 1781 /** 1782 * Save the state of a Canvas context. 1783 * 1784 * @param {CanvasRenderingContext2D} context The 2D context of the Canvas 1785 * element you want to save the state. 1786 * 1787 * @returns {Object} The object has all the state properties and values. 1788 */ 1789 this.stateSave = function (context) { 1790 if (!context || !context.canvas || !this.stateProperties) { 1791 return false; 1792 } 1793 1794 var stateObj = {}, 1795 prop = null, 1796 n = this.stateProperties.length; 1797 1798 for (var i = 0; i < n; i++) { 1799 prop = this.stateProperties[i]; 1800 stateObj[prop] = context[prop]; 1801 } 1802 1803 return stateObj; 1804 }; 1805 1806 /** 1807 * Restore the state of a Canvas context. 1808 * 1809 * @param {CanvasRenderingContext2D} context The 2D context where you want to 1810 * restore the state. 1811 * 1812 * @param {Object} stateObj The state object saved by the {@link 1813 * PaintWeb#stateSave} method. 1814 * 1815 * @returns {Boolean} True if the operation was successful, or false if not. 1816 */ 1817 this.stateRestore = function (context, stateObj) { 1818 if (!context || !context.canvas) { 1819 return false; 1820 } 1821 1822 for (var state in stateObj) { 1823 context[state] = stateObj[state]; 1824 } 1825 1826 return true; 1827 }; 1828 1829 /** 1830 * Allow shadows. This method re-enabled shadow rendering, if it was enabled 1831 * before shadows were disallowed. 1832 * 1833 * <p>The {@link pwlib.appEvent.shadowAllow} event is dispatched. 1834 */ 1835 this.shadowAllow = function () { 1836 if (this.shadowAllowed || !this.shadowSupported) { 1837 return; 1838 } 1839 1840 // Note that some daily builds of Webkit in Chromium fail to render the 1841 // shadow when context.drawImage() is used (see the this.layerUpdate()). 1842 var context = this.layer.context, 1843 cfg = this.config.shadow; 1844 1845 if (cfg.enable) { 1846 context.shadowColor = cfg.shadowColor; 1847 context.shadowOffsetX = cfg.shadowOffsetX; 1848 context.shadowOffsetY = cfg.shadowOffsetY; 1849 context.shadowBlur = cfg.shadowBlur; 1850 } 1851 1852 this.shadowAllowed = true; 1853 1854 this.events.dispatch(new appEvent.shadowAllow(true)); 1855 }; 1856 1857 /** 1858 * Disallow shadows. This method disables shadow rendering, if it is enabled. 1859 * 1860 * <p>The {@link pwlib.appEvent.shadowAllow} event is dispatched. 1861 */ 1862 this.shadowDisallow = function () { 1863 if (!this.shadowAllowed || !this.shadowSupported) { 1864 return; 1865 } 1866 1867 if (this.config.shadow.enable) { 1868 var context = this.layer.context; 1869 context.shadowColor = 'rgba(0,0,0,0)'; 1870 context.shadowOffsetX = 0; 1871 context.shadowOffsetY = 0; 1872 context.shadowBlur = 0; 1873 } 1874 1875 this.shadowAllowed = false; 1876 1877 this.events.dispatch(new appEvent.shadowAllow(false)); 1878 }; 1879 1880 /** 1881 * Update the current image layer by moving the pixels from the buffer onto 1882 * the layer. This method also adds a point into the history. 1883 * 1884 * @returns {Boolean} True if the operation was successful, or false if not. 1885 */ 1886 this.layerUpdate = function () { 1887 this.layer.context.drawImage(this.buffer.canvas, 0, 0); 1888 this.buffer.context.clearRect(0, 0, this.image.width, this.image.height); 1889 this.historyAdd(); 1890 1891 return true; 1892 }; 1893 1894 /** 1895 * Add the current image layer to the history. 1896 * 1897 * <p>Once the history state has been updated, this method dispatches the 1898 * {@link pwlib.appEvent.historyUpdate} event. 1899 * 1900 * @returns {Boolean} True if the operation was successful, or false if not. 1901 */ 1902 // TODO: some day it would be nice to implement a hybrid history system. 1903 this.historyAdd = function () { 1904 var layerContext = this.layer.context, 1905 history = this.history, 1906 prevPos = history.pos; 1907 1908 if (!layerContext.getImageData) { 1909 return false; 1910 } 1911 1912 // We are in an undo-step, trim until the end, eliminating any possible redo-steps. 1913 if (prevPos < history.states.length) { 1914 history.states.splice(prevPos, history.states.length); 1915 } 1916 1917 // TODO: in case of "out of memory" errors... I should show up some error. 1918 try { 1919 history.states.push(layerContext.getImageData(0, 0, this.image.width, 1920 this.image.height)); 1921 } catch (err) { 1922 return false; 1923 } 1924 1925 // If we have too many history ImageDatas, remove the oldest ones 1926 if ('historyLimit' in this.config && 1927 history.states.length > this.config.historyLimit) { 1928 1929 history.states.splice(0, history.states.length 1930 - this.config.historyLimit); 1931 } 1932 history.pos = history.states.length; 1933 1934 this.image.modified = true; 1935 1936 this.events.dispatch(new appEvent.historyUpdate(history.pos, prevPos, 1937 history.pos)); 1938 1939 return true; 1940 }; 1941 1942 /** 1943 * Jump to any ImageData/position in the history. 1944 * 1945 * <p>Once the history state has been updated, this method dispatches the 1946 * {@link pwlib.appEvent.historyUpdate} event. 1947 * 1948 * @param {Number|String} pos The history position to jump to. 1949 * 1950 * <p>If the value is a number, then it must point to an existing index in the 1951 * <var>{@link PaintWeb#history}.states</var> array. 1952 * 1953 * <p>If the value is a string, it must be "undo" or "redo". 1954 * 1955 * @returns {Boolean} True if the operation was successful, or false if not. 1956 */ 1957 this.historyGoto = function (pos) { 1958 var layerContext = this.layer.context, 1959 image = this.image, 1960 history = this.history; 1961 1962 if (!history.states.length || !layerContext.putImageData) { 1963 return false; 1964 } 1965 1966 var cpos = history.pos; 1967 1968 if (pos === 'undo') { 1969 pos = cpos-1; 1970 } else if (pos === 'redo') { 1971 pos = cpos+1; 1972 } 1973 1974 if (pos < 1 || pos > history.states.length) { 1975 return false; 1976 } 1977 1978 var himg = history.states[pos-1]; 1979 if (!himg) { 1980 return false; 1981 } 1982 1983 // Each image in the history can have a different size. As such, the script 1984 // must take this into consideration. 1985 var w = MathMin(image.width, himg.width), 1986 h = MathMin(image.height, himg.height); 1987 1988 layerContext.clearRect(0, 0, image.width, image.height); 1989 1990 try { 1991 // Firefox 3 does not clip the image, if needed. 1992 layerContext.putImageData(himg, 0, 0, 0, 0, w, h); 1993 1994 } catch (err) { 1995 // The workaround is to use a new canvas from which we can copy the 1996 // history image without causing any exceptions. 1997 var tmp = doc.createElement('canvas'); 1998 tmp.width = himg.width; 1999 tmp.height = himg.height; 2000 2001 var tmp2 = tmp.getContext('2d'); 2002 tmp2.putImageData(himg, 0, 0); 2003 2004 layerContext.drawImage(tmp, 0, 0); 2005 2006 tmp2 = tmp = null; 2007 delete tmp2, tmp; 2008 } 2009 2010 history.pos = pos; 2011 2012 this.events.dispatch(new appEvent.historyUpdate(pos, cpos, 2013 history.states.length)); 2014 2015 return true; 2016 }; 2017 2018 /** 2019 * Clear the image history. 2020 * 2021 * <p>This method dispatches the {@link pwlib.appEvent.historyUpdate} event. 2022 * 2023 * @private 2024 */ 2025 this.historyReset = function () { 2026 this.history.pos = 0; 2027 this.history.states = []; 2028 2029 this.events.dispatch(new appEvent.historyUpdate(0, 0, 0)); 2030 }; 2031 2032 /** 2033 * Perform horizontal/vertical line snapping. This method updates the mouse 2034 * coordinates to "snap" with the given coordinates. 2035 * 2036 * @param {Number} x The x-axis location. 2037 * @param {Number} y The y-axis location. 2038 */ 2039 this.toolSnapXY = function (x, y) { 2040 var diffx = MathAbs(_self.mouse.x - x), 2041 diffy = MathAbs(_self.mouse.y - y); 2042 2043 if (diffx > diffy) { 2044 _self.mouse.y = y; 2045 } else { 2046 _self.mouse.x = x; 2047 } 2048 }; 2049 2050 /** 2051 * Activate a drawing tool by ID. 2052 * 2053 * <p>The <var>id</var> provided must be of an existing drawing tool, one that 2054 * has been installed. 2055 * 2056 * <p>The <var>ev</var> argument is an optional DOM Event object which is 2057 * useful when dealing with different types of tool activation, either by 2058 * keyboard or by mouse events. Tool-specific code can implement different 2059 * functionality based on events. 2060 * 2061 * <p>This method dispatches the {@link pwlib.appEvent.toolPreactivate} event 2062 * before creating the new tool instance. Once the new tool is successfully 2063 * activated, the {@link pwlib.appEvent.toolActivate} event is also 2064 * dispatched. 2065 * 2066 * @param {String} id The ID of the drawing tool to be activated. 2067 * @param {Event} [ev] The DOM Event object. 2068 * 2069 * @returns {Boolean} True if the tool has been activated, or false if not. 2070 * 2071 * @see PaintWeb#toolRegister Register a new drawing tool. 2072 * @see PaintWeb#toolUnregister Unregister a drawing tool. 2073 * 2074 * @see pwlib.tools The object holding all the drawing tools. 2075 * @see pwlib.appEvent.toolPreactivate 2076 * @see pwlib.appEvent.toolActivate 2077 */ 2078 this.toolActivate = function (id, ev) { 2079 if (!id || !(id in pwlib.tools) || typeof pwlib.tools[id] !== 'function') { 2080 return false; 2081 } 2082 2083 var tool = pwlib.tools[id], 2084 prevId = this.tool ? this.tool._id : null; 2085 2086 if (prevId && this.tool instanceof pwlib.tools[id]) { 2087 return true; 2088 } 2089 2090 var cancel = this.events.dispatch(new appEvent.toolPreactivate(id, prevId)); 2091 if (cancel) { 2092 return false; 2093 } 2094 2095 var tool_obj = new tool(this, ev); 2096 if (!tool_obj) { 2097 return false; 2098 } 2099 2100 /* 2101 * Each tool can implement its own mouse and keyboard events handler. 2102 * Additionally, tool objects can implement handlers for the deactivation 2103 * and activation events. 2104 * Given tool1 is active and tool2 is going to be activated, then the 2105 * following event handlers will be called: 2106 * 2107 * tool2.preActivate 2108 * tool1.deactivate 2109 * tool2.activate 2110 * 2111 * In the "preActivate" event handler you can cancel the tool activation by 2112 * returning a value which evaluates to false. 2113 */ 2114 2115 if ('preActivate' in tool_obj && !tool_obj.preActivate(ev)) { 2116 tool_obj = null; 2117 return false; 2118 } 2119 2120 // Deactivate the previously active tool 2121 if (this.tool && 'deactivate' in this.tool) { 2122 this.tool.deactivate(ev); 2123 } 2124 2125 this.tool = tool_obj; 2126 2127 this.mouse.buttonDown = false; 2128 2129 // Besides the "constructor", each tool can also have code which is run 2130 // after the deactivation of the previous tool. 2131 if ('activate' in this.tool) { 2132 this.tool.activate(ev); 2133 } 2134 2135 this.events.dispatch(new appEvent.toolActivate(id, prevId)); 2136 2137 return true; 2138 }; 2139 2140 /** 2141 * Register a new drawing tool into PaintWeb. 2142 * 2143 * <p>This method dispatches the {@link pwlib.appEvent.toolRegister} 2144 * application event. 2145 * 2146 * @param {String} id The ID of the new tool. The tool object must exist in 2147 * {@link pwlib.tools}. 2148 * 2149 * @returns {Boolean} True if the tool was successfully registered, or false 2150 * if not. 2151 * 2152 * @see PaintWeb#toolUnregister allows you to unregister tools. 2153 * @see pwlib.tools Holds all the drawing tools. 2154 * @see pwlib.appEvent.toolRegister 2155 */ 2156 this.toolRegister = function (id) { 2157 if (typeof id !== 'string' || !id) { 2158 return false; 2159 } 2160 2161 // TODO: it would be very nice to create the tool instance on register, for 2162 // further extensibility. 2163 2164 var tool = pwlib.tools[id]; 2165 if (typeof tool !== 'function') { 2166 return false; 2167 } 2168 2169 tool.prototype._id = id; 2170 2171 this.events.dispatch(new appEvent.toolRegister(id)); 2172 2173 if (!this.tool && id === this.config.toolDefault) { 2174 return this.toolActivate(id); 2175 } else { 2176 return true; 2177 } 2178 }; 2179 2180 /** 2181 * Unregister a drawing tool from PaintWeb. 2182 * 2183 * <p>This method dispatches the {@link pwlib.appEvent.toolUnregister} 2184 * application event. 2185 * 2186 * @param {String} id The ID of the tool you want to unregister. 2187 * 2188 * @returns {Boolean} True if the tool was unregistered, or false if it does 2189 * not exist or some error occurred. 2190 * 2191 * @see PaintWeb#toolRegister allows you to register new drawing tools. 2192 * @see pwlib.tools Holds all the drawing tools. 2193 * @see pwlib.appEvent.toolUnregister 2194 */ 2195 this.toolUnregister = function (id) { 2196 if (typeof id !== 'string' || !id || !(id in pwlib.tools)) { 2197 return false; 2198 } 2199 2200 this.events.dispatch(new appEvent.toolUnregister(id)); 2201 2202 return true; 2203 }; 2204 2205 /** 2206 * Register a new extension into PaintWeb. 2207 * 2208 * <p>If the extension object being constructed has the 2209 * <code>extensionRegister()</code> method, then it will be invoked, allowing 2210 * any custom extension registration code to run. If the method returns false, 2211 * then the extension will not be registered. 2212 * 2213 * <p>Once the extension is successfully registered, this method dispatches 2214 * the {@link pwlib.appEvent.extensionRegister} application event. 2215 * 2216 * @param {String} id The ID of the new extension. The extension object 2217 * constructor must exist in {@link pwlib.extensions}. 2218 * 2219 * @returns {Boolean} True if the extension was successfully registered, or 2220 * false if not. 2221 * 2222 * @see PaintWeb#extensionUnregister allows you to unregister extensions. 2223 * @see PaintWeb#extensions Holds all the instances of registered extensions. 2224 * @see pwlib.extensions Holds all the extension classes. 2225 */ 2226 this.extensionRegister = function (id) { 2227 if (typeof id !== 'string' || !id) { 2228 return false; 2229 } 2230 2231 var func = pwlib.extensions[id]; 2232 if (typeof func !== 'function') { 2233 return false; 2234 } 2235 2236 func.prototype._id = id; 2237 2238 var obj = new func(_self); 2239 2240 if ('extensionRegister' in obj && !obj.extensionRegister()) { 2241 return false; 2242 } 2243 2244 this.extensions[id] = obj; 2245 this.events.dispatch(new appEvent.extensionRegister(id)); 2246 2247 return true; 2248 }; 2249 2250 /** 2251 * Unregister an extension from PaintWeb. 2252 * 2253 * <p>If the extension object being destructed has the 2254 * <code>extensionUnregister()</code> method, then it will be invoked, 2255 * allowing any custom extension removal code to run. 2256 * 2257 * <p>Before the extension is unregistered, this method dispatches the {@link 2258 * pwlib.appEvent.extensionUnregister} application event. 2259 * 2260 * @param {String} id The ID of the extension object you want to unregister. 2261 * 2262 * @returns {Boolean} True if the extension was removed, or false if it does 2263 * not exist or some error occurred. 2264 * 2265 * @see PaintWeb#extensionRegister allows you to register new extensions. 2266 * @see PaintWeb#extensions Holds all the instances of registered extensions. 2267 * @see pwlib.extensions Holds all the extension classes. 2268 */ 2269 this.extensionUnregister = function (id) { 2270 if (typeof id !== 'string' || !id || !(id in this.extensions)) { 2271 return false; 2272 } 2273 2274 this.events.dispatch(new appEvent.extensionUnregister(id)); 2275 2276 if ('extensionUnregister' in this.extensions[id]) { 2277 this.extensions[id].extensionUnregister(); 2278 } 2279 delete this.extensions[id]; 2280 2281 return true; 2282 }; 2283 2284 /** 2285 * Register a new command in PaintWeb. Commands are simple function objects 2286 * which can be invoked by keyboard shortcuts or by GUI elements. 2287 * 2288 * <p>Once the command is successfully registered, this method dispatches the 2289 * {@link pwlib.appEvent.commandRegister} application event. 2290 * 2291 * @param {String} id The ID of the new command. 2292 * @param {Function} func The command function. 2293 * 2294 * @returns {Boolean} True if the command was successfully registered, or 2295 * false if not. 2296 * 2297 * @see PaintWeb#commandUnregister allows you to unregister commands. 2298 * @see PaintWeb#commands Holds all the registered commands. 2299 */ 2300 this.commandRegister = function (id, func) { 2301 if (typeof id !== 'string' || !id || typeof func !== 'function' || id in 2302 this.commands) { 2303 return false; 2304 } 2305 2306 this.commands[id] = func; 2307 this.events.dispatch(new appEvent.commandRegister(id)); 2308 2309 return true; 2310 }; 2311 2312 /** 2313 * Unregister a command from PaintWeb. 2314 * 2315 * <p>Before the command is unregistered, this method dispatches the {@link 2316 * pwlib.appEvent.commandUnregister} application event. 2317 * 2318 * @param {String} id The ID of the command you want to unregister. 2319 * 2320 * @returns {Boolean} True if the command was removed successfully, or false 2321 * if not. 2322 * 2323 * @see PaintWeb#commandRegister allows you to register new commands. 2324 * @see PaintWeb#commands Holds all the registered commands. 2325 */ 2326 this.commandUnregister = function (id) { 2327 if (typeof id !== 'string' || !id || !(id in this.commands)) { 2328 return false; 2329 } 2330 2331 this.events.dispatch(new appEvent.commandUnregister(id)); 2332 2333 delete this.commands[id]; 2334 2335 return true; 2336 }; 2337 2338 /** 2339 * Load a script into the document. 2340 * 2341 * @param {String} url The script URL you want to insert. 2342 * @param {Function} [handler] The <code>load</code> event handler you want. 2343 */ 2344 this.scriptLoad = function (url, handler) { 2345 if (!handler) { 2346 var elem = doc.createElement('script'); 2347 elem.type = 'text/javascript'; 2348 elem.src = url; 2349 this.elems.head.appendChild(elem); 2350 return; 2351 } 2352 2353 // huh, use XHR then eval() the code. 2354 // browsers do not dispatch the 'load' event reliably for script elements. 2355 2356 /** @ignore */ 2357 var xhr = new XMLHttpRequest(); 2358 2359 /** @ignore */ 2360 xhr.onreadystatechange = function () { 2361 if (!xhr || xhr.readyState !== 4) { 2362 return; 2363 2364 } else if ((xhr.status !== 304 && xhr.status !== 200) || 2365 !xhr.responseText) { 2366 handler(false, xhr); 2367 2368 } else { 2369 try { 2370 eval.call(win, xhr.responseText); 2371 } catch (err) { 2372 eval(xhr.responseText, win); 2373 } 2374 handler(true, xhr); 2375 } 2376 2377 xhr = null; 2378 }; 2379 2380 xhr.open('GET', url); 2381 xhr.send(''); 2382 }; 2383 2384 /** 2385 * Insert a stylesheet into the document. 2386 * 2387 * @param {String} id The stylesheet ID. This is used to avoid inserting the 2388 * same style in the document. 2389 * @param {String} url The URL of the stylesheet you want to insert. 2390 * @param {String} [media='screen, projection'] The media attribute. 2391 * @param {Function} [handler] The <code>load</code> event handler. 2392 */ 2393 this.styleLoad = function (id, url, media, handler) { 2394 id = 'paintweb_style_' + id; 2395 2396 var elem = doc.getElementById(id); 2397 if (elem) { 2398 return; 2399 } 2400 2401 if (!media) { 2402 media = 'screen, projection'; 2403 } 2404 2405 elem = doc.createElement('link'); 2406 2407 if (handler) { 2408 elem.addEventListener('load', handler, false); 2409 } 2410 2411 elem.id = id; 2412 elem.rel = 'stylesheet'; 2413 elem.type = 'text/css'; 2414 elem.media = media; 2415 elem.href = url; 2416 2417 this.elems.head.appendChild(elem); 2418 }; 2419 2420 /** 2421 * Perform action undo. 2422 * 2423 * @returns {Boolean} True if the operation was successful, or false if not. 2424 * 2425 * @see PaintWeb#historyGoto The method invoked by this command. 2426 */ 2427 this.historyUndo = function () { 2428 return _self.historyGoto('undo'); 2429 }; 2430 2431 /** 2432 * Perform action redo. 2433 * 2434 * @returns {Boolean} True if the operation was successful, or false if not. 2435 * 2436 * @see PaintWeb#historyGoto The method invoked by this command. 2437 */ 2438 this.historyRedo = function () { 2439 return _self.historyGoto('redo'); 2440 }; 2441 2442 /** 2443 * Load an image. By loading an image the history is cleared and the Canvas 2444 * dimensions are updated to fit the new image. 2445 * 2446 * <p>This method dispatches two application events: {@link 2447 * pwlib.appEvent.imageSizeChange} and {@link 2448 * pwlib.appEvent.canvasSizeChange}. 2449 * 2450 * @param {Element} importImage The image element you want to load into the 2451 * Canvas. 2452 * 2453 * @returns {Boolean} True if the operation was successful, or false if not. 2454 */ 2455 this.imageLoad = function (importImage) { 2456 if (!importImage || !importImage.width || !importImage.height || 2457 importImage.nodeType !== this.ELEMENT_NODE || 2458 !pwlib.isSameHost(importImage.src, win.location.host)) { 2459 return false; 2460 } 2461 2462 this.historyReset(); 2463 2464 var layerContext = this.layer.context, 2465 layerCanvas = this.layer.canvas, 2466 layerStyle = layerCanvas.style, 2467 bufferCanvas = this.buffer.canvas, 2468 bufferStyle = bufferCanvas.style, 2469 image = this.image, 2470 styleWidth = importImage.width * image.canvasScale, 2471 styleHeight = importImage.height * image.canvasScale, 2472 result = true; 2473 2474 bufferCanvas.width = layerCanvas.width = importImage.width; 2475 bufferCanvas.height = layerCanvas.height = importImage.height; 2476 2477 try { 2478 layerContext.drawImage(importImage, 0, 0); 2479 } catch (err) { 2480 result = false; 2481 bufferCanvas.width = layerCanvas.width = image.width; 2482 bufferCanvas.height = layerCanvas.height = image.height; 2483 } 2484 2485 if (result) { 2486 image.width = importImage.width; 2487 image.height = importImage.height; 2488 // FIXME: MSIE 9 clears the Canvas element when you change the 2489 // elem.style.width/height... *argh* 2490 bufferStyle.width = layerStyle.width = styleWidth + 'px'; 2491 bufferStyle.height = layerStyle.height = styleHeight + 'px'; 2492 _self.config.imageLoad = importImage; 2493 2494 this.events.dispatch(new appEvent.imageSizeChange(image.width, 2495 image.height)); 2496 2497 this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 2498 image.canvasScale)); 2499 } 2500 2501 this.historyAdd(); 2502 image.modified = false; 2503 2504 return result; 2505 }; 2506 2507 /** 2508 * Clear the image. 2509 */ 2510 this.imageClear = function (ev) { 2511 var layerContext = _self.layer.context, 2512 image = _self.image; 2513 2514 layerContext.clearRect(0, 0, image.width, image.height); 2515 2516 // Set the configured background color. 2517 var fillStyle = layerContext.fillStyle; 2518 layerContext.fillStyle = _self.config.backgroundColor; 2519 layerContext.fillRect(0, 0, image.width, image.height); 2520 layerContext.fillStyle = fillStyle; 2521 2522 _self.historyAdd(); 2523 }; 2524 2525 /** 2526 * Save the image. 2527 * 2528 * <p>This method dispatches the {@link pwlib.appEvent.imageSave} event. 2529 * 2530 * <p><strong>Note:</strong> the "Save image" operation relies on integration 2531 * extensions. A vanilla configuration of PaintWeb will simply open the the 2532 * image in a new tab using a data: URL. You must have some event listener for 2533 * the <code>imageSave</code> event and you must prevent the default action. 2534 * 2535 * <p>If the default action for the <code>imageSave</code> application event 2536 * is not prevented, then this method will also dispatch the {@link 2537 * pwlib.appEvent.imageSaveResult} application event. 2538 * 2539 * <p>Your event handler for the <code>imageSave</code> event must dispatch 2540 * the <code>imageSaveResult</code> event. 2541 * 2542 * @param {String} [type="auto"] Image MIME type. This tells the browser which 2543 * format to use when saving the image. If the image format type is not 2544 * supported, then the image is saved as PNG. 2545 * 2546 * <p>You can use the resulting data URL to check which is the actual image 2547 * format. 2548 * 2549 * <p>When <var>type</var> is "auto" then PaintWeb checks the type of the 2550 * image currently loaded ({@link PaintWeb.config.imageLoad}). If the format 2551 * is recognized, then the same format is used to save the image. 2552 * 2553 * @returns {Boolean} True if the operation was successful, or false if not. 2554 */ 2555 this.imageSave = function (type) { 2556 var canvas = _self.layer.canvas, 2557 cfg = _self.config, 2558 img = _self.image, 2559 imageLoad = _self.config.imageLoad, 2560 ext = 'png', idata = null, src = null, pos; 2561 2562 if (!canvas.toDataURL) { 2563 return false; 2564 } 2565 2566 var extMap = {'jpg' : 'image/jpeg', 'jpeg' : 'image/jpeg', 'png' 2567 : 'image/png', 'gif' : 'image/gif'}; 2568 2569 // Detect the MIME type of the image currently loaded. 2570 if (typeof type !== 'string' || !type) { 2571 if (imageLoad && imageLoad.src && imageLoad.src.substr(0, 5) !== 'data:') { 2572 src = imageLoad.src; 2573 pos = src.indexOf('?'); 2574 if (pos !== -1) { 2575 src = src.substr(0, pos); 2576 } 2577 ext = src.substr(src.lastIndexOf('.') + 1).toLowerCase(); 2578 } 2579 2580 type = extMap[ext] || 'image/png'; 2581 } 2582 2583 // We consider that other formats than PNG do not support transparencies. 2584 // Thus, we create a new Canvas element for which we set the configured 2585 // background color, and we render the image onto it. 2586 if (type !== 'image/png') { 2587 canvas = doc.createElement('canvas'); 2588 var context = canvas.getContext('2d'); 2589 2590 canvas.width = img.width; 2591 canvas.height = img.height; 2592 2593 context.fillStyle = cfg.backgroundColor; 2594 context.fillRect(0, 0, img.width, img.height); 2595 context.drawImage(_self.layer.canvas, 0, 0); 2596 2597 context = null; 2598 } 2599 2600 try { 2601 // canvas.toDataURL('image/jpeg', quality) fails in Gecko due to security 2602 // concerns, uh-oh. 2603 if (type === 'image/jpeg' && !pwlib.browser.gecko) { 2604 idata = canvas.toDataURL(type, cfg.jpegSaveQuality); 2605 } else { 2606 idata = canvas.toDataURL(type); 2607 } 2608 } catch (err) { 2609 alert(lang.errorImageSave + "\n" + err); 2610 return false; 2611 } 2612 2613 canvas = null; 2614 2615 if (!idata || idata === 'data:,') { 2616 return false; 2617 } 2618 2619 var ev = new appEvent.imageSave(idata, img.width, img.height), 2620 cancel = _self.events.dispatch(ev); 2621 2622 if (cancel) { 2623 return true; 2624 } 2625 2626 var imgwin = _self.win.open(); 2627 if (!imgwin) { 2628 return false; 2629 } 2630 2631 imgwin.location = idata; 2632 idata = null; 2633 2634 _self.events.dispatch(new appEvent.imageSaveResult(true)); 2635 2636 return true; 2637 }; 2638 2639 /** 2640 * The <code>imageSaveResult</code> application event handler. This method 2641 * PaintWeb-related stuff: for example, the {@link PaintWeb.image.modified} 2642 * flag is turned to false. 2643 * 2644 * @private 2645 * 2646 * @param {pwlib.appEvent.imageSaveResult} ev The application event object. 2647 * 2648 * @see {PaintWeb#imageSave} The method which allows you to save the image. 2649 */ 2650 this.imageSaveResultHandler = function (ev) { 2651 if (ev.successful) { 2652 _self.image.modified = false; 2653 } 2654 }; 2655 2656 /** 2657 * Swap the fill and stroke styles. This is just like in Photoshop, if the 2658 * user presses X, the fill/stroke colors are swapped. 2659 * 2660 * <p>This method dispatches the {@link pwlib.appEvent.configChange} event 2661 * twice for each color (strokeStyle and fillStyle). 2662 */ 2663 this.swapFillStroke = function () { 2664 var fillStyle = _self.config.fillStyle, 2665 strokeStyle = _self.config.strokeStyle; 2666 2667 _self.config.fillStyle = strokeStyle; 2668 _self.config.strokeStyle = fillStyle; 2669 2670 var ev = new appEvent.configChange(strokeStyle, fillStyle, 'fillStyle', '', 2671 _self.config); 2672 2673 _self.events.dispatch(ev); 2674 2675 ev = new appEvent.configChange(fillStyle, strokeStyle, 'strokeStyle', '', 2676 _self.config); 2677 2678 _self.events.dispatch(ev); 2679 }; 2680 2681 /** 2682 * Select all the pixels. This activates the selection tool, and selects the 2683 * entire image. 2684 * 2685 * @param {Event} [ev] The DOM Event object which generated the request. 2686 * @returns {Boolean} True if the operation was successful, or false if not. 2687 * 2688 * @see {pwlib.tools.selection.selectAll} The command implementation. 2689 */ 2690 this.selectAll = function (ev) { 2691 if (_self.toolActivate('selection', ev)) { 2692 return _self.tool.selectAll(ev); 2693 } else { 2694 return false; 2695 } 2696 }; 2697 2698 /** 2699 * Cut the available selection. This only works when the selection tool is 2700 * active and when some selection is available. 2701 * 2702 * @param {Event} [ev] The DOM Event object which generated the request. 2703 * @returns {Boolean} True if the operation was successful, or false if not. 2704 * 2705 * @see {pwlib.tools.selection.selectionCut} The command implementation. 2706 */ 2707 this.selectionCut = function (ev) { 2708 if (!_self.tool || _self.tool._id !== 'selection') { 2709 return false; 2710 } else { 2711 return _self.tool.selectionCut(ev); 2712 } 2713 }; 2714 2715 /** 2716 * Copy the available selection. This only works when the selection tool is 2717 * active and when some selection is available. 2718 * 2719 * @param {Event} [ev] The DOM Event object which generated the request. 2720 * @returns {Boolean} True if the operation was successful, or false if not. 2721 * 2722 * @see {pwlib.tools.selection.selectionCopy} The command implementation. 2723 */ 2724 this.selectionCopy = function (ev) { 2725 if (!_self.tool || _self.tool._id !== 'selection') { 2726 return false; 2727 } else { 2728 return _self.tool.selectionCopy(ev); 2729 } 2730 }; 2731 2732 /** 2733 * Paste the current clipboard image. This only works when some ImageData is 2734 * available in {@link PaintWeb#clipboard}. 2735 * 2736 * @param {Event} [ev] The DOM Event object which generated the request. 2737 * @returns {Boolean} True if the operation was successful, or false if not. 2738 * 2739 * @see {pwlib.tools.selection.clipboardPaste} The command implementation. 2740 */ 2741 this.clipboardPaste = function (ev) { 2742 if (!_self.clipboard || !_self.toolActivate('selection', ev)) { 2743 return false; 2744 } else { 2745 return _self.tool.clipboardPaste(ev); 2746 } 2747 }; 2748 2749 /** 2750 * The <code>configChange</code> application event handler. This method 2751 * updates the Canvas context properties depending on which configuration 2752 * property changed. 2753 * 2754 * @private 2755 * @param {pwlib.appEvent.configChange} ev The application event object. 2756 */ 2757 this.configChangeHandler = function (ev) { 2758 if (ev.group === 'shadow' && _self.shadowSupported && _self.shadowAllowed) { 2759 var context = _self.layer.context, 2760 cfg = ev.groupRef; 2761 2762 // Enable/disable shadows 2763 if (ev.config === 'enable') { 2764 if (ev.value) { 2765 context.shadowColor = cfg.shadowColor; 2766 context.shadowOffsetX = cfg.shadowOffsetX; 2767 context.shadowOffsetY = cfg.shadowOffsetY; 2768 context.shadowBlur = cfg.shadowBlur; 2769 } else { 2770 context.shadowColor = 'rgba(0,0,0,0)'; 2771 context.shadowOffsetX = 0; 2772 context.shadowOffsetY = 0; 2773 context.shadowBlur = 0; 2774 } 2775 return; 2776 } 2777 2778 // Do not update any context properties if shadows are not enabled. 2779 if (!cfg.enable) { 2780 return; 2781 } 2782 2783 switch (ev.config) { 2784 case 'shadowBlur': 2785 case 'shadowOffsetX': 2786 case 'shadowOffsetY': 2787 ev.value = parseInt(ev.value); 2788 case 'shadowColor': 2789 context[ev.config] = ev.value; 2790 } 2791 2792 } else if (ev.group === 'line') { 2793 switch (ev.config) { 2794 case 'lineWidth': 2795 case 'miterLimit': 2796 ev.value = parseInt(ev.value); 2797 case 'lineJoin': 2798 case 'lineCap': 2799 _self.buffer.context[ev.config] = ev.value; 2800 } 2801 2802 } else if (ev.group === 'text') { 2803 switch (ev.config) { 2804 case 'textAlign': 2805 case 'textBaseline': 2806 _self.buffer.context[ev.config] = ev.value; 2807 } 2808 2809 } else if (!ev.group) { 2810 switch (ev.config) { 2811 case 'fillStyle': 2812 case 'strokeStyle': 2813 _self.buffer.context[ev.config] = ev.value; 2814 } 2815 } 2816 }; 2817 2818 /** 2819 * Destroy a PaintWeb instance. This method allows you to unload a PaintWeb 2820 * instance. Extensions, tools and commands are unregistered, and the GUI 2821 * elements are removed. 2822 * 2823 * <p>The scripts and styles loaded are not removed, since they might be used 2824 * by other PaintWeb instances. 2825 * 2826 * <p>The {@link pwlib.appEvent.appDestroy} application event is dispatched 2827 * before the current instance is destroyed. 2828 */ 2829 this.destroy = function () { 2830 this.events.dispatch(new appEvent.appDestroy()); 2831 2832 for (var cmd in this.commands) { 2833 this.commandUnregister(cmd); 2834 } 2835 2836 for (var ext in this.extensions) { 2837 this.extensionUnregister(ext); 2838 } 2839 2840 for (var tool in this.gui.tools) { 2841 this.toolUnregister(tool); 2842 } 2843 2844 this.gui.destroy(); 2845 2846 this.initialized = PaintWeb.INIT_NOT_STARTED; 2847 }; 2848 2849 this.toString = function () { 2850 return 'PaintWeb v' + this.version + ' (build ' + this.build + ')'; 2851 }; 2852 2853 2854 preInit(); 2855 }; 2856 2857 /** 2858 * Application initialization not started. 2859 * @constant 2860 */ 2861 PaintWeb.INIT_NOT_STARTED = 0; 2862 2863 /** 2864 * Application initialization started. 2865 * @constant 2866 */ 2867 PaintWeb.INIT_STARTED = 1; 2868 2869 /** 2870 * Application initialization completed successfully. 2871 * @constant 2872 */ 2873 PaintWeb.INIT_DONE = 2; 2874 2875 /** 2876 * Application initialization failed. 2877 * @constant 2878 */ 2879 PaintWeb.INIT_ERROR = -1; 2880 2881 /** 2882 * PaintWeb base folder. This is determined automatically when the PaintWeb 2883 * script is added in a page. 2884 * @type String 2885 */ 2886 PaintWeb.baseFolder = ''; 2887 2888 (function () { 2889 var scripts = document.getElementsByTagName('script'), 2890 n = scripts.length, 2891 pos, src; 2892 2893 // Determine the baseFolder. 2894 2895 for (var i = 0; i < n; i++) { 2896 src = scripts[i].src; 2897 if (!src || !/paintweb(\.dev|\.src)?\.js/.test(src)) { 2898 continue; 2899 } 2900 2901 pos = src.lastIndexOf('/'); 2902 if (pos !== -1) { 2903 PaintWeb.baseFolder = src.substr(0, pos + 1); 2904 } 2905 2906 break; 2907 } 2908 })(); 2909 2910 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: 2911 2912