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