3 * http://www.robodesign.ro
5 * $Date: 2009-07-26 21:29:57 +0300 $
9 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
10 * @fileOverview Minimal JavaScript library which provides functionality for
11 * cross-browser compatibility support.
15 * @namespace Holds methods and properties necessary throughout the entire
21 * @namespace Holds pre-packaged files.
27 * @namespace Holds the implementation of each drawing tool.
31 * @see PaintWeb#toolRegister Register a new drawing tool into a PaintWeb
33 * @see PaintWeb#toolActivate Activate a drawing tool in a PaintWeb instance.
34 * @see PaintWeb#toolUnregister Unregister a drawing tool from a PaintWeb
37 * @see PaintWeb.config.toolDefault The default tool being activated when
38 * a PaintWeb instance is initialized.
39 * @see PaintWeb.config.tools Holds the list of tools to be loaded automatically
40 * when a PaintWeb instance is initialized.
45 * @namespace Holds all the PaintWeb extensions.
48 * @see PaintWeb#extensionRegister Register a new extension into a PaintWeb
50 * @see PaintWeb#extensionUnregister Unregister an extension from a PaintWeb
52 * @see PaintWeb.config.extensions Holds the list of extensions to be loaded
53 * automatically when a PaintWeb instance is initialized.
55 pwlib.extensions = {};
58 * This function extends objects.
61 * <code>var <var>obj1</var> = {a: 'a1', b: 'b1', d: 'd1'},
62 * <var>obj2</var> = {a: 'a2', b: 'b2', c: 'c2'};
64 * pwlib.extend(<var>obj1</var>, <var>obj2</var>);</code>
66 * // Now <var>obj1.c == 'c2'</var>, while <var>obj1.a</var>, <var>obj1.b</var>
67 * // and <var>obj1.d</var> remain the same.
69 * // If <code>pwlib.extend(true, <var>obj1</var>, <var>obj2</var>)</code> is
70 * // called, then <var>obj1.a</var>, <var>obj1.b</var>, <var>obj1.c</var>
71 * // become all the same as in <var>obj2</var>.
74 * <code>var <var>obj1</var> = {a: 'a1', b: 'b1', extend: pwlib.extend};
75 * <var>obj1</var>.extend({c: 'c1', d: 'd1'});</code>
77 * // In this case the destination object which is to be extend is
80 * @param {Boolean} [overwrite=false] If the first argument is a boolean, then
81 * it will be considered as a boolean flag for overwriting (or not) any existing
82 * methods and properties in the destination object. Thus, any method and
83 * property from the source object will take over those in the destination. The
84 * argument is optional, and if it's omitted, then no method/property will be
87 * @param {Object} [destination=this] The second argument is the optional
88 * destination object: the object which will be extended. By default, the
89 * <var>this</var> object will be extended.
91 * @param {Object} source The third argument must provide list of methods and
92 * properties which will be added to the destination object.
94 pwlib.extend = function () {
95 var name, src, sval, dval;
97 if (typeof arguments[0] === 'boolean') {
107 if (typeof src === 'undefined') {
112 if (typeof dest === 'undefined') {
119 if (force || typeof dval === 'undefined') {
126 * Retrieve a string formatted with the provided variables.
128 * <p>The language string must be available in the global <var>lang</var>
131 * <p>The string can contain any number of variables in the form of
132 * <code>{var_name}</code>.
135 * lang.table_cells = "The table {name} has {n} cells.";
138 * console.log(pwlib.strf(lang.table_cells, {'name' : 'tbl1', 'n' : 11}));
139 * // The output is 'The table tbl1 has 11 cells.'
141 * @param {String} str The string you want to output.
143 * @param {Object} [vars] The variables you want to set in the language string.
145 * @returns {String} The string updated with the variables you provided.
147 pwlib.strf = function (str, vars) {
155 re = new RegExp('{' + i + '}', 'g');
156 str = str.replace(re, vars[i]);
163 * Parse a JSON string. This method uses the global JSON parser provided by
164 * the browser natively. The small difference is that this method allows
165 * normal JavaScript comments in the JSON string.
167 * @param {String} str The JSON string to parse.
168 * @returns The JavaScript object that was parsed.
170 pwlib.jsonParse = function (str) {
171 str = str.replace(/\s*\/\*(\s|.)+?\*\//g, '').
172 replace(/^\s*\/\/.*$/gm, '');
174 return JSON.parse(str);
178 * Load a file from a given URL using XMLHttpRequest.
180 * @param {String} url The URL you want to load.
182 * @param {Function} handler The <code>onreadystatechange</code> event handler
183 * for the XMLHttpRequest object. Your event handler will always receive the
184 * XMLHttpRequest object as the first parameter.
186 * @param {String} [method="GET"] The HTTP method to use for loading the URL.
188 * @param {String} [send] The string you want to send in an HTTP POST request.
190 * @param {Object} [headers] An object holding the header names and values you
191 * want to set for the request.
193 * @returns {XMLHttpRequest} The XMLHttpRequest object created by this method.
195 pwlib.xhrLoad = function (url, handler, method, send, headers) {
197 throw new TypeError('The first argument must be a string!');
212 var xhr = new XMLHttpRequest();
213 xhr.onreadystatechange = function () { handler(xhr); };
214 xhr.open(method, url);
216 for (var header in headers) {
217 xhr.setRequestHeader(header, headers[header]);
226 * Check if an URL points to a resource from the same host as the desired one.
228 * <p>Note that data URIs always return true.
230 * @param {String} url The URL you want to check.
231 * @param {String} host The host you want in the URL.
233 * @returns {Boolean} True if the <var>url</var> points to a resource from the
234 * <var>host</var> given, or false otherwise.
236 pwlib.isSameHost = function (url, host) {
241 var pos = url.indexOf(':'),
242 proto = url.substr(0, pos + 1).toLowerCase();
244 if (proto === 'data:') {
248 if (proto !== 'http:' && proto !== 'https:') {
252 var urlHost = url.replace(/^https?:\/\//i, '');
253 pos = urlHost.indexOf('/');
255 urlHost = urlHost.substr(0, pos);
258 if (urlHost !== host) {
266 * @class Custom application event.
268 * @param {String} type Event type.
269 * @param {Boolean} [cancelable=false] Tells if the event can be cancelled or
272 * @throws {TypeError} If the <var>type</var> parameter is not a string.
273 * @throws {TypeError} If the <var>cancelable</var> parameter is not a string.
275 * @see pwlib.appEvents for the application events interface which allows adding
276 * and removing event listeners.
278 pwlib.appEvent = function (type, cancelable) {
279 if (typeof type !== 'string') {
280 throw new TypeError('The first argument must be a string');
281 } else if (typeof cancelable === 'undefined') {
283 } else if (typeof cancelable !== 'boolean') {
284 throw new TypeError('The second argument must be a boolean');
288 * Event target object.
294 * Tells if the event can be cancelled or not.
297 this.cancelable = cancelable;
300 * Tells if the event has the default action prevented or not.
303 this.defaultPrevented = false;
312 * Prevent the default action of the event.
314 this.preventDefault = function () {
316 this.defaultPrevented = true;
321 * Stop the event propagation to other event handlers.
323 this.stopPropagation = function () {
324 this.propagationStopped_ = true;
329 * @class Application initialization event. This event is not cancelable.
331 * @augments pwlib.appEvent
333 * @param {Number} state The initialization state.
334 * @param {String} [errorMessage] The error message, if any.
336 * @throws {TypeError} If the <var>state</var> is not a number.
338 pwlib.appEvent.appInit = function (state, errorMessage) {
339 if (typeof state !== 'number') {
340 throw new TypeError('The first argument must be a number.');
344 * Application initialization not started.
347 this.INIT_NOT_STARTED = 0;
350 * Application initialization started.
353 this.INIT_STARTED = 1;
356 * Application initialization completed successfully.
362 * Application initialization failed.
365 this.INIT_ERROR = -1;
368 * Initialization state.
374 * Initialization error message, if any.
377 this.errorMessage = errorMessage || null;
379 pwlib.appEvent.call(this, 'appInit');
383 * @class Application destroy event. This event is not cancelable.
385 * @augments pwlib.appEvent
387 pwlib.appEvent.appDestroy = function () {
388 pwlib.appEvent.call(this, 'appDestroy');
392 * @class GUI show event. This event is not cancelable.
394 * @augments pwlib.appEvent
396 pwlib.appEvent.guiShow = function () {
397 pwlib.appEvent.call(this, 'guiShow');
401 * @class GUI hide event. This event is not cancelable.
403 * @augments pwlib.appEvent
405 pwlib.appEvent.guiHide = function () {
406 pwlib.appEvent.call(this, 'guiHide');
410 * @class Tool preactivation event. This event is cancelable.
412 * @augments pwlib.appEvent
414 * @param {String} id The ID of the new tool being activated.
415 * @param {String|null} prevId The ID of the previous tool.
417 * @throws {TypeError} If the <var>id</var> is not a string.
418 * @throws {TypeError} If the <var>prevId</var> is not a string or null.
420 pwlib.appEvent.toolPreactivate = function (id, prevId) {
421 if (typeof id !== 'string') {
422 throw new TypeError('The first argument must be a string.');
423 } else if (prevId !== null && typeof prevId !== 'string') {
424 throw new TypeError('The second argument must be a string or null.');
437 this.prevId = prevId;
439 pwlib.appEvent.call(this, 'toolPreactivate', true);
443 * @class Tool activation event. This event is not cancelable.
445 * @augments pwlib.appEvent
447 * @param {String} id The ID the tool which was activated.
448 * @param {String|null} prevId The ID of the previous tool.
450 * @throws {TypeError} If the <var>id</var> is not a string.
451 * @throws {TypeError} If the <var>prevId</var> is not a string or null.
453 pwlib.appEvent.toolActivate = function (id, prevId) {
454 if (typeof id !== 'string') {
455 throw new TypeError('The first argument must be a string.');
456 } else if (prevId !== null && typeof prevId !== 'string') {
457 throw new TypeError('The second argument must be a string or null.');
470 this.prevId = prevId;
472 pwlib.appEvent.call(this, 'toolActivate');
476 * @class Tool registration event. This event is not cancelable.
478 * @augments pwlib.appEvent
480 * @param {String} id The ID of the tool being registered in an active PaintWeb
483 * @throws {TypeError} If the <var>id</var> is not a string.
485 pwlib.appEvent.toolRegister = function (id) {
486 if (typeof id !== 'string') {
487 throw new TypeError('The first argument must be a string.');
496 pwlib.appEvent.call(this, 'toolRegister');
500 * @class Tool removal event. This event is not cancelable.
502 * @augments pwlib.appEvent
504 * @param {String} id The ID of the tool being unregistered in an active
507 * @throws {TypeError} If the <var>id</var> is not a string.
509 pwlib.appEvent.toolUnregister = function (id) {
510 if (typeof id !== 'string') {
511 throw new TypeError('The first argument must be a string.');
520 pwlib.appEvent.call(this, 'toolUnregister');
524 * @class Extension registration event. This event is not cancelable.
526 * @augments pwlib.appEvent
528 * @param {String} id The ID of the extension being registered in an active
531 * @throws {TypeError} If the <var>id</var> is not a string.
533 pwlib.appEvent.extensionRegister = function (id) {
534 if (typeof id !== 'string') {
535 throw new TypeError('The first argument must be a string.');
544 pwlib.appEvent.call(this, 'extensionRegister');
548 * @class Extension removal event. This event is not cancelable.
550 * @augments pwlib.appEvent
552 * @param {String} id The ID of the extension being unregistered in an active
555 * @throws {TypeError} If the <var>id</var> is not a string.
557 pwlib.appEvent.extensionUnregister = function (id) {
558 if (typeof id !== 'string') {
559 throw new TypeError('The first argument must be a string.');
568 pwlib.appEvent.call(this, 'extensionUnregister');
572 * @class Command registration event. This event is not cancelable.
574 * @augments pwlib.appEvent
576 * @param {String} id The ID of the command being registered in an active
579 * @throws {TypeError} If the <var>id</var> is not a string.
581 pwlib.appEvent.commandRegister = function (id) {
582 if (typeof id !== 'string') {
583 throw new TypeError('The first argument must be a string.');
592 pwlib.appEvent.call(this, 'commandRegister');
596 * @class Command removal event. This event is not cancelable.
598 * @augments pwlib.appEvent
600 * @param {String} id The ID of the command being unregistered in an active
603 * @throws {TypeError} If the <var>id</var> is not a string.
605 pwlib.appEvent.commandUnregister = function (id) {
606 if (typeof id !== 'string') {
607 throw new TypeError('The first argument must be a string.');
616 pwlib.appEvent.call(this, 'commandUnregister');
620 * @class The image save event. This event is cancelable.
622 * @augments pwlib.appEvent
624 * @param {String} dataURL The data URL generated by the browser holding the
625 * pixels of the image being saved, in PNG format.
626 * @param {Number} width The image width.
627 * @param {Number} height The image height.
629 pwlib.appEvent.imageSave = function (dataURL, width, height) {
631 * The image saved by the browser, using the base64 encoding.
634 this.dataURL = dataURL;
646 this.height = height;
648 pwlib.appEvent.call(this, 'imageSave', true);
651 * @class The image save result event. This event is not cancelable.
653 * @augments pwlib.appEvent
655 * @param {Boolean} successful Tells if the image save was successful or not.
656 * @param {String} [url] The image address.
657 * @param {String} [urlNew] The new image address. Provide this parameter, if,
658 * for example, you allow saving images from a remote server to a local server.
659 * In such cases the image address changes.
661 pwlib.appEvent.imageSaveResult = function (successful, url, urlNew) {
663 * Tells if the image save was successful or not.
666 this.successful = successful;
675 * The new image address.
678 this.urlNew = urlNew;
680 pwlib.appEvent.call(this, 'imageSaveResult');
684 * @class History navigation event. This event is not cancelable.
686 * @augments pwlib.appEvent
688 * @param {Number} currentPos The new history position.
689 * @param {Number} previousPos The previous history position.
690 * @param {Number} states The number of history states available.
692 * @throws {TypeError} If any of the arguments are not numbers.
694 pwlib.appEvent.historyUpdate = function (currentPos, previousPos, states) {
695 if (typeof currentPos !== 'number' || typeof previousPos !== 'number' ||
696 typeof states !== 'number') {
697 throw new TypeError('All arguments must be numbers.');
701 * Current history position.
704 this.currentPos = currentPos;
707 * Previous history position.
710 this.previousPos = previousPos;
713 * History states count.
716 this.states = states;
718 pwlib.appEvent.call(this, 'historyUpdate');
722 * @class Image size change event. This event is not cancelable.
724 * @augments pwlib.appEvent
726 * @param {Number} width The new image width.
727 * @param {Number} height The new image height.
729 * @throws {TypeError} If any of the arguments are not numbers.
731 pwlib.appEvent.imageSizeChange = function (width, height) {
732 if (typeof width !== 'number' || typeof height !== 'number') {
733 throw new TypeError('Both arguments must be numbers.');
746 this.height = height;
748 pwlib.appEvent.call(this, 'imageSizeChange');
752 * @class Canvas size change event. This event is not cancelable.
754 * <p>Note that the Canvas size is not the same as the image size. Canvas size
755 * refers to the scaling of the Canvas elements being applied (due to image
756 * zooming or due to browser zoom / DPI).
758 * @augments pwlib.appEvent
760 * @param {Number} width The new Canvas style width.
761 * @param {Number} height The new Canvas style height.
762 * @param {Number} scale The new Canvas scaling factor.
764 * @throws {TypeError} If any of the arguments are not numbers.
766 pwlib.appEvent.canvasSizeChange = function (width, height, scale) {
767 if (typeof width !== 'number' || typeof height !== 'number' || typeof scale
769 throw new TypeError('All the arguments must be numbers.');
773 * New Canvas style width.
779 * New Canvas style height.
782 this.height = height;
785 * The new Canvas scaling factor.
790 pwlib.appEvent.call(this, 'canvasSizeChange');
794 * @class Image zoom event. This event is cancelable.
796 * @augments pwlib.appEvent
798 * @param {Number} zoom The new image zoom level.
800 * @throws {TypeError} If the <var>zoom</var> argument is not a number.
802 pwlib.appEvent.imageZoom = function (zoom) {
803 if (typeof zoom !== 'number') {
804 throw new TypeError('The first argument must be a number.');
808 * The new image zoom level.
813 pwlib.appEvent.call(this, 'imageZoom', true);
817 * @class Image crop event. This event is cancelable.
819 * @augments pwlib.appEvent
821 * @param {Number} x The crop start position on the x-axis.
822 * @param {Number} y The crop start position on the y-axis.
823 * @param {Number} width The cropped image width.
824 * @param {Number} height The cropped image height.
826 * @throws {TypeError} If any of the arguments are not numbers.
828 pwlib.appEvent.imageCrop = function (x, y, width, height) {
829 if (typeof x !== 'number' || typeof y !== 'number' || typeof width !==
830 'number' || typeof height !== 'number') {
831 throw new TypeError('All arguments must be numbers.');
835 * The crop start position the x-axis.
841 * The crop start position the y-axis.
847 * The cropped image width.
853 * The cropped image height.
856 this.height = height;
858 pwlib.appEvent.call(this, 'imageCrop', true);
862 * @class Configuration change event. This event is not cancelable.
864 * @augments pwlib.appEvent
866 * @param {String|Number|Boolean} value The new value.
867 * @param {String|Number|Boolean} previousValue The previous value.
868 * @param {String} config The configuration property that just changed.
869 * @param {String} group The configuration group where the property is found.
870 * @param {Object} groupRef The configuration group object reference.
872 * @throws {TypeError} If the <var>prop</var> argument is not a string.
873 * @throws {TypeError} If the <var>group</var> argument is not a string.
874 * @throws {TypeError} If the <var>groupRef</var> argument is not an object.
876 pwlib.appEvent.configChange = function (value, previousValue, config, group,
878 if (typeof config !== 'string') {
879 throw new TypeError('The third argument must be a string.');
880 } else if (typeof group !== 'string') {
881 throw new TypeError('The fourth argument must be a string.');
882 } else if (typeof groupRef !== 'object') {
883 throw new TypeError('The fifth argument must be an object.');
892 * The previous value.
894 this.previousValue = previousValue;
897 * Configuration property name.
900 this.config = config;
903 * Configuration group name.
909 * Reference to the object holding the configuration property.
912 this.groupRef = groupRef;
914 pwlib.appEvent.call(this, 'configChange');
918 * @class Canvas shadows allowed change event. This event is not cancelable.
920 * @augments pwlib.appEvent
922 * @param {Boolean} allowed Tells the new allowance value.
924 * @throws {TypeError} If the argument is not a boolean value.
926 pwlib.appEvent.shadowAllow = function (allowed) {
927 if (typeof allowed !== 'boolean') {
928 throw new TypeError('The first argument must be a boolean.');
932 * Tells if the Canvas shadows are allowed or not.
935 this.allowed = allowed;
937 pwlib.appEvent.call(this, 'shadowAllow');
941 * @class Clipboard update event. This event is not cancelable.
943 * @augments pwlib.appEvent
945 * @param {ImageData} data Holds the clipboard ImageData.
947 pwlib.appEvent.clipboardUpdate = function (data) {
949 * The clipboard image data.
954 pwlib.appEvent.call(this, 'clipboardUpdate');
958 * @class An interface for adding, removing and dispatching of custom
959 * application events.
961 * @param {Object} target_ The target for all the events.
963 * @see pwlib.appEvent to create application event objects.
965 pwlib.appEvents = function (target_) {
967 * Holds the list of event types and event handlers.
977 * Add an event listener.
979 * @param {String} type The event you want to listen for.
980 * @param {Function} handler The event handler.
982 * @returns {Number} The event ID.
984 * @throws {TypeError} If the <var>type</var> argument is not a string.
985 * @throws {TypeError} If the <var>handler</var> argument is not a function.
987 * @see pwlib.appEvents#remove to remove events.
988 * @see pwlib.appEvents#dispatch to dispatch an event.
990 this.add = function (type, handler) {
991 if (typeof type !== 'string') {
992 throw new TypeError('The first argument must be a string.');
993 } else if (typeof handler !== 'function') {
994 throw new TypeError('The second argument must be a function.');
999 if (!(type in events_)) {
1003 events_[type][id] = handler;
1009 * Remove an event listener.
1011 * @param {String} type The event type.
1012 * @param {Number} id The event ID.
1014 * @throws {TypeError} If the <var>type</var> argument is not a string.
1016 * @see pwlib.appEvents#add to add events.
1017 * @see pwlib.appEvents#dispatch to dispatch an event.
1019 this.remove = function (type, id) {
1020 if (typeof type !== 'string') {
1021 throw new TypeError('The first argument must be a string.');
1024 if (!(type in events_) || !(id in events_[type])) {
1028 delete events_[type][id];
1032 * Dispatch an event.
1034 * @param {String} type The event type.
1035 * @param {pwlib.appEvent} ev The event object.
1037 * @returns {Boolean} True if the <code>event.preventDefault()</code> has been
1038 * invoked by one of the event handlers, or false if not.
1040 * @throws {TypeError} If the <var>type</var> parameter is not a string.
1041 * @throws {TypeError} If the <var>ev</var> parameter is not an object.
1043 * @see pwlib.appEvents#add to add events.
1044 * @see pwlib.appEvents#remove to remove events.
1045 * @see pwlib.appEvent the generic event object.
1047 this.dispatch = function (ev) {
1048 if (typeof ev !== 'object') {
1049 throw new TypeError('The second argument must be an object.');
1050 } else if (typeof ev.type !== 'string') {
1051 throw new TypeError('The second argument must be an application event ' +
1055 // No event handlers.
1056 if (!(ev.type in events_)) {
1060 ev.target = target_;
1062 var id, handlers = events_[ev.type];
1063 for (id in handlers) {
1064 handlers[id].call(target_, ev);
1066 if (ev.propagationStopped_) {
1071 return ev.defaultPrevented;
1077 * @namespace Holds browser information.
1084 if (window.navigator && window.navigator.userAgent) {
1085 ua = window.navigator.userAgent.toLowerCase();
1091 pwlib.browser.opera = window.opera ? true : /\bopera\b/.test(ua);
1094 * Webkit is the render engine used primarily by Safari. It's also used by
1095 * Google Chrome and GNOME Epiphany.
1099 pwlib.browser.webkit = /\b(applewebkit|webkit)\b/.test(ua);
1102 * Firefox uses the Gecko render engine.
1106 // In some variations of the User Agent strings provided by Opera, Firefox is
1108 pwlib.browser.firefox = /\bfirefox\b/.test(ua) && !pwlib.browser.opera;
1111 * Gecko is the render engine used by Firefox and related products.
1115 // Typically, the user agent string of WebKit also mentions Gecko. Additionally,
1116 // Opera mentions Gecko for tricking some sites.
1117 pwlib.browser.gecko = /\bgecko\b/.test(ua) && !pwlib.browser.opera &&
1118 !pwlib.browser.webkit;
1121 * Microsoft Internet Explorer. The future of computing.
1125 // Again, Opera allows users to easily fake the UA.
1126 pwlib.browser.msie = /\bmsie\b/.test(ua) && !pwlib.browser.opera;
1129 * Presto is the render engine used by Opera.
1133 // Older versions of Opera did not mention Presto in the UA string.
1134 pwlib.browser.presto = /\bpresto\b/.test(ua) || pwlib.browser.opera;
1138 * Browser operating system
1142 pwlib.browser.os = (ua.match(/\b(windows|linux)\b/) || [])[1];
1145 * Tells if the browser is running on an OLPC XO. Typically, only the default
1146 * Gecko-based browser includes the OLPC XO tokens in the user agent string.
1150 pwlib.browser.olpcxo = ua.match(/\bolpc\b/) && ua.match(/\bxo\b/);
1157 * @namespace Holds methods and properties necessary for DOM manipulation.
1162 * @namespace Holds the list of virtual key identifiers and a few characters,
1163 * each being associated to a key code commonly used by Web browsers.
1167 pwlib.dom.keyNames = {
1223 * @namespace Holds the list of codes, each being associated to a virtual key
1228 pwlib.dom.keyCodes = {
1230 * For almost each key code, these comments give the key name, the
1231 * keyIdentifier from the DOM 3 Events spec and the Unicode character
1232 * information (if you would use the decimal code for direct conversion to
1233 * a character, e.g. String.fromCharCode()). Obviously, the Unicode character
1234 * information is not to be used, since these are only virtual key codes (not
1235 * really char codes) associated to key names.
1237 * Each key name in here tries to follow the same style as the defined
1238 * keyIdentifiers from the DOM 3 Events. Thus for the Page Down button,
1239 * 'PageDown' is used (not other variations like 'pag-up'), and so on.
1241 * Multiple key codes might be associated to the same key - it's not an error.
1243 * Note that this list is not an exhaustive key codes list. This means that
1244 * for key A or for key 0, the script will do String.fromCharCode(keyCode), to
1245 * determine the key. For the case of alpha-numeric keys, this works fine.
1250 * Unicode: U+0003 [End of text]
1252 * Note 1: This keyCode is only used in Safari 2 (older Webkit) for the Enter
1255 * Note 2: In Gecko this keyCode is used for the Cancel key (see
1262 * Unicode: U+0006 [Acknowledge]
1264 * Note: Taken from Gecko (DOM_VK_HELP).
1270 * Unicode: U+0008 [Backspace]
1271 * keyIdentifier: U+0008
1277 * Unicode: U+0009 [Horizontal tab]
1278 * keyIdentifier: U+0009
1284 * Unicode: U+0010 [Line feed (LF) / New line (NL) / End of line (EOL)]
1286 * Note: Taken from the Unicode characters list. If it ends up as a keyCode in
1287 * some event, it's simply considered as being the Enter key.
1292 * Key: NumPad_Center
1293 * Unicode: U+000C [Form feed]
1294 * keyIdentifier: Clear
1296 * Note 1: This keyCode is used when NumLock is off, and the user pressed the
1297 * 5 key on the numeric pad.
1299 * Note 2: Safari 2 (older Webkit) assigns this keyCode to the NumLock key
1306 * Unicode: U+000D [Carriage return (CR)]
1307 * keyIdentifier: Enter
1309 * Note 1: This is the keyCode used by most of the Web browsers when the Enter
1312 * Note 2: Gecko associates the DOM_VK_RETURN to this keyCode.
1318 * Unicode: U+000E [Shift out]
1320 * Note: Taken from Gecko (DOM_VK_ENTER).
1326 * Unicode: U+0010 [Data link escape]
1327 * keyIdentifier: Shift
1329 * Note: In older Safari (Webkit) versions Shift+Tab is assigned a different
1330 * keyCode: keyCode 25.
1336 * Unicode: U+0011 [Device control one]
1337 * keyIdentifier: Control
1343 * Unicode: U+0012 [Device control two]
1344 * keyIdentifier: Alt
1350 * Unicode: U+0013 [Device control three]
1351 * keyIdentifier: Pause
1357 * Unicode: U+0014 [Device control four]
1358 * keyIdentifier: CapsLock
1364 * Unicode: U+0018 [Cancel]
1365 * keyIdentifier: U+0018
1371 * Unicode: U+001B [Escape]
1372 * keyIdentifier: U+001B
1378 * Unicode: U+0020 [Space]
1379 * keyIdentifier: U+0020
1384 * Key: PageUp or NumPad_North_East
1385 * Unicode: U+0021 ! [Exclamation mark]
1386 * keyIdentifier: PageUp
1391 * Key: PageDown or NumPad_South_East
1392 * Unicode: U+0022 " [Quotation mark]
1393 * keyIdentifier: PageDown
1398 * Key: End or NumPad_South_West
1399 * Unicode: U+0023 # [Number sign]
1400 * keyIdentifier: PageDown
1405 * Key: Home or NumPad_North_West
1406 * Unicode: U+0024 $ [Dollar sign]
1407 * keyIdentifier: Home
1412 * Key: Left or NumPad_West
1413 * Unicode: U+0025 % [Percent sign]
1414 * keyIdentifier: Left
1419 * Key: Up or NumPad_North
1420 * Unicode: U+0026 & [Ampersand]
1426 * Key: Right or NumPad_East
1427 * Unicode: U+0027 ' [Apostrophe]
1428 * keyIdentifier: Right
1433 * Key: Down or NumPad_South
1434 * Unicode: U+0028 ( [Left parenthesis]
1435 * keyIdentifier: Down
1441 * Unicode: U+002C , [Comma]
1442 * keyIdentifier: PrintScreen
1444 //44: 'PrintScreen',
1447 * Key: Insert or NumPad_Insert
1448 * Unicode: U+002D - [Hyphen-Minus]
1449 * keyIdentifier: Insert
1454 * Key: Delete or NumPad_Delete
1455 * Unicode: U+002E . [Full stop / period]
1456 * keyIdentifier: U+007F
1462 * Unicode: U+005B [ [Left square bracket]
1463 * keyIdentifier: Win
1465 * Disabled: rarely needed.
1471 * Unicode: U+005C \ [Reverse solidus / Backslash]
1472 * keyIdentifier: Win
1477 * Key: Menu/ContextMenu
1478 * Unicode: U+005D ] [Right square bracket]
1479 * keyIdentifier: ...
1481 * Disabled: Is it Meta? Is it Menu, ContextMenu, what? Too much mess.
1483 //93: 'ContextMenu',
1487 * Unicode: U+0060 ` [Grave accent]
1494 * Unicode: U+0061 a [Latin small letter a]
1501 * Unicode: U+0062 b [Latin small letter b]
1508 * Unicode: U+0063 c [Latin small letter c]
1515 * Unicode: U+0064 d [Latin small letter d]
1522 * Unicode: U+0065 e [Latin small letter e]
1529 * Unicode: U+0066 f [Latin small letter f]
1536 * Unicode: U+0067 g [Latin small letter g]
1543 * Unicode: U+0068 h [Latin small letter h]
1550 * Unicode: U+0069 i [Latin small letter i]
1556 * Key: NumPad_Multiply
1557 * Unicode: U+0070 j [Latin small letter j]
1558 * keyIdentifier: U+002A * [Asterisk / Star]
1564 * Unicode: U+0071 k [Latin small letter k]
1565 * keyIdentifier: U+002B + [Plus]
1571 * Unicode: U+0073 m [Latin small letter m]
1572 * keyIdentifier: U+002D + [Hyphen / Minus]
1577 * Key: NumPad_Period
1578 * Unicode: U+0074 n [Latin small letter n]
1579 * keyIdentifier: U+002E . [Period]
1584 * Key: NumPad_Division
1585 * Unicode: U+0075 o [Latin small letter o]
1586 * keyIdentifier: U+002F / [Solidus / Slash]
1605 * Unicode: U+007F [Delete]
1606 * keyIdentifier: U+007F
1612 * Unicode: U+0090 [Device control string]
1613 * keyIdentifier: NumLock
1617 186: ';', // º (Masculine ordinal indicator)
1627 222: "'" // Þ (Latin capital letter thorn)
1630 //229: 'WinIME', // å or WinIME or something else in Webkit
1631 //255: 'NumLock', // ÿ, Gecko and Chrome, Windows XP in VirtualBox
1632 //376: 'NumLock' // Ÿ, Opera, Windows XP in VirtualBox
1635 if (pwlib.browser.gecko) {
1636 pwlib.dom.keyCodes[3] = 'Cancel'; // DOM_VK_CANCEL
1640 * @namespace Holds a list of common wrong key codes in Web browsers.
1644 pwlib.dom.keyCodes_fixes = {
1645 42: pwlib.dom.keyNames['*'], // char * to key *
1646 47: pwlib.dom.keyNames['/'], // char / to key /
1647 59: pwlib.dom.keyNames[';'], // char ; to key ;
1648 61: pwlib.dom.keyNames['='], // char = to key =
1649 96: 48, // NumPad_0 to char 0
1650 97: 49, // NumPad_1 to char 1
1651 98: 50, // NumPad_2 to char 2
1652 99: 51, // NumPad_3 to char 3
1653 100: 52, // NumPad_4 to char 4
1654 101: 53, // NumPad_5 to char 5
1655 102: 54, // NumPad_6 to char 6
1656 103: 55, // NumPad_7 to char 7
1657 104: 56, // NumPad_8 to char 8
1658 105: 57, // NumPad_9 to char 9
1659 //106: 56, // NumPad_Multiply to char 8
1660 //107: 187, // NumPad_Plus to key =
1661 109: pwlib.dom.keyNames['-'], // NumPad_Minus to key -
1662 110: pwlib.dom.keyNames['.'], // NumPad_Period to key .
1663 111: pwlib.dom.keyNames['/'] // NumPad_Division to key /
1667 * @namespace Holds the list of broken key codes generated by older Webkit
1672 pwlib.dom.keyCodes_Safari2 = {
1673 63232: pwlib.dom.keyNames.Up, // 38
1674 63233: pwlib.dom.keyNames.Down, // 40
1675 63234: pwlib.dom.keyNames.Left, // 37
1676 63235: pwlib.dom.keyNames.Right, // 39
1677 63236: pwlib.dom.keyNames.F1, // 112
1678 63237: pwlib.dom.keyNames.F2, // 113
1679 63238: pwlib.dom.keyNames.F3, // 114
1680 63239: pwlib.dom.keyNames.F4, // 115
1681 63240: pwlib.dom.keyNames.F5, // 116
1682 63241: pwlib.dom.keyNames.F6, // 117
1683 63242: pwlib.dom.keyNames.F7, // 118
1684 63243: pwlib.dom.keyNames.F8, // 119
1685 63244: pwlib.dom.keyNames.F9, // 120
1686 63245: pwlib.dom.keyNames.F10, // 121
1687 63246: pwlib.dom.keyNames.F11, // 122
1688 63247: pwlib.dom.keyNames.F12, // 123
1689 63248: pwlib.dom.keyNames.PrintScreen, // 44
1690 63272: pwlib.dom.keyNames['Delete'], // 46
1691 63273: pwlib.dom.keyNames.Home, // 36
1692 63275: pwlib.dom.keyNames.End, // 35
1693 63276: pwlib.dom.keyNames.PageUp, // 33
1694 63277: pwlib.dom.keyNames.PageDown, // 34
1695 63289: pwlib.dom.keyNames.NumLock, // 144
1696 63302: pwlib.dom.keyNames.Insert // 45
1701 * A complete keyboard events cross-browser compatibility layer.
1703 * <p>Unfortunately, due to the important differences across Web browsers,
1704 * simply using the available properties in a single keyboard event is not
1705 * enough to accurately determine the key the user pressed. Thus, one needs to
1706 * have event handlers for all keyboard-related events <code>keydown</code>,
1707 * <code>keypress</code> and <code>keyup</code>.
1709 * <p>This class provides a complete keyboard event compatibility layer. For any
1710 * new instance you provide the DOM element you want to listen events for, and
1711 * the event handlers for any of the three events <code>keydown</code>
1712 * / <code>keypress</code> / <code>keyup</code>.
1714 * <p>Your event handlers will receive the original DOM Event object, with
1715 * several new properties defined:
1718 * <li><var>event.keyCode_</var> holds the correct code for event key.
1720 * <li><var>event.key_</var> holds the key the user pressed. It can be either
1721 * a key name like "PageDown", "Delete", "Enter", or it is a character like
1724 * <li><var>event.charCode_</var> holds the Unicode character decimal code.
1726 * <li><var>event.char_</var> holds the character generated by the event.
1728 * <li><var>event.repeat_</var> is a boolean property telling if the
1729 * <code>keypress</code> event is repeated - the user is holding down the key
1730 * for a long-enough period of time to generate multiple events.
1733 * <p>The character-related properties, <var>charCode_</var> and
1734 * <var>char_</var> are only available in the <code>keypress</code> and
1735 * <code>keyup</code> event objects.
1737 * <p>This class will ensure that the <code>keypress</code> event is always
1738 * fired in Webkit and MSIE for all keys, except modifiers. For modifier keys
1739 * like <kbd>Shift</kbd>, <kbd>Control</kbd>, and <kbd>Alt</kbd>, the
1740 * <code>keypress</code> event will not be fired, even if the Web browser does
1743 * <p>Some user agents like Webkit repeat the <code>keydown</code> event while
1744 * the user holds down a key. This class will ensure that only the
1745 * <code>keypress</code> event is repeated.
1747 * <p>If you want to prevent the default action for an event, you should prevent
1748 * it on <code>keypress</code>. This class will prevent the default action for
1749 * <code>keydown</code> if need (in MSIE).
1752 * <code>var <var>klogger</var> = function (<var>ev</var>) {
1753 * console.log(<var>ev</var>.type +
1754 * ' keyCode_ ' + <var>ev</var>.keyCode_ +
1755 * ' key_ ' + <var>ev</var>.key_ +
1756 * ' charCode_ ' + <var>ev</var>.charCode_ +
1757 * ' char_ ' + <var>ev</var>.char_ +
1758 * ' repeat_ ' + <var>ev</var>.repeat_);
1761 * var <var>kbListener</var> = new pwlib.dom.KeyboardEventListener(window,
1762 * {keydown: <var>klogger</var>,
1763 * keypress: <var>klogger</var>,
1764 * keyup: <var>klogger</var>});</code>
1766 * // later when you're done...
1767 * <code><var>kbListener</var>.detach();</code>
1769 * @class A complete keyboard events cross-browser compatibility layer.
1771 * @param {Element} elem_ The DOM Element you want to listen events for.
1773 * @param {Object} handlers_ The object holding the list of event handlers
1774 * associated to the name of each keyboard event you want to listen. To listen
1775 * for all the three keyboard events use <code>{keydown: <var>fn1</var>,
1776 * keypress: <var>fn2</var>, keyup: <var>fn3</var>}</code>.
1778 * @throws {TypeError} If the <var>handlers_</var> object does not contain any
1781 pwlib.dom.KeyboardEventListener = function (elem_, handlers_) {
1785 For the keyup and keydown events the keyCode provided is that of the virtual
1786 key irrespective of other modifiers (e.g. Shift). Generally, during the
1787 keypress event, the keyCode holds the Unicode value of the character
1788 resulted from the key press, say an alphabetic upper/lower-case char,
1789 depending on the actual intent of the user and depending on the currently
1790 active keyboard layout.
1793 * Pressing p you get keyCode 80 in keyup/keydown, and keyCode 112 in
1794 keypress. String.fromCharCode(80) = 'P' and String.fromCharCode(112) = 'p'.
1795 * Pressing P you get keyCode 80 in all events.
1796 * Pressing F1 you get keyCode 112 in keyup, keydown and keypress.
1797 * Pressing 9 you get keyCode 57 in all events.
1798 * Pressing Shift+9 you get keyCode 57 in keyup/keydown, and keyCode 40 in
1799 keypress. String.fromCharCode(57) = '9' and String.fromCharCode(40) = '('.
1801 * Using the Greek layout when you press v on an US keyboard you get the
1802 output character ω. The keyup/keydown events hold keyCode 86 which is V.
1803 This does make sense, since it's the virtual key code we are dealing with
1804 - not the character code, not the result of pressing the key. The keypress
1805 event will hold keyCode 969 (ω).
1807 * Pressing NumPad_Minus you get keyCode 109 in keyup/keydown and keyCode 45
1808 in keypress. Again, this happens because in keyup/keydown you don't get the
1809 character code, you get the key code, as indicated above. For
1810 your information: String.fromCharCode(109) = 'm' and String.fromCharCode(45)
1813 Therefore, we need to map all the codes of several keys, like F1-F12,
1814 Escape, Enter, Tab, etc. This map is held by pwlib.dom.keyCodes. It
1815 associates, for example, code 112 to F1, or 13 to Enter. This map is used to
1816 detect virtual keys in all events.
1818 (This is only the general story, details about browser-specific differences
1821 If the code given by the browser doesn't appear in keyCode maps, it's used
1822 as is. The key_ returned is that of String.fromCharCode(keyCode).
1824 In all browsers we consider all events having keyCode <= 32, as being events
1825 generated by a virtual key (not a character). As such, the keyCode value is
1826 always searched in pwlib.dom.keyCodes.
1828 As you might notice from the above description, in the keypress event we
1829 cannot tell the difference from say F1 and p, because both give the code
1830 112. In Gecko and Webkit we can tell the difference because these UAs also
1831 set the charCode event property when the key generates a character. If F1 is
1832 pressed, or some other virtual key, charCode is never set.
1834 In Opera the charCode property is never set. However, the 'which' event
1835 property is not set for several virtual keys. This means we can tell the
1836 difference between a character and a virtual key. However, there's a catch:
1837 not *all* virtual keys have the 'which' property unset. Known exceptions:
1838 Backspace (8), Tab (9), Enter (13), Shift (16), Control (17), Alt (18),
1839 Pause (19), Escape (27), End (35), Home (36), Insert (45), Delete (46) and
1840 NumLock (144). Given we already consider any keyCode <= 32 being one of some
1841 virtual key, fewer exceptions remain. We only have the End, Home, Insert,
1842 Delete and the NumLock keys which cannot be 100% properly detected in the
1843 keypress event, in Opera. To properly detect End/Home we can check if the
1844 Shift modifier is active or not. If the user wants # instead of End, then
1845 Shift must be active. The same goes for $ and Home. Thus we now only have
1846 the '-' (Insert) and the '.' (Delete) characters incorrectly detected as
1847 being Insert/Delete.
1849 The above brings us to one of the main visible difference, when comparing
1850 the pwlib.dom.KeyboardEventListener class and the simple
1851 pwlib.dom.KeyboardEvent.getKey() function. In getKey(), for the keypress
1852 event we cannot accurately determine the exact key, because it requires
1853 checking the keyCode used for the keydown event. The KeyboardEventListener
1854 class monitors all the keyboard events, ensuring a more accurate key
1857 Different keyboard layouts and international characters are generally
1858 supported. Tested and known to work with the Cyrillic alphabet (Greek
1859 keyboard layout) and with the US Dvorak keyboard layouts.
1861 Opera does not fire the keyup event for international characters when
1862 running on Linux. For example, this happens with the Greek keyboard layout,
1863 when trying Cyrillic characters.
1865 Gecko gives no keyCode/charCode/which for international characters when
1866 running on Linux, in the keyup/keydown events. Thus, all such keys remain
1867 unidentified for these two events. For the keypress event there are no
1868 issues with such characters.
1870 Webkit and Konqueror 4 also implement the keyIdentifier property from the
1871 DOM 3 Events specification. In theory, this should be great, but it's not
1872 without problems. Sometimes keyCode/charCode/which are all 0, but
1873 keyIdentifier is properly set. For several virtual keys the keyIdentifier
1874 value is simply 'U+0000'. Thus, the keyIdentifier is used only if the value
1875 is not 'Unidentified' / 'U+0000', and only when keyCode/charCode/which are
1878 Konqueror 4 does not use the 'U+XXXX' notation for Unicode characters. It
1879 simply gives the character, directly.
1881 Additionally, Konqueror seems to have some problems with several keyCodes in
1882 keydown/keyup. For example, the key '[' gives keyCode 91 instead of 219.
1883 Thus, it effectively gives the Unicode for the character, not the key code.
1884 This issue is visible with other key as well.
1886 NumPad_Clear is unidentified on Linux in all browsers, but it works on
1889 In MSIE the keypress event is only fired for characters and for Escape,
1890 Space and Enter. Similarly, Webkit only fires the keypress event for
1891 characters. However, Webkit does not fire keypress for Escape.
1893 International characters and different keyboard layouts seem to work fine in
1896 As of MSIE 4.0, the keypress event fires for the following keys:
1897 * Letters: A - Z (uppercase and lowercase)
1899 * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
1900 * System: Escape (27), Space (32), Enter (13)
1902 Documentation about the keypress event:
1903 http://msdn.microsoft.com/en-us/library/ms536939(VS.85).aspx
1905 As of MSIE 4.0, the keydown event fires for the following keys:
1906 * Editing: Delete (46), Insert (45)
1907 * Function: F1 - F12
1908 * Letters: A - Z (uppercase and lowercase)
1909 * Navigation: Home, End, Left, Right, Up, Down
1911 * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
1912 * System: Escape (27), Space (32), Shift (16), Tab (9)
1914 As of MSIE 5, the event also fires for the following keys:
1915 * Editing: Backspace (8)
1916 * Navigation: PageUp (33), PageDown (34)
1917 * System: Shift+Tab (9)
1919 Documentation about the keydown event:
1920 http://msdn.microsoft.com/en-us/library/ms536938(VS.85).aspx
1922 As of MSIE 4.0, the keyup event fires for the following keys:
1923 * Editing: Delete, Insert
1924 * Function: F1 - F12
1925 * Letters: A - Z (uppercase and lowercase)
1926 * Navigation: Home (36), End (35), Left (37), Right (39), Up (38), Down (40)
1928 * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
1929 * System: Escape (27), Space (32), Shift (16), Tab (9)
1931 As of MSIE 5, the event also fires for the following keys:
1932 * Editing: Backspace (8)
1933 * Navigation: PageUp (33), PageDown (34)
1934 * System: Shift+Tab (9)
1936 Documentation about the keyup event:
1937 http://msdn.microsoft.com/en-us/library/ms536940(VS.85).aspx
1939 For further gory details and a different implementation see:
1940 http://code.google.com/p/doctype/source/browse/trunk/goog/events/keycodes.js
1941 http://code.google.com/p/doctype/source/browse/trunk/goog/events/keyhandler.js
1943 Opera keydown/keyup:
1944 These events fire for all keys, including for modifiers.
1945 keyCode is always set.
1946 charCode is never set.
1947 which is always set.
1948 keyIdentifier is always undefined.
1951 This event fires for all keys, except for modifiers themselves.
1952 keyCode is always set.
1953 charCode is never set.
1954 which is set for all characters. which = 0 for several virtual keys.
1955 which is known to be set for: Backspace (8), Tab (9), Enter (13), Shift
1956 (16), Control (17), Alt (18), Pause (19), Escape (27), End (35), Home
1957 (36), Insert (45), Delete (46), NumLock (144).
1958 which is known to be unset for: F1 - F12, PageUp (33), PageDown (34), Left
1959 (37), Up (38), Right (39), Down (40).
1960 keyIdentifier is always undefined.
1962 MSIE keyup/keypress/keydown:
1963 Event firing conditions are described above.
1964 keyCode is always set.
1965 charCode is never set.
1967 keyIdentifier is always undefined.
1969 Webkit keydown/keyup:
1970 These events fires for all keys, including for modifiers.
1971 keyCode is always set.
1972 charCode is never set.
1973 which is always set.
1974 keyIdentifier is always set.
1977 This event fires for characters keys, similarly to MSIE (see above info).
1978 keyCode is always set.
1979 charCode is always set for all characters.
1980 which is always set.
1981 keyIdentifier is null.
1983 Gecko keydown/keyup:
1984 These events fire for all keys, including for modifiers.
1985 keyCode is always set.
1986 charCode is never set.
1987 which is always set.
1988 keyIdentifier is always undefined.
1991 This event fires for all keys, except for modifiers themselves.
1992 keyCode is only set for virtual keys, not for characters.
1993 charCode is always set for all characters.
1994 which is always set for all characters and for the Enter virtual key.
1995 keyIdentifier is always undefined.
1997 Another important difference between the KeyboardEventListener class and the
1998 getKey() function is that the class tries to ensure that the keypress event
1999 is fired for the handler, even if the Web browser does not do it natively.
2000 Also, the class tries to provide a consistent approach to keyboard event
2001 repetition when the user holds down a key for longer periods of time, by
2002 repeating only the keypress event.
2004 On Linux, Opera, Firefox and Konqueror do not repeat the keydown event, only
2005 keypress. On Windows, Opera, Firefox and MSIE do repeat the keydown and
2006 keypress events while the user holds down the key. Webkit repeats the
2007 keydown and the keypress (when it fires) events on both systems.
2009 The default action can be prevented for during keydown in MSIE, and during
2010 keypress for the other browsers. In Webkit when keypress doesn't fire,
2011 keydown needs to be prevented.
2013 The KeyboardEventListener class tries to bring consistency. The keydown
2014 event never repeats, only the keypress event repeats and it always fires for
2015 all keys. The keypress event never fires for modifiers. Events should always
2016 be prevented during keypress - the class deals with preventing the event
2017 during keydown or keypress as needed in Webkit and MSIE.
2019 If no code/keyIdentifier is given by the browser, the getKey() function
2020 returns null. In the case of the KeyboardEventListener class, keyCode_
2021 / key_ / charCode_ / char_ will be null or undefined.
2025 * During a keyboard event flow, this holds the current key code, starting
2026 * from the <code>keydown</code> event.
2031 var keyCode_ = null;
2034 * During a keyboard event flow, this holds the current key, starting from the
2035 * <code>keydown</code> event.
2043 * During a keyboard event flow, this holds the current character code,
2044 * starting from the <code>keypress</code> event.
2049 var charCode_ = null;
2052 * During a keyboard event flow, this holds the current character, starting
2053 * from the <code>keypress</code> event.
2061 * True if the current keyboard event is repeating. This happens when the user
2062 * holds down a key for longer periods of time.
2067 var repeat_ = false;
2071 throw new TypeError('The first argument must be of type an object.');
2074 if (!handlers_.keydown && !handlers_.keypress && !handlers_.keyup) {
2075 throw new TypeError('The provided handlers object has no keyboard event' +
2079 if (handlers_.keydown && typeof handlers_.keydown !== 'function') {
2080 throw new TypeError('The keydown event handler is not a function!');
2082 if (handlers_.keypress && typeof handlers_.keypress !== 'function') {
2083 throw new TypeError('The keypress event handler is not a function!');
2085 if (handlers_.keyup && typeof handlers_.keyup !== 'function') {
2086 throw new TypeError('The keyup event handler is not a function!');
2090 * Attach the keyboard event listeners to the current DOM element.
2092 this.attach = function () {
2099 // FIXME: I have some ideas for a solution to the problem of having multiple
2100 // event handlers like these attached to the same element. Somehow, only one
2101 // should do all the needed work.
2103 elem_.addEventListener('keydown', keydown, false);
2104 elem_.addEventListener('keypress', keypress, false);
2105 elem_.addEventListener('keyup', keyup, false);
2109 * Detach the keyboard event listeners from the current DOM element.
2111 this.detach = function () {
2112 elem_.removeEventListener('keydown', keydown, false);
2113 elem_.removeEventListener('keypress', keypress, false);
2114 elem_.removeEventListener('keyup', keyup, false);
2124 * Dispatch an event.
2126 * <p>This function simply invokes the handler for the event of the given
2127 * <var>type</var>. The handler receives the <var>ev</var> event.
2130 * @param {String} type The event type to dispatch.
2131 * @param {Event} ev The DOM Event object to dispatch to the handler.
2133 function dispatch (type, ev) {
2134 if (!handlers_[type]) {
2138 var handler = handlers_[type];
2140 if (type === ev.type) {
2141 handler.call(elem_, ev);
2144 // This happens when the keydown event tries to dispatch a keypress event.
2146 // FIXME: I could use createEvent() ... food for thought for later
2148 pwlib.extend(ev_new, ev);
2151 // Make sure preventDefault() is not borked...
2152 ev_new.preventDefault = function () {
2153 ev.preventDefault();
2156 handler.call(elem_, ev_new);
2161 * The <code>keydown</code> event handler. This function determines the key
2162 * pressed by the user, and checks if the <code>keypress</code> event will
2163 * fire in the current Web browser, or not. If it does not, a synthetic
2164 * <code>keypress</code> event will be fired.
2167 * @param {Event} ev The DOM Event object.
2169 function keydown (ev) {
2177 ev.keyCode_ = keyCode_;
2179 ev.repeat_ = key_ && prevKey === key_ ? true : false;
2181 repeat_ = ev.repeat_;
2183 // When the user holds down a key for a longer period of time, the keypress
2184 // event is generally repeated. However, in Webkit keydown is repeated (and
2185 // keypress if it fires keypress for the key). As such, we do not dispatch
2186 // the keydown event when a key event starts to be repeated.
2188 dispatch('keydown', ev);
2191 // MSIE and Webkit only fire the keypress event for characters
2192 // (alpha-numeric and symbols).
2193 if (!isModifierKey(key_) && !firesKeyPress(ev)) {
2194 ev.type_ = 'keydown';
2200 * The <code>keypress</code> event handler. This function determines the
2201 * character generated by the keyboard event.
2204 * @param {Event} ev The DOM Event object.
2206 function keypress (ev) {
2207 // We reuse the keyCode_/key_ from the keydown event, because ev.keyCode
2208 // generally holds the character code during the keypress event.
2209 // However, if keyCode_ is not available, try to determine the key for this
2216 ev.keyCode_ = keyCode_;
2221 ev.charCode_ = charCode_;
2224 // Any subsequent keypress event is considered a repeated keypress (the user
2225 // is holding down the key).
2226 ev.repeat_ = repeat_;
2231 if (!isModifierKey(key_)) {
2232 dispatch('keypress', ev);
2237 * The <code>keyup</code> event handler.
2240 * @param {Event} ev The DOM Event object.
2242 function keyup (ev) {
2244 * Try to determine the keyCode_ for keyup again, even if we might already
2245 * have it from keydown. This is needed because the user might press some
2246 * key which only generates the keydown and keypress events, after which
2247 * a sudden keyup event is fired for a completely different key.
2249 * Example: in Opera press F2 then Escape. It will first generate two
2250 * events, keydown and keypress, for the F2 key. When you press Escape to
2251 * close the dialog box, the script receives keyup for Escape.
2255 ev.keyCode_ = keyCode_;
2258 // Provide the character info from the keypress event in keyup as well.
2259 ev.charCode_ = charCode_;
2262 dispatch('keyup', ev);
2272 * Tells if the <var>key</var> is a modifier or not.
2275 * @param {String} key The key name.
2276 * @returns {Boolean} True if the <var>key</var> is a modifier, or false if
2279 function isModifierKey (key) {
2293 * Tells if the current Web browser will fire the <code>keypress</code> event
2294 * for the current <code>keydown</code> event object.
2297 * @param {Event} ev The DOM Event object.
2298 * @returns {Boolean} True if the Web browser will fire
2299 * a <code>keypress</code> event, or false if not.
2301 function firesKeyPress (ev) {
2302 // Gecko does not fire keypress for the Up/Down arrows when the target is an
2304 if ((key_ === 'Up' || key_ === 'Down') && pwlib.browser.gecko && ev.target
2305 && ev.target.tagName.toLowerCase() === 'input') {
2309 if (!pwlib.browser.msie && !pwlib.browser.webkit) {
2313 // Check if the key is a character key, or not.
2314 // If it's not a character, then keypress will not fire.
2315 // Known exceptions: keypress fires for Space, Enter and Escape in MSIE.
2316 if (key_ && key_ !== 'Space' && key_ !== 'Enter' && key_ !== 'Escape' &&
2317 key_.length !== 1) {
2321 // Webkit doesn't fire keypress for Escape as well ...
2322 if (pwlib.browser.webkit && key_ === 'Escape') {
2326 // MSIE does not fire keypress if you hold Control / Alt down, while Shift
2327 // is off. Albeit, based on testing I am not completely sure if Shift needs
2328 // to be down or not. Sometimes MSIE won't fire keypress even if I hold
2329 // Shift down, and sometimes it does. Eh.
2330 if (pwlib.browser.msie && !ev.shiftKey && (ev.ctrlKey || ev.altKey)) {
2338 * Determine the key and the key code for the current DOM Event object. This
2339 * function updates the <var>keyCode_</var> and the <var>key_</var> variables
2340 * to hold the result.
2343 * @param {Event} ev The DOM Event object.
2345 function findKeyCode (ev) {
2347 * If the event has no keyCode/which/keyIdentifier values, then simply do
2348 * not overwrite any existing keyCode_/key_.
2350 if (ev.type === 'keyup' && !ev.keyCode && !ev.which && (!ev.keyIdentifier ||
2351 ev.keyIdentifier === 'Unidentified' || ev.keyIdentifier === 'U+0000')) {
2358 // Try to use keyCode/which.
2359 if (ev.keyCode || ev.which) {
2360 keyCode_ = ev.keyCode || ev.which;
2362 // Fix Webkit quirks
2363 if (pwlib.browser.webkit) {
2364 // Old Webkit gives keyCode 25 when Shift+Tab is used.
2365 if (keyCode_ == 25 && this.shiftKey) {
2366 keyCode_ = pwlib.dom.keyNames.Tab;
2367 } else if (keyCode_ >= 63232 && keyCode_ in pwlib.dom.keyCodes_Safari2) {
2368 // Old Webkit gives wrong values for several keys.
2369 keyCode_ = pwlib.dom.keyCodes_Safari2[keyCode_];
2373 // Fix keyCode quirks in all browsers.
2374 if (keyCode_ in pwlib.dom.keyCodes_fixes) {
2375 keyCode_ = pwlib.dom.keyCodes_fixes[keyCode_];
2378 key_ = pwlib.dom.keyCodes[keyCode_] || String.fromCharCode(keyCode_);
2383 // Try to use ev.keyIdentifier. This is only available in Webkit and
2384 // Konqueror 4, each having some quirks. Sometimes the property is needed,
2385 // because keyCode/which are not always available.
2389 id = ev.keyIdentifier;
2391 if (!id || id === 'Unidentified' || id === 'U+0000') {
2395 if (id.substr(0, 2) === 'U+') {
2396 // Webkit gives character codes using the 'U+XXXX' notation, as per spec.
2397 keyCode = parseInt(id.substr(2), 16);
2399 } else if (id.length === 1) {
2400 // Konqueror 4 implements keyIdentifier, and they provide the Unicode
2401 // character directly, instead of using the 'U+XXXX' notation.
2402 keyCode = id.charCodeAt(0);
2407 * Common keyIdentifiers like 'PageDown' are used as they are.
2408 * We determine the common keyCode used by Web browsers, from the
2409 * pwlib.dom.keyNames object.
2411 keyCode_ = pwlib.dom.keyNames[id] || null;
2417 // Some keyIdentifiers like 'U+007F' (127: Delete) need to become key names.
2418 if (keyCode in pwlib.dom.keyCodes && (keyCode <= 32 || keyCode == 127 ||
2420 key_ = pwlib.dom.keyCodes[keyCode];
2423 key = String.fromCharCode(keyCode);
2426 // Konqueror gives lower-case chars
2427 key_ = key.toUpperCase();
2429 keyCode = key_.charCodeAt(0);
2433 // Correct the keyCode, make sure it's a common keyCode, not the Unicode
2434 // decimal representation of the character.
2435 if (key_ === 'Delete' || key_.length === 1 && key_ in pwlib.dom.keyNames) {
2436 keyCode = pwlib.dom.keyNames[key_];
2443 * Determine the character and the character code for the current DOM Event
2444 * object. This function updates the <var>charCode_</var> and the
2445 * <var>char_</var> variables to hold the result.
2448 * @param {Event} ev The DOM Event object.
2450 function findCharCode (ev) {
2454 // Webkit and Gecko implement ev.charCode.
2456 charCode_ = ev.charCode;
2457 char_ = String.fromCharCode(ev.charCode);
2462 // Try the keyCode mess.
2463 if (ev.keyCode || ev.which) {
2464 var keyCode = ev.keyCode || ev.which;
2468 // We accept some keyCodes.
2470 case pwlib.dom.keyNames.Tab:
2471 case pwlib.dom.keyNames.Enter:
2472 case pwlib.dom.keyNames.Space:
2476 // Do not consider the keyCode a character code, if during the keydown
2477 // event it was determined the key does not generate a character, unless
2478 // it's Tab, Enter or Space.
2479 if (!force && key_ && key_.length !== 1) {
2483 // If the keypress event at hand is synthetically dispatched by keydown,
2484 // then special treatment is needed. This happens only in Webkit and MSIE.
2485 if (ev.type_ === 'keydown') {
2486 var key = pwlib.dom.keyCodes[keyCode];
2487 // Check if the keyCode points to a single character.
2488 // If it does, use it.
2489 if (key && key.length === 1) {
2490 charCode_ = key.charCodeAt(0); // keyCodes != charCodes
2493 } else if (keyCode >= 32 || force) {
2494 // For normal keypress events, we are done.
2495 charCode_ = keyCode;
2496 char_ = String.fromCharCode(keyCode);
2505 * Webkit and Konqueror do not provide a keyIdentifier in the keypress
2506 * event, as per spec. However, in the unlikely case when the keyCode is
2507 * missing, and the keyIdentifier is available, we use it.
2509 * This property might be used when a synthetic keypress event is generated
2510 * by the keydown event, and keyCode/charCode/which are all not available.
2515 id = ev.keyIdentifier;
2517 if (id && id !== 'Unidentified' && id !== 'U+0000' &&
2518 (id.substr(0, 2) === 'U+' || id.length === 1)) {
2520 // Characters in Konqueror...
2521 if (id.length === 1) {
2522 charCode = id.charCodeAt(0);
2526 // Webkit uses the 'U+XXXX' notation as per spec.
2527 charCode = parseInt(id.substr(2), 16);
2530 if (charCode == pwlib.dom.keyNames.Tab ||
2531 charCode == pwlib.dom.keyNames.Enter ||
2532 charCode >= 32 && charCode != 127 &&
2533 charCode != pwlib.dom.keyNames.NumLock) {
2535 charCode_ = charCode;
2536 char_ = c || String.fromCharCode(charCode);
2542 // Try to use the key determined from the previous keydown event, if it
2543 // holds a character.
2544 if (key_ && key_.length === 1) {
2545 charCode_ = key_.charCodeAt(0);
2553 // Check out the libmacrame project: http://code.google.com/p/libmacrame.
2555 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
2558 * Copyright (C) 2008, 2009 Mihai Şucan
2560 * This file is part of PaintWeb.
2562 * PaintWeb is free software: you can redistribute it and/or modify
2563 * it under the terms of the GNU General Public License as published by
2564 * the Free Software Foundation, either version 3 of the License, or
2565 * (at your option) any later version.
2567 * PaintWeb is distributed in the hope that it will be useful,
2568 * but WITHOUT ANY WARRANTY; without even the implied warranty of
2569 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2570 * GNU General Public License for more details.
2572 * You should have received a copy of the GNU General Public License
2573 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
2575 * $URL: http://code.google.com/p/paintweb $
2576 * $Date: 2009-07-02 16:07:14 +0300 $
2580 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
2581 * @fileOverview Holds the selection tool implementation.
2585 * @class The selection tool.
2587 * @param {PaintWeb} app Reference to the main paint application object.
2589 pwlib.tools.selection = function (app) {
2591 appEvent = pwlib.appEvent,
2592 bufferContext = app.buffer.context,
2593 clearInterval = app.win.clearInterval,
2594 config = app.config.selection,
2598 layerCanvas = app.layer.canvas,
2599 layerContext = app.layer.context,
2600 marqueeStyle = null,
2603 MathRound = Math.round,
2605 setInterval = app.win.setInterval,
2606 snapXY = app.toolSnapXY;
2609 * The interval ID used for invoking the drawing operation every few
2613 * @see PaintWeb.config.toolDrawDelay
2618 * Tells if the drawing canvas needs to be updated or not.
2624 var needsRedraw = false;
2627 * The selection has been dropped, and the mouse button is down. The user has
2628 * two choices: he releases the mouse button, thus the selection is dropped
2629 * and the tool switches to STATE_NONE, or he moves the mouse in order to
2630 * start a new selection (STATE_DRAWING).
2633 this.STATE_PENDING = -1;
2636 * No selection is available.
2639 this.STATE_NONE = 0;
2642 * The user is drawing a selection.
2645 this.STATE_DRAWING = 1;
2648 * The selection rectangle is available.
2651 this.STATE_SELECTED = 2;
2654 * The user is dragging/moving the selection rectangle.
2657 this.STATE_DRAGGING = 3;
2660 * The user is resizing the selection rectangle.
2663 this.STATE_RESIZING = 4;
2666 * Selection state. Known states:
2669 * <li>{@link pwlib.tools.selection#STATE_PENDING} - Selection dropped after
2670 * the <code>mousedown</code> event is fired. The script can switch to
2671 * STATE_DRAWING if the mouse moves, or to STATE_NONE if it does not
2672 * (allowing the user to drop the selection).
2674 * <li>{@link pwlib.tools.selection#STATE_NONE} - No selection is available.
2676 * <li>{@link pwlib.tools.selection#STATE_DRAWING} - The user is drawing the
2677 * selection rectangle.
2679 * <li>{@link pwlib.tools.selection#STATE_SELECTED} - The selection
2680 * rectangle is available.
2682 * <li>{@link pwlib.tools.selection#STATE_DRAGGING} - The user is
2683 * dragging/moving the current selection.
2685 * <li>{@link pwlib.tools.selection#STATE_RESIZING} - The user is resizing
2686 * the current selection.
2690 * @default STATE_NONE
2692 this.state = this.STATE_NONE;
2695 * Holds the starting point on the <var>x</var> axis of the image, for any
2696 * ongoing operation.
2704 * Holds the starting point on the <var>y</var> axis of the image, for the any
2705 * ongoing operation.
2713 * Holds selection information and image.
2718 * Selection start point, on the <var>x</var> axis.
2724 * Selection start point, on the <var>y</var> axis.
2742 * Selection original width. The user can make a selection rectangle of
2743 * a given width and height, but after that he/she can resize the selection.
2749 * Selection original height. The user can make a selection rectangle of
2750 * a given width and height, but after that he/she can resize the selection.
2756 * Tells if the selected ImageData has been cut out or not from the
2762 layerCleared: false,
2765 * Selection marquee/border element.
2771 * Selection buffer context which holds the selected pixels.
2772 * @type CanvasRenderingContext2D
2777 * Selection buffer canvas which holds the selected pixels.
2778 * @type HTMLCanvasElement
2784 * The area type under the current mouse location.
2786 * <p>When the selection is available the mouse location can be on top/inside
2787 * the selection rectangle, on the border of the selection, or outside the
2790 * <p>Possible values: 'in', 'out', 'border'.
2796 var mouseArea = 'out';
2799 * The resize type. If the mouse is on top of the selection border, then the
2800 * selection can be resized. The direction of the resize operation is
2801 * determined by the location of the mouse.
2803 * <p>While the user resizes the selection this variable can hold the
2804 * following values: 'n' (North), 'ne' (North-East), 'e' (East), 'se'
2805 * (South-East), 's' (South), 'sw' (South-West), 'w' (West), 'nw'
2812 var mouseResize = null;
2814 // shorthands / private variables
2815 var sel = this.selection,
2816 borderDouble = config.borderWidth * 2,
2817 ev_canvasSizeChangeId = null,
2818 ev_configChangeId = null,
2823 * The last selection rectangle that was drawn. This is used by the selection
2824 * drawing functions.
2829 // We avoid retrieving the mouse coordinates during the mouseup event, due to
2830 // the Opera bug DSK-232264.
2834 * The tool preactivation code. This function prepares the selection canvas
2837 * @returns {Boolean} True if the activation did not fail, or false otherwise.
2838 * If false is returned, the selection tool cannot be activated.
2840 this.preActivate = function () {
2841 if (!('canvasContainer' in gui.elems)) {
2842 alert(lang.errorToolActivate);
2846 // The selection image buffer.
2847 sel.canvas = app.doc.createElement('canvas');
2849 alert(lang.errorToolActivate);
2853 sel.canvas.width = 5;
2854 sel.canvas.height = 5;
2856 sel.context = sel.canvas.getContext('2d');
2858 alert(lang.errorToolActivate);
2862 sel.marquee = app.doc.createElement('div');
2864 alert(lang.errorToolActivate);
2867 sel.marquee.className = gui.classPrefix + 'selectionMarquee';
2868 marqueeStyle = sel.marquee.style;
2874 * The tool activation code. This method sets-up multiple event listeners for
2875 * several target objects.
2877 this.activate = function () {
2878 // Older browsers do not support get/putImageData, thus non-transparent
2879 // selections cannot be used.
2880 if (!layerContext.putImageData || !layerContext.getImageData) {
2881 config.transparent = true;
2886 marqueeStyle.borderWidth = config.borderWidth + 'px';
2887 sel.marquee.addEventListener('mousedown', marqueeMousedown, false);
2888 sel.marquee.addEventListener('mousemove', marqueeMousemove, false);
2889 sel.marquee.addEventListener('mouseup', marqueeMouseup, false);
2891 gui.elems.canvasContainer.appendChild(sel.marquee);
2893 // Disable the Canvas shadow.
2894 app.shadowDisallow();
2896 // Application event listeners.
2897 ev_canvasSizeChangeId = app.events.add('canvasSizeChange',
2898 ev_canvasSizeChange);
2899 ev_configChangeId = app.events.add('configChange', ev_configChange);
2901 // Register selection-related commands
2902 app.commandRegister('selectionCrop', _self.selectionCrop);
2903 app.commandRegister('selectionDelete', _self.selectionDelete);
2904 app.commandRegister('selectionFill', _self.selectionFill);
2907 timer = setInterval(timerFn, app.config.toolDrawDelay);
2914 * The tool deactivation code. This removes all event listeners and cleans up
2917 this.deactivate = function () {
2919 clearInterval(timer);
2923 _self.selectionMerge();
2925 sel.marquee.removeEventListener('mousedown', marqueeMousedown, false);
2926 sel.marquee.removeEventListener('mousemove', marqueeMousemove, false);
2927 sel.marquee.removeEventListener('mouseup', marqueeMouseup, false);
2929 marqueeStyle = null;
2930 gui.elems.canvasContainer.removeChild(sel.marquee);
2932 delete sel.context, sel.canvas, sel.marquee;
2934 // Re-enable canvas shadow.
2937 // Remove the application event listeners.
2938 if (ev_canvasSizeChangeId) {
2939 app.events.remove('canvasSizeChange', ev_canvasSizeChangeId);
2941 if (ev_configChangeId) {
2942 app.events.remove('configChange', ev_configChangeId);
2945 // Unregister selection-related commands
2946 app.commandUnregister('selectionCrop');
2947 app.commandUnregister('selectionDelete');
2948 app.commandUnregister('selectionFill');
2954 * The <code>mousedown</code> event handler. Depending on the mouse location,
2955 * this method does initiate different selection operations: drawing,
2956 * dropping, dragging or resizing.
2958 * <p>Hold the <kbd>Control</kbd> key down to temporarily toggle the
2959 * transformation mode.
2961 * @param {Event} ev The DOM Event object.
2963 this.mousedown = function (ev) {
2964 if (_self.state !== _self.STATE_NONE &&
2965 _self.state !== _self.STATE_SELECTED) {
2969 // Update the current mouse position, this is used as the start position for most of the operations.
2973 shiftKey = ev.shiftKey;
2974 ctrlKey = ev.ctrlKey;
2977 // No selection is available, then start drawing a selection.
2978 if (_self.state === _self.STATE_NONE) {
2979 _self.state = _self.STATE_DRAWING;
2980 marqueeStyle.display = '';
2981 gui.statusShow('selectionDraw');
2986 // STATE_SELECTED: selection available.
2990 * Check if the user clicked outside the selection: drop the selection,
2991 * switch to STATE_PENDING, clear the image buffer and put the current
2992 * selection buffer in the image layer.
2994 * If the user moves the mouse without taking the finger off the mouse
2995 * button, then a new selection rectangle will start to be drawn: the script
2996 * will switch to STATE_DRAWING.
2998 * If the user simply takes the finger off the mouse button (mouseup), then
2999 * the script will switch to STATE_NONE (no selection available).
3001 switch (mouseArea) {
3003 _self.state = _self.STATE_PENDING;
3005 gui.statusShow('selectionActive');
3006 selectionMergeStrict();
3011 // The mouse area: 'in' for drag.
3012 _self.state = _self.STATE_DRAGGING;
3013 gui.statusShow('selectionDrag');
3017 // 'border' for resize (the user is clicking on the borders).
3018 _self.state = _self.STATE_RESIZING;
3019 gui.statusShow('selectionResize');
3022 // Temporarily toggle the transformation mode if the user holds the Control
3025 config.transform = !config.transform;
3028 // If there's any ImageData currently in memory, which was "cut" out from
3029 // the current layer, then put it back on the layer. This needs to be done
3030 // only when the selection.transform mode is not active - that's when the
3031 // drag/resize operation only changes the selection, not the pixels
3033 if (sel.layerCleared && !config.transform) {
3034 selectionMergeStrict();
3036 } else if (!sel.layerCleared && config.transform) {
3037 // When the user starts dragging/resizing the ImageData we must cut out
3038 // the current selection from the image layer.
3039 selectionBufferInit();
3046 * The <code>mousemove</code> event handler.
3048 * @param {Event} ev The DOM Event object.
3050 this.mousemove = function (ev) {
3051 shiftKey = ev.shiftKey;
3056 * The timer function. When the mouse button is down, this method performs the
3057 * dragging/resizing operation. When the mouse button is not down, this method
3058 * simply tracks the mouse location for the purpose of determining the area
3059 * being pointed at: the selection, the borders, or if the mouse is outside
3063 function timerFn () {
3068 switch (_self.state) {
3069 case _self.STATE_PENDING:
3070 // selection dropped, switch to draw selection
3071 _self.state = _self.STATE_DRAWING;
3072 marqueeStyle.display = '';
3073 gui.statusShow('selectionDraw');
3075 case _self.STATE_DRAWING:
3079 case _self.STATE_SELECTED:
3083 case _self.STATE_DRAGGING:
3087 case _self.STATE_RESIZING:
3091 needsRedraw = false;
3095 * The <code>mouseup</code> event handler. This method ends any selection
3098 * <p>This method dispatches the {@link pwlib.appEvent.selectionChange}
3099 * application event when the selection state is changed or when the selection
3100 * size/location is updated.
3102 * @param {Event} ev The DOM Event object.
3104 this.mouseup = function (ev) {
3105 // Allow click+mousemove+click, not only mousedown+move+up
3106 if (_self.state !== _self.STATE_PENDING &&
3107 mouse.x === x0 && mouse.y === y0) {
3111 needsRedraw = false;
3113 shiftKey = ev.shiftKey;
3115 config.transform = !config.transform;
3118 if (_self.state === _self.STATE_PENDING) {
3119 // Selection dropped? If yes, switch to the no selection state.
3120 _self.state = _self.STATE_NONE;
3121 app.events.dispatch(new appEvent.selectionChange(_self.state));
3125 } else if (!lastSel) {
3126 _self.state = _self.STATE_NONE;
3128 gui.statusShow('selectionActive');
3129 app.events.dispatch(new appEvent.selectionChange(_self.state));
3137 if ('width' in lastSel) {
3138 sel.width = lastSel.width;
3139 sel.height = lastSel.height;
3142 _self.state = _self.STATE_SELECTED;
3144 app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y,
3145 sel.width, sel.height));
3147 gui.statusShow('selectionAvailable');
3153 * The <code>mousedown</code> event handler for the selection marquee element.
3156 * @param {Event} ev The DOM Event object.
3158 function marqueeMousedown (ev) {
3159 if (mouse.buttonDown) {
3162 mouse.buttonDown = true;
3164 ev.preventDefault();
3166 _self.mousedown(ev);
3170 * The <code>mousemove</code> event handler for the selection marquee element.
3173 * @param {Event} ev The DOM Event object.
3175 function marqueeMousemove (ev) {
3176 if ('layerX' in ev) {
3177 mouse.x = MathRound((this.offsetLeft + ev.layerX) / image.canvasScale);
3178 mouse.y = MathRound((this.offsetTop + ev.layerY) / image.canvasScale);
3179 } else if ('offsetX' in ev) {
3180 mouse.x = MathRound((this.offsetLeft + ev.offsetX) / image.canvasScale);
3181 mouse.y = MathRound((this.offsetTop + ev.offsetY) / image.canvasScale);
3184 shiftKey = ev.shiftKey;
3189 * The <code>mouseup</code> event handler for the selection marquee element.
3192 * @param {Event} ev The DOM Event object.
3194 function marqueeMouseup (ev) {
3195 if (!mouse.buttonDown) {
3198 mouse.buttonDown = false;
3200 ev.preventDefault();
3206 * Hide the selection marquee element.
3209 function marqueeHide () {
3210 marqueeStyle.display = 'none';
3211 marqueeStyle.top = '-' + (borderDouble + 50) + 'px';
3212 marqueeStyle.left = '-' + (borderDouble + 50) + 'px';
3213 marqueeStyle.width = '1px';
3214 marqueeStyle.height = '1px';
3215 marqueeStyle.cursor = '';
3219 * Perform the selection rectangle drawing operation.
3223 function selectionDraw () {
3224 var x = MathMin(mouse.x, x0),
3225 y = MathMin(mouse.y, y0),
3226 w = MathAbs(mouse.x - x0),
3227 h = MathAbs(mouse.y - y0);
3229 // Constrain the shape to a square.
3232 if (y === mouse.y) {
3237 if (x === mouse.x) {
3244 var mw = w * image.canvasScale - borderDouble,
3245 mh = h * image.canvasScale - borderDouble;
3247 if (mw < 1 || mh < 1) {
3252 marqueeStyle.top = (y * image.canvasScale) + 'px';
3253 marqueeStyle.left = (x * image.canvasScale) + 'px';
3254 marqueeStyle.width = mw + 'px';
3255 marqueeStyle.height = mh + 'px';
3257 lastSel = {'x': x, 'y': y, 'width': w, 'height': h};
3261 * Perform the selection drag operation.
3265 * @returns {false|Array} False is returned if the selection is too small,
3266 * otherwise an array of two elements is returned. The array holds the
3267 * selection coordinates, x and y.
3269 function selectionDrag () {
3270 // Snapping on the X/Y axis
3275 var x = sel.x + mouse.x - x0,
3276 y = sel.y + mouse.y - y0;
3278 // Dragging the ImageData
3279 if (config.transform) {
3280 bufferContext.clearRect(0, 0, image.width, image.height);
3282 if (!config.transparent) {
3283 bufferContext.fillRect(x, y, sel.width, sel.height);
3287 // source image, dest x, dest y, dest width, dest height
3288 bufferContext.drawImage(sel.canvas, x, y, sel.width, sel.height);
3291 marqueeStyle.top = (y * image.canvasScale) + 'px';
3292 marqueeStyle.left = (x * image.canvasScale) + 'px';
3294 lastSel = {'x': x, 'y': y};
3298 * Perform the selection resize operation.
3302 * @returns {false|Array} False is returned if the selection is too small,
3303 * otherwise an array of four elements is returned. The array holds the
3304 * selection information: x, y, width and height.
3306 function selectionResize () {
3307 var diffx = mouse.x - x0,
3308 diffy = mouse.y - y0,
3314 switch (mouseResize) {
3359 // Constrain the rectangle to have the same aspect ratio as the initial
3362 var p = sel.width / sel.height,
3366 switch (mouseResize.charAt(0)) {
3369 w2 = MathRound(h*p);
3372 h2 = MathRound(w/p);
3375 switch (mouseResize) {
3395 var mw = w * image.canvasScale - borderDouble,
3396 mh = h * image.canvasScale - borderDouble;
3398 if (mw < 1 || mh < 1) {
3403 // Resizing the ImageData
3404 if (config.transform) {
3405 bufferContext.clearRect(0, 0, image.width, image.height);
3407 if (!config.transparent) {
3408 bufferContext.fillRect(x, y, w, h);
3412 // source image, dest x, dest y, dest width, dest height
3413 bufferContext.drawImage(sel.canvas, x, y, w, h);
3416 marqueeStyle.top = (y * image.canvasScale) + 'px';
3417 marqueeStyle.left = (x * image.canvasScale) + 'px';
3418 marqueeStyle.width = mw + 'px';
3419 marqueeStyle.height = mh + 'px';
3421 lastSel = {'x': x, 'y': y, 'width': w, 'height': h};
3425 * Determine the are where the mouse is located: if it is inside or outside of
3426 * the selection rectangle, or on the selection border.
3429 function mouseAreaUpdate () {
3430 var border = config.borderWidth / image.canvasScale,
3432 x1_out = sel.x + sel.width,
3433 y1_out = sel.y + sel.height,
3434 x1_in = x1_out - border,
3435 y1_in = y1_out - border,
3438 x0_in = sel.x + border,
3439 y0_in = sel.y + border;
3443 // Inside the rectangle
3444 if (mouse.x < x1_in && mouse.y < y1_in &&
3445 mouse.x > x0_in && mouse.y > y0_in) {
3450 // On one of the borders (north/south)
3451 if (mouse.x >= x0_out && mouse.x <= x1_out &&
3452 mouse.y >= y0_out && mouse.y <= y0_in) {
3455 } else if (mouse.x >= x0_out && mouse.x <= x1_out &&
3456 mouse.y >= y1_in && mouse.y <= y1_out) {
3461 if (mouse.y >= y0_out && mouse.y <= y1_out &&
3462 mouse.x >= x0_out && mouse.x <= x0_in) {
3465 } else if (mouse.y >= y0_out && mouse.y <= y1_out &&
3466 mouse.x >= x1_in && mouse.x <= x1_out) {
3470 if (cursor !== '') {
3471 mouseResize = cursor;
3472 cursor += '-resize';
3473 mouseArea = 'border';
3477 // Due to bug 126457 Opera will not automatically update the cursor,
3478 // therefore they will not see any visual feedback.
3479 if (cursor !== marqueeStyle.cursor) {
3480 marqueeStyle.cursor = cursor;
3485 * The <code>canvasSizeChange</code> application event handler. This method
3486 * makes sure the selection size stays in sync.
3489 * @param {pwlib.appEvent.canvasSizeChange} ev The application event object.
3491 function ev_canvasSizeChange (ev) {
3492 if (_self.state !== _self.STATE_SELECTED) {
3496 marqueeStyle.top = (sel.y * ev.scale) + 'px';
3497 marqueeStyle.left = (sel.x * ev.scale) + 'px';
3498 marqueeStyle.width = (sel.width * ev.scale - borderDouble) + 'px';
3499 marqueeStyle.height = (sel.height * ev.scale - borderDouble) + 'px';
3503 * The <code>configChange</code> application event handler. This method makes
3504 * sure that changes to the selection transparency configuration option are
3508 * @param {pwlib.appEvent.configChange} ev The application event object.
3510 function ev_configChange (ev) {
3511 // Continue only if the selection rectangle is available.
3512 if (ev.group !== 'selection' || ev.config !== 'transparent' ||
3513 !config.transform || _self.state !== _self.STATE_SELECTED) {
3517 if (!sel.layerCleared) {
3518 selectionBufferInit();
3521 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3524 bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height);
3527 // Draw the updated selection
3528 bufferContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height);
3532 * Initialize the selection buffer, when the user starts dragging or resizing
3533 * the selected pixels.
3537 function selectionBufferInit () {
3542 sumX = sel.x + sel.width,
3543 sumY = sel.y + sel.height,
3546 sel.widthOriginal = w;
3547 sel.heightOriginal = h;
3560 if (sumX > image.width) {
3561 w = image.width - sel.x;
3563 if (sumY > image.height) {
3564 h = image.height - sel.y;
3567 if (!config.transparent) {
3568 bufferContext.fillRect(x, y, w, h);
3572 // source image, src x, src y, src w, src h, dest x, dest y, dest w, dest h
3573 bufferContext.drawImage(layerCanvas, x, y, w, h, x, y, w, h);
3575 sel.canvas.width = sel.widthOriginal;
3576 sel.canvas.height = sel.heightOriginal;
3578 // Also put the selected pixels into the selection buffer.
3579 sel.context.drawImage(layerCanvas, x, y, w, h, dx, dy, w, h);
3581 // Clear the selected pixels from the image
3582 layerContext.clearRect(x, y, w, h);
3583 sel.layerCleared = true;
3589 * Perform the selection buffer merge onto the current image layer.
3592 function selectionMergeStrict () {
3593 if (!sel.layerCleared) {
3597 if (!config.transparent) {
3598 layerContext.fillRect(sel.x, sel.y, sel.width, sel.height);
3601 layerContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height);
3602 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3604 sel.layerCleared = false;
3605 sel.canvas.width = 5;
3606 sel.canvas.height = 5;
3612 * Merge the selection buffer onto the current image layer.
3614 * <p>This method dispatches the {@link pwlib.appEvent.selectionChange}
3615 * application event.
3617 * @returns {Boolean} True if the operation was successful, or false if not.
3619 this.selectionMerge = function () {
3620 if (_self.state !== _self.STATE_SELECTED) {
3624 selectionMergeStrict();
3626 _self.state = _self.STATE_NONE;
3628 gui.statusShow('selectionActive');
3630 app.events.dispatch(new appEvent.selectionChange(_self.state));
3636 * Select all the entire image.
3638 * <p>This method dispatches the {@link pwlib.appEvent.selectionChange}
3639 * application event.
3641 * @returns {Boolean} True if the operation was successful, or false if not.
3643 this.selectAll = function () {
3644 if (_self.state !== _self.STATE_NONE && _self.state !==
3645 _self.STATE_SELECTED) {
3649 if (_self.state === _self.STATE_SELECTED) {
3650 selectionMergeStrict();
3652 _self.state = _self.STATE_SELECTED;
3653 marqueeStyle.display = '';
3658 sel.width = image.width;
3659 sel.height = image.height;
3661 marqueeStyle.top = '0px';
3662 marqueeStyle.left = '0px';
3663 marqueeStyle.width = (sel.width*image.canvasScale - borderDouble) + 'px';
3664 marqueeStyle.height = (sel.height*image.canvasScale - borderDouble) + 'px';
3668 app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y,
3669 sel.width, sel.height));
3675 * Cut the selected pixels. The associated ImageData is stored in {@link
3676 * PaintWeb#clipboard}.
3678 * <p>This method dispatches two application events: {@link
3679 * pwlib.appEvent.clipboardUpdate} and {@link pwlib.appEvent.selectionChange}.
3681 * @returns {Boolean} True if the operation was successful, or false if not.
3683 this.selectionCut = function () {
3684 if (!_self.selectionCopy()) {
3688 if (sel.layerCleared) {
3689 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3691 sel.canvas.width = 5;
3692 sel.canvas.height = 5;
3693 sel.layerCleared = false;
3696 layerContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3700 _self.state = _self.STATE_NONE;
3703 app.events.dispatch(new appEvent.selectionChange(_self.state));
3704 gui.statusShow('selectionActive');
3710 * Copy the selected pixels. The associated ImageData is stored in {@link
3711 * PaintWeb#clipboard}.
3713 * <p>This method dispatches the {@link pwlib.appEvent.clipboardUpdate}
3714 * application event.
3716 * @returns {Boolean} True if the operation was successful, or false if not.
3718 this.selectionCopy = function () {
3719 if (_self.state !== _self.STATE_SELECTED) {
3723 if (!layerContext.getImageData || !layerContext.putImageData) {
3724 alert(lang.errorClipboardUnsupported);
3728 if (!sel.layerCleared) {
3731 sumX = sel.width + sel.x;
3732 sumY = sel.height + sel.y;
3734 if (sumX > image.width) {
3735 w = image.width - sel.x;
3737 if (sumY > image.height) {
3738 h = image.height - sel.y;
3742 app.clipboard = layerContext.getImageData(sel.x, sel.y, w, h);
3744 alert(lang.failedSelectionCopy);
3750 app.clipboard = sel.context.getImageData(0, 0, sel.widthOriginal,
3751 sel.heightOriginal);
3753 alert(lang.failedSelectionCopy);
3758 app.events.dispatch(new appEvent.clipboardUpdate(app.clipboard));
3764 * Paste an image from the "clipboard". The {@link PaintWeb#clipboard} object
3765 * must be an ImageData. This method will generate a new selection which will
3766 * hold the pasted image.
3768 * <p>The {@link pwlib.appEvent.selectionChange} application event is
3771 * <p>If the {@link PaintWeb.config.selection.transform} value is false, then
3772 * it becomes true. The {@link pwlib.appEvent.configChange} application is
3775 * @returns {Boolean} True if the operation was successful, or false if not.
3777 this.clipboardPaste = function () {
3778 if (!app.clipboard || _self.state !== _self.STATE_NONE && _self.state !==
3779 _self.STATE_SELECTED) {
3783 if (!layerContext.getImageData || !layerContext.putImageData) {
3784 alert(lang.errorClipboardUnsupported);
3788 // The default position for the pasted image is the top left corner of the
3789 // visible area, taking into consideration the zoom level.
3790 var x = MathRound(gui.elems.viewport.scrollLeft / image.canvasScale),
3791 y = MathRound(gui.elems.viewport.scrollTop / image.canvasScale),
3792 w = app.clipboard.width,
3793 h = app.clipboard.height;
3795 sel.canvas.width = w;
3796 sel.canvas.height = h;
3797 sel.context.putImageData(app.clipboard, 0, 0);
3799 if (_self.state === _self.STATE_SELECTED) {
3800 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3802 _self.state = _self.STATE_SELECTED;
3805 if (!config.transparent) {
3806 bufferContext.fillRect(x, y, w, h);
3808 bufferContext.drawImage(sel.canvas, x, y, w, h);
3810 sel.widthOriginal = sel.width = w;
3811 sel.heightOriginal = sel.height = h;
3814 sel.layerCleared = true;
3816 marqueeStyle.top = (y * image.canvasScale) + 'px';
3817 marqueeStyle.left = (x * image.canvasScale) + 'px';
3818 marqueeStyle.width = (w * image.canvasScale - borderDouble) + 'px';
3819 marqueeStyle.height = (h * image.canvasScale - borderDouble) + 'px';
3820 marqueeStyle.display = '';
3822 if (!config.transform) {
3823 config.transform = true;
3824 app.events.dispatch(new appEvent.configChange(true, false, 'transform',
3825 'selection', config));
3830 app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y,
3831 sel.width, sel.height));
3833 gui.statusShow('selectionAvailable');
3839 * Perform selection delete.
3841 * <p>This method changes the {@link PaintWeb.config.selection.transform}
3842 * value to false if the current selection has pixels that are currently being
3843 * manipulated. In such cases, the {@link pwlib.appEvent.configChange}
3844 * application event is also dispatched.
3846 * @returns {Boolean} True if the operation was successful, or false if not.
3848 this.selectionDelete = function () {
3849 // Delete the pixels from the image if they are not deleted already.
3850 if (_self.state !== _self.STATE_SELECTED) {
3854 if (!sel.layerCleared) {
3855 layerContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3859 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3860 sel.layerCleared = false;
3861 sel.canvas.width = 5;
3862 sel.canvas.height = 5;
3864 if (config.transform) {
3865 config.transform = false;
3866 app.events.dispatch(new appEvent.configChange(false, true, 'transform',
3867 'selection', config));
3875 * Drop the current selection.
3877 * <p>This method dispatches the {@link pwlib.appEvent.selectionChange}
3878 * application event.
3880 * @returns {Boolean} True if the operation was successful, or false if not.
3882 this.selectionDrop = function () {
3883 if (_self.state !== _self.STATE_SELECTED) {
3887 if (sel.layerCleared) {
3888 bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3889 sel.canvas.width = 5;
3890 sel.canvas.height = 5;
3891 sel.layerCleared = false;
3894 _self.state = _self.STATE_NONE;
3897 gui.statusShow('selectionActive');
3899 app.events.dispatch(new appEvent.selectionChange(_self.state));
3905 * Fill the available selection with the current
3906 * <var>bufferContext.fillStyle</var>.
3908 * @returns {Boolean} True if the operation was successful, or false if not.
3910 this.selectionFill = function () {
3911 if (_self.state !== _self.STATE_SELECTED) {
3915 if (sel.layerCleared) {
3916 sel.context.fillStyle = bufferContext.fillStyle;
3917 sel.context.fillRect(0, 0, sel.widthOriginal, sel.heightOriginal);
3918 bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height);
3921 layerContext.fillStyle = bufferContext.fillStyle;
3922 layerContext.fillRect(sel.x, sel.y, sel.width, sel.height);
3930 * Crop the image to selection width and height. The selected pixels become
3933 * <p>This method invokes the {@link this#selectionMerge} and {@link
3934 * PaintWeb#imageCrop} methods.
3936 * @returns {Boolean} True if the operation was successful, or false if not.
3938 this.selectionCrop = function () {
3939 if (_self.state !== _self.STATE_SELECTED) {
3943 _self.selectionMerge();
3950 if (sumX > image.width) {
3951 w -= sumX - image.width;
3953 if (sumY > image.height) {
3954 h -= sumY - image.height;
3957 app.imageCrop(sel.x, sel.y, w, h);
3963 * The <code>keydown</code> event handler. This method calls selection-related
3964 * commands associated to keyboard shortcuts.
3966 * @param {Event} ev The DOM Event object.
3968 * @returns {Boolean} True if the keyboard shortcut was recognized, or false
3971 * @see PaintWeb.config.selection.keys holds the keyboard shortcuts
3974 this.keydown = function (ev) {
3976 case config.keys.transformToggle:
3977 // Toggle the selection transformation mode.
3978 config.transform = !config.transform;
3979 app.events.dispatch(new appEvent.configChange(config.transform,
3980 !config.transform, 'transform', 'selection', config));
3983 case config.keys.selectionCrop:
3984 return _self.selectionCrop(ev);
3986 case config.keys.selectionDelete:
3987 return _self.selectionDelete(ev);
3989 case config.keys.selectionDrop:
3990 return _self.selectionDrop(ev);
3992 case config.keys.selectionFill:
3993 return _self.selectionFill(ev);
4004 * @class Selection change event. This event is not cancelable.
4006 * @augments pwlib.appEvent
4008 * @param {Number} state Tells the new state of the selection.
4009 * @param {Number} [x] Selection start position on the x-axis of the image.
4010 * @param {Number} [y] Selection start position on the y-axis of the image.
4011 * @param {Number} [width] Selection width.
4012 * @param {Number} [height] Selection height.
4014 pwlib.appEvent.selectionChange = function (state, x, y, width, height) {
4016 * No selection is available.
4019 this.STATE_NONE = 0;
4022 * Selection available.
4025 this.STATE_SELECTED = 2;
4034 * Selection location on the x-axis of the image.
4040 * Selection location on the y-axis of the image.
4055 this.height = height;
4057 pwlib.appEvent.call(this, 'selectionChange');
4060 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
4063 * Copyright (C) 2008, 2009 Mihai Şucan
4065 * This file is part of PaintWeb.
4067 * PaintWeb is free software: you can redistribute it and/or modify
4068 * it under the terms of the GNU General Public License as published by
4069 * the Free Software Foundation, either version 3 of the License, or
4070 * (at your option) any later version.
4072 * PaintWeb is distributed in the hope that it will be useful,
4073 * but WITHOUT ANY WARRANTY; without even the implied warranty of
4074 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4075 * GNU General Public License for more details.
4077 * You should have received a copy of the GNU General Public License
4078 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
4080 * $URL: http://code.google.com/p/paintweb $
4081 * $Date: 2009-06-15 20:27:08 +0300 $
4085 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
4086 * @fileOverview Holds the hand tool implementation.
4090 * @class The hand tool. This tool allows the user to drag the image canvas
4091 * inside the viewport.
4093 * @param {PaintWeb} app Reference to the main paint application object.
4095 pwlib.tools.hand = function (app) {
4097 bufferCanvas = app.buffer.canvas,
4098 bufferStyle = bufferCanvas.style,
4099 config = app.config;
4100 clearInterval = app.win.clearInterval,
4102 MathRound = Math.round,
4104 viewport = app.gui.elems.viewport,
4107 setInterval = app.win.setInterval;
4110 * The interval ID used for invoking the viewport drag operation every few
4114 * @see PaintWeb.config.toolDrawDelay
4119 * Tells if the viewport needs to be scrolled.
4125 var needsScroll = false;
4128 * Holds the previous tool ID.
4133 this.prevTool = null;
4140 * Tool preactivation event handler.
4142 * @returns {Boolean} True if the tool can become active, or false if not.
4144 this.preActivate = function () {
4149 _self.prevTool = app.tool._id;
4151 // Check if the image canvas can be scrolled within the viewport.
4153 var cs = app.win.getComputedStyle(viewport, null),
4154 bwidth = parseInt(bufferStyle.width),
4155 bheight = parseInt(bufferStyle.height);
4157 vwidth = parseInt(cs.width),
4158 vheight = parseInt(cs.height);
4160 if (vheight < bheight || vwidth < bwidth) {
4168 * Tool activation event handler.
4170 this.activate = function () {
4171 bufferStyle.cursor = 'move';
4172 app.shadowDisallow();
4176 * Tool deactivation event handler.
4178 this.deactivate = function (ev) {
4180 clearInterval(timer);
4182 app.doc.removeEventListener('mousemove', ev_mousemove, false);
4183 app.doc.removeEventListener('mouseup', ev_mouseup, false);
4186 bufferStyle.cursor = '';
4191 * Initialize the canvas drag.
4193 * @param {Event} ev The DOM event object.
4195 this.mousedown = function (ev) {
4198 l0 = viewport.scrollLeft;
4199 t0 = viewport.scrollTop;
4201 needsScroll = false;
4203 app.doc.addEventListener('mousemove', ev_mousemove, false);
4204 app.doc.addEventListener('mouseup', ev_mouseup, false);
4207 timer = setInterval(viewportScroll, config.toolDrawDelay);
4214 * The <code>mousemove</code> event handler. This simply stores the current
4217 * @param {Event} ev The DOM Event object.
4219 function ev_mousemove (ev) {
4226 * Perform the canvas drag operation. This function is called every few
4229 * <p>Press <kbd>Escape</kbd> to stop dragging and to get back to the previous
4232 function viewportScroll () {
4234 viewport.scrollTop = t0 - y1 + y0;
4235 viewport.scrollLeft = l0 - x1 + x0;
4236 needsScroll = false;
4241 * The <code>mouseup</code> event handler.
4243 function ev_mouseup (ev) {
4245 clearInterval(timer);
4252 app.doc.removeEventListener('mousemove', ev_mousemove, false);
4253 app.doc.removeEventListener('mouseup', ev_mouseup, false);
4255 mouse.buttonDown = false;
4259 * Allows the user to press <kbd>Escape</kbd> to stop dragging the canvas, and
4260 * to return to the previous tool.
4262 * @param {Event} ev The DOM Event object.
4264 * @returns {Boolean} True if the key was recognized, or false if not.
4266 this.keydown = function (ev) {
4267 if (!_self.prevTool || ev.kid_ != 'Escape') {
4271 app.toolActivate(_self.prevTool, ev);
4276 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
4279 * Copyright (C) 2008, 2009 Mihai Şucan
4281 * This file is part of PaintWeb.
4283 * PaintWeb is free software: you can redistribute it and/or modify
4284 * it under the terms of the GNU General Public License as published by
4285 * the Free Software Foundation, either version 3 of the License, or
4286 * (at your option) any later version.
4288 * PaintWeb is distributed in the hope that it will be useful,
4289 * but WITHOUT ANY WARRANTY; without even the implied warranty of
4290 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4291 * GNU General Public License for more details.
4293 * You should have received a copy of the GNU General Public License
4294 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
4296 * $URL: http://code.google.com/p/paintweb $
4297 * $Date: 2009-06-11 20:21:13 +0300 $
4301 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
4302 * @fileOverview Holds the rectangle tool implementation.
4306 * @class The rectangle tool.
4308 * @param {PaintWeb} app Reference to the main paint application object.
4310 pwlib.tools.rectangle = function (app) {
4312 clearInterval = app.win.clearInterval,
4313 config = app.config,
4314 context = app.buffer.context,
4320 setInterval = app.win.setInterval;
4323 * The interval ID used for invoking the drawing operation every few
4327 * @see PaintWeb.config.toolDrawDelay
4332 * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the
4339 var shiftKey = false;
4342 * Tells if the drawing canvas needs to be updated or not.
4348 var needsRedraw = false;
4351 * Holds the starting point on the <var>x</var> axis of the image, for the
4352 * current drawing operation.
4360 * Holds the starting point on the <var>y</var> axis of the image, for the
4361 * current drawing operation.
4369 * Tool deactivation event handler.
4371 this.deactivate = function () {
4373 clearInterval(timer);
4377 if (mouse.buttonDown) {
4378 context.clearRect(0, 0, image.width, image.height);
4381 needsRedraw = false;
4385 * Initialize the drawing operation.
4387 * @param {Event} ev The DOM Event object.
4389 this.mousedown = function (ev) {
4394 timer = setInterval(_self.draw, config.toolDrawDelay);
4396 shiftKey = ev.shiftKey;
4397 needsRedraw = false;
4399 gui.statusShow('rectangleMousedown');
4405 * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
4407 * @param {Event} ev The DOM Event object.
4409 this.mousemove = function (ev) {
4410 shiftKey = ev.shiftKey;
4415 * Perform the drawing operation. This function is called every few
4418 * <p>Hold down the <kbd>Shift</kbd> key to draw a square.
4419 * <p>Press <kbd>Escape</kbd> to cancel the drawing operation.
4421 * @see PaintWeb.config.toolDrawDelay
4423 this.draw = function () {
4428 context.clearRect(0, 0, image.width, image.height);
4430 var x = MathMin(mouse.x, x0),
4431 y = MathMin(mouse.y, y0),
4432 w = MathAbs(mouse.x - x0),
4433 h = MathAbs(mouse.y - y0);
4436 needsRedraw = false;
4440 // Constrain the shape to a square
4455 if (config.shapeType != 'stroke') {
4456 context.fillRect(x, y, w, h);
4459 if (config.shapeType != 'fill') {
4460 context.strokeRect(x, y, w, h);
4463 needsRedraw = false;
4467 * End the drawing operation, once the user releases the mouse button.
4469 * @param {Event} ev The DOM Event object.
4471 this.mouseup = function (ev) {
4472 // Allow click+mousemove, not only mousedown+move+up
4473 if (mouse.x == x0 && mouse.y == y0) {
4474 mouse.buttonDown = true;
4479 clearInterval(timer);
4483 shiftKey = ev.shiftKey;
4486 gui.statusShow('rectangleActive');
4492 * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
4494 * @param {Event} ev The DOM Event object.
4496 * @returns {Boolean} True if the drawing operation was cancelled, or false if
4499 this.keydown = function (ev) {
4500 if (!mouse.buttonDown || ev.kid_ != 'Escape') {
4505 clearInterval(timer);
4509 context.clearRect(0, 0, image.width, image.height);
4510 mouse.buttonDown = false;
4511 needsRedraw = false;
4513 gui.statusShow('rectangleActive');
4519 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
4523 * Copyright (C) 2008, 2009 Mihai Şucan
4525 * This file is part of PaintWeb.
4527 * PaintWeb is free software: you can redistribute it and/or modify
4528 * it under the terms of the GNU General Public License as published by
4529 * the Free Software Foundation, either version 3 of the License, or
4530 * (at your option) any later version.
4532 * PaintWeb is distributed in the hope that it will be useful,
4533 * but WITHOUT ANY WARRANTY; without even the implied warranty of
4534 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4535 * GNU General Public License for more details.
4537 * You should have received a copy of the GNU General Public License
4538 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
4540 * $URL: http://code.google.com/p/paintweb $
4541 * $Date: 2009-07-01 18:44:56 +0300 $
4545 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
4546 * @fileOverview Holds the ellipse tool implementation.
4550 * @class The ellipse tool.
4552 * @param {PaintWeb} app Reference to the main paint application object.
4554 pwlib.tools.ellipse = function (app) {
4556 clearInterval = app.win.clearInterval,
4557 config = app.config,
4558 context = app.buffer.context,
4564 setInterval = app.win.setInterval;
4567 * The interval ID used for invoking the drawing operation every few
4571 * @see PaintWeb.config.toolDrawDelay
4576 * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the
4583 var shiftKey = false;
4586 * Tells if the drawing canvas needs to be updated or not.
4592 var needsRedraw = false;
4594 var K = 4*((Math.SQRT2-1)/3);
4597 * Holds the starting point on the <var>x</var> axis of the image, for the
4598 * current drawing operation.
4606 * Holds the starting point on the <var>y</var> axis of the image, for the
4607 * current drawing operation.
4615 * Tool deactivation event handler.
4617 this.deactivate = function () {
4619 clearInterval(timer);
4623 if (mouse.buttonDown) {
4624 context.clearRect(0, 0, image.width, image.height);
4627 needsRedraw = false;
4633 * Initialize the drawing operation.
4635 * @param {Event} ev The DOM Event object.
4637 this.mousedown = function (ev) {
4638 // The mouse start position
4643 timer = setInterval(_self.draw, config.toolDrawDelay);
4645 shiftKey = ev.shiftKey;
4646 needsRedraw = false;
4648 gui.statusShow('ellipseMousedown');
4654 * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
4656 * @param {Event} ev The DOM Event object.
4658 this.mousemove = function (ev) {
4659 shiftKey = ev.shiftKey;
4664 * Perform the drawing operation. This function is called every few
4667 * <p>Hold down the <kbd>Shift</kbd> key to draw a circle.
4668 * <p>Press <kbd>Escape</kbd> to cancel the drawing operation.
4670 * @see PaintWeb.config.toolDrawDelay
4672 this.draw = function () {
4677 context.clearRect(0, 0, image.width, image.height);
4679 var rectx0 = MathMin(mouse.x, x0),
4680 rectx1 = MathMax(mouse.x, x0),
4681 recty0 = MathMin(mouse.y, y0),
4682 recty1 = MathMax(mouse.y, y0);
4686 A(rectx0, recty0), B(rectx1, recty0), C(rectx1, recty1), D(rectx0, recty1)
4689 var w = rectx1-rectx0,
4693 needsRedraw = false;
4697 // Constrain the ellipse to be a circle
4701 if (recty0 == mouse.y) {
4708 if (rectx0 == mouse.x) {
4724 // Ellipse radius*Kappa, for the Bézier curve control points
4728 context.beginPath();
4731 context.moveTo(cx, recty0);
4733 // Control points: cp1x, cp1y, cp2x, cp2y, destx, desty
4734 // go clockwise: top-middle, right-middle, bottom-middle, then left-middle
4735 context.bezierCurveTo(cx + rx, recty0, rectx1, cy - ry, rectx1, cy);
4736 context.bezierCurveTo(rectx1, cy + ry, cx + rx, recty1, cx, recty1);
4737 context.bezierCurveTo(cx - rx, recty1, rectx0, cy + ry, rectx0, cy);
4738 context.bezierCurveTo(rectx0, cy - ry, cx - rx, recty0, cx, recty0);
4740 if (config.shapeType != 'stroke') {
4743 if (config.shapeType != 'fill') {
4747 context.closePath();
4749 needsRedraw = false;
4753 * End the drawing operation, once the user releases the mouse button.
4755 * @param {Event} ev The DOM Event object.
4757 this.mouseup = function (ev) {
4758 // Allow click+mousemove, not only mousedown+move+up
4759 if (mouse.x == x0 && mouse.y == y0) {
4760 mouse.buttonDown = true;
4765 clearInterval(timer);
4769 shiftKey = ev.shiftKey;
4772 gui.statusShow('ellipseActive');
4778 * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
4780 * @param {Event} ev The DOM Event object.
4782 * @returns {Boolean} True if the drawing operation was cancelled, or false if
4785 this.keydown = function (ev) {
4786 if (!mouse.buttonDown || ev.kid_ != 'Escape') {
4791 clearInterval(timer);
4795 context.clearRect(0, 0, image.width, image.height);
4796 mouse.buttonDown = false;
4797 needsRedraw = false;
4799 gui.statusShow('ellipseActive');
4805 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
4808 * Copyright (C) 2008, 2009 Mihai Şucan
4810 * This file is part of PaintWeb.
4812 * PaintWeb is free software: you can redistribute it and/or modify
4813 * it under the terms of the GNU General Public License as published by
4814 * the Free Software Foundation, either version 3 of the License, or
4815 * (at your option) any later version.
4817 * PaintWeb is distributed in the hope that it will be useful,
4818 * but WITHOUT ANY WARRANTY; without even the implied warranty of
4819 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4820 * GNU General Public License for more details.
4822 * You should have received a copy of the GNU General Public License
4823 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
4825 * $URL: http://code.google.com/p/paintweb $
4826 * $Date: 2009-06-11 20:28:07 +0300 $
4830 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
4831 * @fileOverview Holds the polygon tool implementation.
4835 * @class The polygon tool.
4837 * @param {PaintWeb} app Reference to the main paint application object.
4839 pwlib.tools.polygon = function (app) {
4841 clearInterval = app.win.clearInterval,
4842 config = app.config,
4843 context = app.buffer.context,
4848 setInterval = app.win.setInterval,
4849 snapXY = app.toolSnapXY;
4852 * Holds the points in the polygon being drawn.
4860 * The interval ID used for invoking the drawing operation every few
4864 * @see PaintWeb.config.toolDrawDelay
4869 * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the
4876 var shiftKey = false;
4879 * Tells if the drawing canvas needs to be updated or not.
4885 var needsRedraw = false;
4888 * The tool deactivation method, used for clearing the buffer.
4890 this.deactivate = function () {
4892 clearInterval(timer);
4896 if (points.length) {
4897 context.clearRect(0, 0, image.width, image.height);
4900 needsRedraw = false;
4907 * The <code>mousedown</code> event handler.
4909 * @param {Event} ev The DOM Event object.
4910 * @returns {Boolean} True if the event handler executed, or false if not.
4912 this.mousedown = function (ev) {
4913 if (points.length == 0) {
4914 points.push([mouse.x, mouse.y]);
4918 timer = setInterval(_self.draw, config.toolDrawDelay);
4921 shiftKey = ev.shiftKey;
4922 needsRedraw = false;
4924 gui.statusShow('polygonMousedown');
4930 * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
4932 * @param {Event} ev The DOM Event object.
4934 this.mousemove = function (ev) {
4935 shiftKey = ev.shiftKey;
4942 * @see PaintWeb.config.toolDrawDelay
4944 this.draw = function (ev) {
4949 var n = points.length;
4951 if (!n || (n == 1 && !mouse.buttonDown)) {
4952 needsRedraw = false;
4956 // Snapping on the X/Y axis for the current point (if available).
4957 if (mouse.buttonDown && shiftKey) {
4958 snapXY(points[n-1][0], points[n-1][1]);
4961 context.clearRect(0, 0, image.width, image.height);
4962 context.beginPath();
4963 context.moveTo(points[0][0], points[0][1]);
4965 // Draw the path of the polygon
4966 for (var i = 0; i < n; i++) {
4967 context.lineTo(points[i][0], points[i][1]);
4970 if (mouse.buttonDown) {
4971 context.lineTo(mouse.x, mouse.y);
4974 if (config.shapeType != 'stroke') {
4978 // In the case where we only have a straight line, draw a stroke even if no
4979 // stroke should be drawn, such that the user has better visual feedback.
4980 if (config.shapeType != 'fill' || n == 1) {
4984 context.closePath();
4986 needsRedraw = false;
4990 * The <code>mouseup</code> event handler.
4992 * @param {Event} ev The DOM Event object.
4993 * @returns {Boolean} True if the event handler executed, or false if not.
4995 this.mouseup = function (ev) {
4996 var n = points.length;
4998 // Allow click+mousemove+click, not only mousedown+mousemove+mouseup.
4999 // Do this only for the first point in the polygon.
5000 if (n == 1 && mouse.x == points[n-1][0] && mouse.y == points[n-1][1]) {
5001 mouse.buttonDown = true;
5006 clearInterval(timer);
5010 shiftKey = ev.shiftKey;
5014 snapXY(points[n-1][0], points[n-1][1]);
5017 var diffx1 = MathAbs(mouse.x - points[0][0]),
5018 diffy1 = MathAbs(mouse.y - points[0][1]),
5019 diffx2 = MathAbs(mouse.x - points[n-1][0]),
5020 diffy2 = MathAbs(mouse.y - points[n-1][1]);
5022 // End the polygon if the new point is close enough to the first/last point.
5023 if ((diffx1 < 5 && diffy1 < 5) || (diffx2 < 5 && diffy2 < 5)) {
5024 // Add the start point to complete the polygon shape.
5025 points.push(points[0]);
5030 gui.statusShow('polygonActive');
5037 gui.statusShow('polygonEnd');
5039 gui.statusShow('polygonAddPoint');
5042 points.push([mouse.x, mouse.y]);
5049 * The <code>keydown</code> event handler. This method allows the user to
5050 * cancel drawing the current polygon, using the <kbd>Escape</kbd> key. The
5051 * <kbd>Enter</kbd> key can be used to accept the current polygon shape, and
5052 * end the drawing operation.
5054 * @param {Event} ev The DOM Event object.
5056 * @returns {Boolean} True if the keyboard shortcut was recognized, or false
5059 this.keydown = function (ev) {
5060 var n = points.length;
5061 if (!n || (ev.kid_ != 'Escape' && ev.kid_ != 'Enter')) {
5066 clearInterval(timer);
5069 mouse.buttonDown = false;
5071 if (ev.kid_ == 'Escape') {
5072 context.clearRect(0, 0, image.width, image.height);
5073 needsRedraw = false;
5075 } else if (ev.kid_ == 'Enter') {
5076 // Add the point of the last mousemove event, and the start point, to
5077 // complete the polygon.
5078 points.push([mouse.x, mouse.y]);
5079 points.push(points[0]);
5086 gui.statusShow('polygonActive');
5092 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
5095 * Copyright (C) 2008, 2009 Mihai Şucan
5097 * This file is part of PaintWeb.
5099 * PaintWeb is free software: you can redistribute it and/or modify
5100 * it under the terms of the GNU General Public License as published by
5101 * the Free Software Foundation, either version 3 of the License, or
5102 * (at your option) any later version.
5104 * PaintWeb is distributed in the hope that it will be useful,
5105 * but WITHOUT ANY WARRANTY; without even the implied warranty of
5106 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
5107 * GNU General Public License for more details.
5109 * You should have received a copy of the GNU General Public License
5110 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
5112 * $URL: http://code.google.com/p/paintweb $
5113 * $Date: 2009-06-11 20:23:04 +0300 $
5117 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
5118 * @fileOverview Holds the line tool implementation.
5122 * @class The line tool.
5124 * @param {PaintWeb} app Reference to the main paint application object.
5126 pwlib.tools.line = function (app) {
5128 clearInterval = app.win.clearInterval,
5129 config = app.config,
5130 context = app.buffer.context,
5134 setInterval = app.win.setInterval,
5135 snapXY = app.toolSnapXY;
5138 * The interval ID used for invoking the drawing operation every few
5142 * @see PaintWeb.config.toolDrawDelay
5147 * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the
5154 var shiftKey = false;
5157 * Tells if the drawing canvas needs to be updated or not.
5163 var needsRedraw = false;
5166 * Holds the starting point on the <var>x</var> axis of the image, for the
5167 * current drawing operation.
5175 * Holds the starting point on the <var>y</var> axis of the image, for the
5176 * current drawing operation.
5184 * Tool deactivation event handler.
5186 this.deactivate = function () {
5188 clearInterval(timer);
5192 if (mouse.buttonDown) {
5193 context.clearRect(0, 0, image.width, image.height);
5196 needsRedraw = false;
5202 * Initialize the drawing operation, by storing the location of the pointer,
5203 * the start position.
5205 * @param {Event} ev The DOM Event object.
5207 this.mousedown = function (ev) {
5212 timer = setInterval(_self.draw, config.toolDrawDelay);
5214 shiftKey = ev.shiftKey;
5215 needsRedraw = false;
5217 gui.statusShow('lineMousedown');
5223 * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
5225 * @param {Event} ev The DOM Event object.
5227 this.mousemove = function (ev) {
5228 shiftKey = ev.shiftKey;
5233 * Perform the drawing operation. This function is called every few
5236 * <p>Hold down the <kbd>Shift</kbd> key to draw a straight
5237 * horizontal/vertical line.
5238 * <p>Press <kbd>Escape</kbd> to cancel the drawing operation.
5240 * @see PaintWeb.config.toolDrawDelay
5242 this.draw = function () {
5247 context.clearRect(0, 0, image.width, image.height);
5249 // Snapping on the X/Y axis.
5254 context.beginPath();
5255 context.moveTo(x0, y0);
5256 context.lineTo(mouse.x, mouse.y);
5258 context.closePath();
5260 needsRedraw = false;
5264 * End the drawing operation, once the user releases the mouse button.
5266 * @param {Event} ev The DOM Event object.
5268 this.mouseup = function (ev) {
5269 // Allow users to click then drag, not only mousedown+drag+mouseup.
5270 if (mouse.x == x0 && mouse.y == y0) {
5271 mouse.buttonDown = true;
5276 clearInterval(timer);
5280 shiftKey = ev.shiftKey;
5282 gui.statusShow('lineActive');
5289 * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
5291 * @param {Event} ev The DOM Event object.
5293 * @returns {Boolean} True if the drawing operation was cancelled, or false if
5296 this.keydown = function (ev) {
5297 if (!mouse.buttonDown || ev.kid_ != 'Escape') {
5302 clearInterval(timer);
5306 context.clearRect(0, 0, image.width, image.height);
5307 mouse.buttonDown = false;
5308 needsRedraw = false;
5310 gui.statusShow('lineActive');
5316 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
5319 * Copyright (C) 2008, 2009 Mihai Şucan
5321 * This file is part of PaintWeb.
5323 * PaintWeb is free software: you can redistribute it and/or modify
5324 * it under the terms of the GNU General Public License as published by
5325 * the Free Software Foundation, either version 3 of the License, or
5326 * (at your option) any later version.
5328 * PaintWeb is distributed in the hope that it will be useful,
5329 * but WITHOUT ANY WARRANTY; without even the implied warranty of
5330 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
5331 * GNU General Public License for more details.
5333 * You should have received a copy of the GNU General Public License
5334 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
5336 * $URL: http://code.google.com/p/paintweb $
5337 * $Date: 2009-06-22 22:31:49 +0300 $
5341 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
5342 * @fileOverview Holds the text tool implementation.
5345 // TODO: make this tool nicer to use.
5348 * @class The text tool.
5350 * @param {PaintWeb} app Reference to the main paint application object.
5352 pwlib.tools.text = function (app) {
5354 clearInterval = app.win.clearInterval,
5355 config = app.config.text,
5356 context = app.buffer.context,
5361 MathRound = Math.round,
5363 setInterval = app.win.setInterval;
5366 * The interval ID used for invoking the drawing operation every few
5370 * @see PaintWeb.config.toolDrawDelay
5375 * Holds the previous tool ID.
5380 var prevTool = app.tool ? app.tool._id : null;
5383 * Tells if the drawing canvas needs to be updated or not.
5389 var needsRedraw = false;
5391 var inputString = null,
5392 input_fontFamily = null,
5393 ev_configChangeId = null,
5394 ns_svg = "http://www.w3.org/2000/svg",
5401 * Tool preactivation code. This method check if the browser has support for
5402 * rendering text in Canvas.
5404 * @returns {Boolean} True if the tool can be activated successfully, or false
5407 this.preActivate = function () {
5408 if (!gui.inputs.textString || !gui.inputs.text_fontFamily ||
5409 !gui.elems.viewport) {
5414 // Canvas 2D Text API
5415 if (context.fillText && context.strokeText) {
5419 // Opera can only render text via SVG Text.
5420 // Note: support for Opera has been disabled.
5421 // There are severe SVG redraw issues when updating the SVG text element.
5422 // Besides, there are important memory leaks.
5423 // Ultimately, there's a deal breaker: security violation. The SVG document
5424 // which is rendered inside Canvas is considered "external"
5425 // - get/putImageData() and toDataURL() stop working after drawImage(svg) is
5427 /*if (pwlib.browser.opera) {
5431 // Gecko 1.9.0 had its own proprietary Canvas 2D Text API.
5432 if (context.mozPathText) {
5436 alert(lang.errorTextUnsupported);
5441 * The tool activation code. This sets up a few variables, starts the drawing
5442 * timer and adds event listeners as needed.
5444 this.activate = function () {
5445 // Reset the mouse coordinates to the scroll top/left corner such that the
5446 // text is rendered there.
5447 mouse.x = Math.round(gui.elems.viewport.scrollLeft / image.canvasScale),
5448 mouse.y = Math.round(gui.elems.viewport.scrollTop / image.canvasScale),
5450 input_fontFamily = gui.inputs.text_fontFamily;
5451 inputString = gui.inputs.textString;
5453 if (!context.fillText && pwlib.browser.opera) {
5454 ev_configChangeId = app.events.add('configChange', ev_configChange_opera);
5455 inputString.addEventListener('input', ev_configChange_opera, false);
5456 inputString.addEventListener('change', ev_configChange_opera, false);
5458 ev_configChangeId = app.events.add('configChange', ev_configChange);
5459 inputString.addEventListener('input', ev_configChange, false);
5460 inputString.addEventListener('change', ev_configChange, false);
5463 // Render text using the Canvas 2D context text API defined by HTML 5.
5464 if (context.fillText && context.strokeText) {
5465 _self.draw = _self.draw_spec;
5467 } else if (pwlib.browser.opera) {
5468 // Render text using a SVG Text element which is copied into Canvas using
5470 _self.draw = _self.draw_opera;
5473 } else if (context.mozPathText) {
5474 // Render text using proprietary API available in Gecko 1.9.0.
5475 _self.draw = _self.draw_moz;
5476 textWidth = context.mozMeasureText(inputString.value);
5480 timer = setInterval(_self.draw, app.config.toolDrawDelay);
5486 * The tool deactivation simply consists of removing the event listeners added
5487 * when the tool was constructed, and clearing the buffer canvas.
5489 this.deactivate = function () {
5491 clearInterval(timer);
5494 needsRedraw = false;
5496 if (ev_configChangeId) {
5497 app.events.remove('configChange', ev_configChangeId);
5500 if (!context.fillText && pwlib.browser.opera) {
5501 inputString.removeEventListener('input', ev_configChange_opera, false);
5502 inputString.removeEventListener('change', ev_configChange_opera, false);
5504 inputString.removeEventListener('input', ev_configChange, false);
5505 inputString.removeEventListener('change', ev_configChange, false);
5511 context.clearRect(0, 0, image.width, image.height);
5517 * Initialize the SVG document for Opera. This is used for rendering the text.
5520 function initOpera () {
5521 svgDoc = doc.createElementNS(ns_svg, 'svg');
5522 svgDoc.setAttributeNS(ns_svg, 'version', '1.1');
5524 svgText = doc.createElementNS(ns_svg, 'text');
5525 svgText.appendChild(doc.createTextNode(inputString.value));
5526 svgDoc.appendChild(svgText);
5528 svgText.style.font = context.font;
5530 if (app.config.shapeType !== 'stroke') {
5531 svgText.style.fill = context.fillStyle;
5533 svgText.style.fill = 'none';
5536 if (app.config.shapeType !== 'fill') {
5537 svgText.style.stroke = context.strokeStyle;
5538 svgText.style.strokeWidth = context.lineWidth;
5540 svgText.style.stroke = 'none';
5541 svgText.style.strokeWidth = context.lineWidth;
5544 textWidth = svgText.getComputedTextLength();
5545 textHeight = svgText.getBBox().height;
5547 svgDoc.setAttributeNS(ns_svg, 'width', textWidth);
5548 svgDoc.setAttributeNS(ns_svg, 'height', textHeight + 10);
5549 svgText.setAttributeNS(ns_svg, 'x', 0);
5550 svgText.setAttributeNS(ns_svg, 'y', textHeight);
5554 * The <code>configChange</code> application event handler. This is also the
5555 * <code>input</code> and <code>change</code> event handler for the text
5556 * string input element. This method updates the Canvas text-related
5557 * properties as needed, and re-renders the text.
5559 * <p>This function is not used on Opera.
5561 * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event
5564 function ev_configChange (ev) {
5565 if (ev.type === 'input' || ev.type === 'change' ||
5566 (!ev.group && ev.config === 'shapeType') ||
5567 (ev.group === 'line' && ev.config === 'lineWidth')) {
5570 // Update the text width.
5571 if (!context.fillText && context.mozMeasureText) {
5572 textWidth = context.mozMeasureText(inputString.value);
5577 if (ev.type !== 'configChange' && ev.group !== 'text') {
5583 switch (ev.config) {
5585 if (ev.value === '+') {
5594 if (config.italic) {
5597 font += config.fontSize + 'px ' + config.fontFamily;
5598 context.font = font;
5600 if ('mozTextStyle' in context) {
5601 context.mozTextStyle = font;
5605 case 'textBaseline':
5609 // Update the text width.
5610 if (ev.config !== 'textAlign' && ev.config !== 'textBaseline' &&
5611 !context.fillText && context.mozMeasureText) {
5612 textWidth = context.mozMeasureText(inputString.value);
5617 * The <code>configChange</code> application event handler. This is also the
5618 * <code>input</code> and <code>change</code> event handler for the text
5619 * string input element. This method updates the Canvas text-related
5620 * properties as needed, and re-renders the text.
5622 * <p>This is function is specific to Opera.
5624 * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event
5627 function ev_configChange_opera (ev) {
5628 if (ev.type === 'input' || ev.type === 'change') {
5629 svgText.replaceChild(doc.createTextNode(this.value), svgText.firstChild);
5633 if (!ev.group && ev.config === 'shapeType') {
5634 if (ev.value !== 'stroke') {
5635 svgText.style.fill = context.fillStyle;
5637 svgText.style.fill = 'none';
5640 if (ev.value !== 'fill') {
5641 svgText.style.stroke = context.strokeStyle;
5642 svgText.style.strokeWidth = context.lineWidth;
5644 svgText.style.stroke = 'none';
5645 svgText.style.strokeWidth = context.lineWidth;
5650 if (!ev.group && ev.config === 'fillStyle') {
5651 if (app.config.shapeType !== 'stroke') {
5652 svgText.style.fill = ev.value;
5657 if ((!ev.group && ev.config === 'strokeStyle') ||
5658 (ev.group === 'line' && ev.config === 'lineWidth')) {
5659 if (app.config.shapeType !== 'fill') {
5660 svgText.style.stroke = context.strokeStyle;
5661 svgText.style.strokeWidth = context.lineWidth;
5666 if (ev.type === 'configChange' && ev.group === 'text') {
5668 switch (ev.config) {
5670 if (ev.value === '+') {
5679 if (config.italic) {
5682 font += config.fontSize + 'px ' + config.fontFamily;
5683 context.font = font;
5684 svgText.style.font = font;
5687 case 'textBaseline':
5692 textWidth = svgText.getComputedTextLength();
5693 textHeight = svgText.getBBox().height;
5695 svgDoc.setAttributeNS(ns_svg, 'width', textWidth);
5696 svgDoc.setAttributeNS(ns_svg, 'height', textHeight + 10);
5697 svgText.setAttributeNS(ns_svg, 'x', 0);
5698 svgText.setAttributeNS(ns_svg, 'y', textHeight);
5702 * Add a new font family into the font family drop down. This function is
5703 * invoked by the <code>ev_configChange()</code> function when the user
5704 * attempts to add a new font family.
5708 * @param {pwlib.appEvent.configChange} ev The application event object.
5710 function fontFamilyAdd (ev) {
5711 var new_font = prompt(lang.promptTextFont) || '';
5712 new_font = new_font.replace(/^\s+/, '').replace(/\s+$/, '') ||
5715 // Check if the font name is already in the list.
5716 var opt, new_font2 = new_font.toLowerCase(),
5717 n = input_fontFamily.options.length;
5719 for (var i = 0; i < n; i++) {
5720 opt = input_fontFamily.options[i];
5721 if (opt.value.toLowerCase() == new_font2) {
5722 config.fontFamily = opt.value;
5723 input_fontFamily.selectedIndex = i;
5724 input_fontFamily.value = config.fontFamily;
5725 ev.value = config.fontFamily;
5731 // Add the new font.
5732 opt = doc.createElement('option');
5733 opt.value = new_font;
5734 opt.appendChild(doc.createTextNode(new_font));
5735 input_fontFamily.insertBefore(opt, input_fontFamily.options[n-1]);
5736 input_fontFamily.selectedIndex = n-1;
5737 input_fontFamily.value = new_font;
5738 ev.value = new_font;
5739 config.fontFamily = new_font;
5743 * The <code>mousemove</code> event handler.
5745 this.mousemove = function () {
5750 * Perform the drawing operation using standard 2D context methods.
5752 * @see PaintWeb.config.toolDrawDelay
5754 this.draw_spec = function () {
5759 context.clearRect(0, 0, image.width, image.height);
5761 if (app.config.shapeType != 'stroke') {
5762 context.fillText(inputString.value, mouse.x, mouse.y);
5765 if (app.config.shapeType != 'fill') {
5766 context.strokeText(inputString.value, mouse.x, mouse.y);
5769 needsRedraw = false;
5773 * Perform the drawing operation in Gecko 1.9.0.
5775 this.draw_moz = function () {
5780 context.clearRect(0, 0, image.width, image.height);
5785 if (config.textAlign === 'center') {
5786 x -= MathRound(textWidth / 2);
5787 } else if (config.textAlign === 'right') {
5791 if (config.textBaseline === 'top') {
5792 y += config.fontSize;
5793 } else if (config.textBaseline === 'middle') {
5794 y += MathRound(config.fontSize / 2);
5797 context.setTransform(1, 0, 0, 1, x, y);
5798 context.beginPath();
5799 context.mozPathText(inputString.value);
5801 if (app.config.shapeType != 'stroke') {
5805 if (app.config.shapeType != 'fill') {
5808 context.closePath();
5809 context.setTransform(1, 0, 0, 1, 0, 0);
5811 needsRedraw = false;
5815 * Perform the drawing operation in Opera using SVG.
5817 this.draw_opera = function () {
5822 context.clearRect(0, 0, image.width, image.height);
5827 if (config.textAlign === 'center') {
5828 x -= MathRound(textWidth / 2);
5829 } else if (config.textAlign === 'right') {
5833 if (config.textBaseline === 'bottom') {
5835 } else if (config.textBaseline === 'middle') {
5836 y -= MathRound(textHeight / 2);
5839 context.drawImage(svgDoc, x, y);
5841 needsRedraw = false;
5845 * The <code>click</code> event handler. This method completes the drawing
5846 * operation by inserting the text into the layer canvas.
5848 this.click = function () {
5854 * The <code>keydown</code> event handler allows users to press the
5855 * <kbd>Escape</kbd> key to cancel the drawing operation and return to the
5858 * @param {Event} ev The DOM Event object.
5859 * @returns {Boolean} True if the key was recognized, or false if not.
5861 this.keydown = function (ev) {
5862 if (!prevTool || ev.kid_ != 'Escape') {
5866 mouse.buttonDown = false;
5867 app.toolActivate(prevTool, ev);
5873 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
5876 * Copyright (C) 2008, 2009 Mihai Şucan
5878 * This file is part of PaintWeb.
5880 * PaintWeb is free software: you can redistribute it and/or modify
5881 * it under the terms of the GNU General Public License as published by
5882 * the Free Software Foundation, either version 3 of the License, or
5883 * (at your option) any later version.
5885 * PaintWeb is distributed in the hope that it will be useful,
5886 * but WITHOUT ANY WARRANTY; without even the implied warranty of
5887 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
5888 * GNU General Public License for more details.
5890 * You should have received a copy of the GNU General Public License
5891 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
5893 * $URL: http://code.google.com/p/paintweb $
5894 * $Date: 2009-07-01 18:43:53 +0300 $
5898 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
5899 * @fileOverview Holds the Bézier curve tool implementation.
5903 * @class The Bézier curve tool.
5905 * @param {PaintWeb} app Reference to the main paint application object.
5907 pwlib.tools.bcurve = function (app) {
5909 clearInterval = app.win.clearInterval,
5910 config = app.config,
5911 context = app.buffer.context,
5915 setInterval = app.win.setInterval,
5916 snapXY = app.toolSnapXY;
5919 * Holds the points in the Bézier curve being drawn.
5927 * The interval ID used for invoking the drawing operation every few
5931 * @see PaintWeb.config.toolDrawDelay
5936 * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the
5943 var shiftKey = false;
5946 * Tells if the drawing canvas needs to be updated or not.
5952 var needsRedraw = false;
5955 * The tool deactivation method, used for clearing the buffer.
5957 this.deactivate = function () {
5959 clearInterval(timer);
5963 if (points.length) {
5964 context.clearRect(0, 0, image.width, image.height);
5967 needsRedraw = false;
5974 * The <code>mousedown</code> event handler.
5976 * @param {Event} ev The DOM Event object.
5978 this.mousedown = function (ev) {
5979 if (points.length == 0) {
5980 gui.statusShow('bcurveSnapping');
5981 points.push([mouse.x, mouse.y]);
5985 timer = setInterval(_self.draw, config.toolDrawDelay);
5988 shiftKey = ev.shiftKey;
5989 needsRedraw = false;
5995 * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
5997 * @param {Event} ev The DOM Event object.
5999 this.mousemove = function (ev) {
6000 shiftKey = ev.shiftKey;
6005 * Draw the Bézier curve, using the available points.
6007 * @see PaintWeb.config.toolDrawDelay
6009 this.draw = function () {
6014 var n = points.length;
6016 // Add the temporary point while the mouse button is down.
6017 if (mouse.buttonDown) {
6018 if (shiftKey && n == 1) {
6019 snapXY(points[0][0], points[0][1]);
6021 points.push([mouse.x, mouse.y]);
6028 p3 = points[3] || points[2],
6029 lineWidth = context.lineWidth,
6030 strokeStyle = context.strokeStyle;
6032 if (mouse.buttonDown) {
6036 context.clearRect(0, 0, image.width, image.height);
6039 needsRedraw = false;
6043 // Draw the main line
6045 context.beginPath();
6046 context.moveTo(p0[0], p0[1]+2);
6047 context.lineTo(p1[0], p1[1]+2);
6048 context.lineWidth = 1;
6049 context.strokeStyle = '#000000';
6051 context.closePath();
6053 context.lineWidth = lineWidth;
6054 context.strokeStyle = strokeStyle;
6056 needsRedraw = false;
6060 // Draw the Bézier curve
6062 context.beginPath();
6063 context.moveTo(p0[0], p0[1]);
6064 context.bezierCurveTo(
6069 if (config.shapeType != 'stroke') {
6073 if (config.shapeType != 'fill') {
6077 context.closePath();
6079 needsRedraw = false;
6083 * The <code>mouseup</code> event handler. This method stores the current
6084 * mouse coordinates as a point to be used for drawing the Bézier curve.
6086 * @param {Event} ev The DOM Event object.
6088 this.mouseup = function (ev) {
6089 var n = points.length;
6091 // Allow click+mousemove+click, not only mousedown+mousemove+mouseup.
6092 // Do this only for the start point.
6093 if (n == 1 && mouse.x == points[0][0] && mouse.y == points[0][1]) {
6094 mouse.buttonDown = true;
6099 clearInterval(timer);
6103 if (n == 1 && ev.shiftKey) {
6104 snapXY(points[0][0], points[0][1]);
6107 // We need 4 points to draw the Bézier curve: start, end, and two control
6110 points.push([mouse.x, mouse.y]);
6115 // Make sure the canvas is up-to-date.
6116 shiftKey = ev.shiftKey;
6119 if (n == 2 || n == 3) {
6120 gui.statusShow('bcurveControlPoint' + (n-1));
6121 } else if (n == 4) {
6122 gui.statusShow('bcurveActive');
6131 * The <code>keydown</code> event handler. This method allows the user to
6132 * press the <kbd>Escape</kbd> key to cancel the current drawing operation.
6134 * @param {Event} ev The DOM Event object.
6136 * @returns {Boolean} True if the keyboard shortcut was recognized, or false
6139 this.keydown = function (ev) {
6140 if (!points.length || ev.kid_ != 'Escape') {
6145 clearInterval(timer);
6149 context.clearRect(0, 0, image.width, image.height);
6152 needsRedraw = false;
6153 mouse.buttonDown = false;
6155 gui.statusShow('bcurveActive');
6161 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
6164 * Copyright (C) 2008, 2009 Mihai Şucan
6166 * This file is part of PaintWeb.
6168 * PaintWeb is free software: you can redistribute it and/or modify
6169 * it under the terms of the GNU General Public License as published by
6170 * the Free Software Foundation, either version 3 of the License, or
6171 * (at your option) any later version.
6173 * PaintWeb is distributed in the hope that it will be useful,
6174 * but WITHOUT ANY WARRANTY; without even the implied warranty of
6175 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6176 * GNU General Public License for more details.
6178 * You should have received a copy of the GNU General Public License
6179 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
6181 * $URL: http://code.google.com/p/paintweb $
6182 * $Date: 2009-07-06 16:20:38 +0300 $
6186 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
6187 * @fileOverview Holds the "Insert image" tool implementation.
6190 // TODO: allow inserting images from a different host, using server-side magic.
6193 * @class The "Insert image" tool.
6195 * @param {PaintWeb} app Reference to the main paint application object.
6197 pwlib.tools.insertimg = function (app) {
6199 canvasImage = app.image,
6200 clearInterval = app.win.clearInterval,
6201 config = app.config,
6202 context = app.buffer.context,
6207 MathRound = Math.round,
6209 setInterval = app.win.setInterval;
6212 * Holds the previous tool ID.
6217 var prevTool = app.tool ? app.tool._id : null;
6220 * The interval ID used for invoking the drawing operation every few
6224 * @see PaintWeb.config.toolDrawDelay
6229 * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the
6236 var shiftKey = false;
6239 * Tells if the drawing canvas needs to be updated or not.
6245 var needsRedraw = false;
6248 * Holds the starting point on the <var>x</var> axis of the image, for the
6249 * current drawing operation.
6257 * Holds the starting point on the <var>y</var> axis of the image, for the
6258 * current drawing operation.
6266 * Tells if the image element loaded or not.
6271 var imageLoaded = false;
6274 * Holds the image aspect ratio, used by the resize method.
6282 * Holds the DOM image element.
6287 var imageElement = null;
6290 * Holds the image address.
6294 this.url = 'http://';
6298 * The tool preactivation code. This function asks the user to provide an URL
6299 * to the image which is desired to be inserted into the canvas.
6301 * @returns {Boolean} True if the URL provided is correct. False is returned
6302 * if the URL is not provided or if it's incorrect. When false is returned the
6303 * tool activation is cancelled.
6305 this.preActivate = function () {
6306 if (!gui.elems.viewport) {
6310 _self.url = prompt(lang.promptInsertimg, _self.url);
6312 if (!_self.url || _self.url.toLowerCase() === 'http://') {
6316 // Remember the URL.
6317 pwlib.extend(true, _self.constructor.prototype, {url: _self.url});
6319 if (!pwlib.isSameHost(_self.url, app.win.location.host)) {
6320 alert(lang.errorInsertimgHost);
6328 * The tool activation event handler. This function is called once the
6329 * previous tool has been deactivated.
6331 this.activate = function () {
6332 imageElement = new Image();
6333 imageElement.addEventListener('load', ev_imageLoaded, false);
6334 imageElement.src = _self.url;
6340 * The tool deactivation event handler.
6342 this.deactivate = function () {
6344 imageElement = null;
6348 clearInterval(timer);
6351 needsRedraw = false;
6353 context.clearRect(0, 0, canvasImage.width, canvasImage.height);
6359 * The <code>load</code> event handler for the image element. This method
6360 * makes sure the image dimensions are synchronized with the zoom level, and
6361 * draws the image on the canvas.
6365 function ev_imageLoaded () {
6366 // Did the image already load?
6371 // The default position for the inserted image is the top left corner of the visible area, taking into consideration the zoom level.
6372 var x = MathRound(gui.elems.viewport.scrollLeft / canvasImage.canvasScale),
6373 y = MathRound(gui.elems.viewport.scrollTop / canvasImage.canvasScale);
6375 context.clearRect(0, 0, canvasImage.width, canvasImage.height);
6378 context.drawImage(imageElement, x, y);
6380 alert(lang.errorInsertimg);
6385 needsRedraw = false;
6388 timer = setInterval(_self.draw, config.toolDrawDelay);
6391 gui.statusShow('insertimgLoaded');
6395 * The <code>mousedown</code> event handler. This method stores the current
6396 * mouse location and the image aspect ratio for later reuse by the
6397 * <code>draw()</code> method.
6399 * @param {Event} ev The DOM Event object.
6401 this.mousedown = function (ev) {
6403 alert(lang.errorInsertimgNotLoaded);
6410 // The image aspect ratio - used by the draw() method when the user holds
6411 // the Shift key down.
6412 imageRatio = imageElement.width / imageElement.height;
6413 shiftKey = ev.shiftKey;
6415 gui.statusShow('insertimgResize');
6417 if (ev.stopPropagation) {
6418 ev.stopPropagation();
6423 * The <code>mousemove</code> event handler.
6425 * @param {Event} ev The DOM Event object.
6427 this.mousemove = function (ev) {
6428 shiftKey = ev.shiftKey;
6433 * Perform the drawing operation. When the mouse button is not down, the user
6434 * is allowed to pick where he/she wants to insert the image element, inside
6435 * the canvas. Once the <code>mousedown</code> event is fired, this method
6436 * allows the user to resize the image inside the canvas.
6438 * @see PaintWeb.config.toolDrawDelay
6440 this.draw = function () {
6441 if (!imageLoaded || !needsRedraw) {
6445 context.clearRect(0, 0, canvasImage.width, canvasImage.height);
6447 // If the user is holding down the mouse button, then allow him/her to
6448 // resize the image.
6449 if (mouse.buttonDown) {
6450 var w = MathAbs(mouse.x - x0),
6451 h = MathAbs(mouse.y - y0),
6452 x = MathMin(mouse.x, x0),
6453 y = MathMin(mouse.y, y0);
6456 needsRedraw = false;
6460 // If the Shift key is down, constrain the image to have the same aspect
6461 // ratio as the original image element.
6467 h = MathRound(w/imageRatio);
6472 w = MathRound(h*imageRatio);
6476 context.drawImage(imageElement, x, y, w, h);
6478 // If the mouse button is not down, simply allow the user to pick where
6479 // he/she wants to insert the image element.
6480 context.drawImage(imageElement, mouse.x, mouse.y);
6483 needsRedraw = false;
6487 * The <code>mouseup</code> event handler. This method completes the drawing
6488 * operation by inserting the image in the layer canvas, and by activating the
6491 * @param {Event} ev The DOM Event object.
6493 this.mouseup = function (ev) {
6499 clearInterval(timer);
6506 app.toolActivate(prevTool, ev);
6509 if (ev.stopPropagation) {
6510 ev.stopPropagation();
6515 * The <code>keydown</code> event handler allows users to press the
6516 * <kbd>Escape</kbd> key to cancel the drawing operation and return to the
6519 * @param {Event} ev The DOM Event object.
6520 * @returns {Boolean} True if the key was recognized, or false if not.
6522 this.keydown = function (ev) {
6523 if (!prevTool || ev.kid_ != 'Escape') {
6528 clearInterval(timer);
6532 mouse.buttonDown = false;
6533 app.toolActivate(prevTool, ev);
6539 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
6542 * Copyright (C) 2008, 2009 Mihai Şucan
6544 * This file is part of PaintWeb.
6546 * PaintWeb is free software: you can redistribute it and/or modify
6547 * it under the terms of the GNU General Public License as published by
6548 * the Free Software Foundation, either version 3 of the License, or
6549 * (at your option) any later version.
6551 * PaintWeb is distributed in the hope that it will be useful,
6552 * but WITHOUT ANY WARRANTY; without even the implied warranty of
6553 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6554 * GNU General Public License for more details.
6556 * You should have received a copy of the GNU General Public License
6557 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
6559 * $URL: http://code.google.com/p/paintweb $
6560 * $Date: 2009-06-15 15:25:29 +0300 $
6564 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
6565 * @fileOverview Holds the pencil tool implementation.
6569 * @class The drawing pencil.
6571 * @param {PaintWeb} app Reference to the main paint application object.
6573 pwlib.tools.pencil = function (app) {
6575 clearInterval = app.win.clearInterval,
6576 context = app.buffer.context,
6579 setInterval = app.win.setInterval;
6582 * The interval ID used for running the pencil drawing operation every few
6586 * @see PaintWeb.config.toolDrawDelay
6591 * Holds the points needed to be drawn. Each point is added by the
6592 * <code>mousemove</code> event handler.
6600 * Holds the last point on the <var>x</var> axis of the image, for the current
6601 * drawing operation.
6609 * Holds the last point on the <var>y</var> axis of the image, for the current
6610 * drawing operation.
6618 * Tool deactivation event handler.
6620 this.deactivate = function () {
6622 clearInterval(timer);
6626 if (mouse.buttonDown) {
6627 context.clearRect(0, 0, image.width, image.height);
6634 * Initialize the drawing operation.
6636 this.mousedown = function () {
6642 timer = setInterval(_self.draw, app.config.toolDrawDelay);
6649 * Save the mouse coordinates in the array.
6651 this.mousemove = function () {
6652 if (mouse.buttonDown) {
6653 points.push(mouse.x, mouse.y);
6658 * Draw the points in the stack. This function is called every few
6661 * @see PaintWeb.config.toolDrawDelay
6663 this.draw = function () {
6664 var i = 0, n = points.length;
6669 context.beginPath();
6670 context.moveTo(x0, y0);
6675 context.lineTo(x0, y0);
6679 context.closePath();
6685 * End the drawing operation, once the user releases the mouse button.
6687 this.mouseup = function () {
6688 if (mouse.x == x0 && mouse.y == y0) {
6689 points.push(x0+1, y0+1);
6693 clearInterval(timer);
6704 * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
6706 * @param {Event} ev The DOM Event object.
6708 * @returns {Boolean} True if the drawing operation was cancelled, or false if
6711 this.keydown = function (ev) {
6712 if (!mouse.buttonDown || ev.kid_ != 'Escape') {
6717 clearInterval(timer);
6721 context.clearRect(0, 0, image.width, image.height);
6722 mouse.buttonDown = false;
6729 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
6732 * Copyright (C) 2008, 2009 Mihai Şucan
6734 * This file is part of PaintWeb.
6736 * PaintWeb is free software: you can redistribute it and/or modify
6737 * it under the terms of the GNU General Public License as published by
6738 * the Free Software Foundation, either version 3 of the License, or
6739 * (at your option) any later version.
6741 * PaintWeb is distributed in the hope that it will be useful,
6742 * but WITHOUT ANY WARRANTY; without even the implied warranty of
6743 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6744 * GNU General Public License for more details.
6746 * You should have received a copy of the GNU General Public License
6747 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
6749 * $URL: http://code.google.com/p/paintweb $
6750 * $Date: 2009-07-02 15:37:38 +0300 $
6754 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
6755 * @fileOverview Holds the color picker implementation.
6759 * @class The color picker tool.
6761 * @param {PaintWeb} app Reference to the main paint application object.
6763 pwlib.tools.cpicker = function (app) {
6765 colormixer = app.extensions.colormixer,
6766 context = app.layer.context,
6769 MathRound = Math.round,
6773 * Holds the ID of the previously active tool. Once the user completes the
6774 * color picking operation, the previous tool is activated.
6779 var prevTool = null;
6782 * Holds a reference to the target color input. This is a GUI color input
6786 * @type pwlib.guiColorInput
6788 var targetInput = null;
6791 * Holds the previous color values - before the user started picking
6792 * a different color.
6797 var prevColor = null;
6800 * Tells if the color mixer is active for the current target input.
6805 var colormixerActive = false;
6808 * Tells if the current color values are accepted by the user. This value is
6809 * used by the tool deactivation code.
6814 var colorAccepted = false;
6817 * The <code>preActivate</code> event handler. This method checks if the
6818 * browser implements the <code>getImageData()</code> context method. If not,
6819 * the color picker tool cannot be used.
6821 this.preActivate = function () {
6822 // The latest versions of all browsers which implement Canvas, also
6823 // implement the getImageData() method. This was only a problem with some
6824 // old versions (eg. Opera 9.2).
6825 if (!context.getImageData) {
6826 alert(lang.errorCpickerUnsupported);
6830 if (app.tool && app.tool._id) {
6831 prevTool = app.tool._id;
6838 * The <code>activate</code> event handler. This method determines the current
6839 * target input in the Color Mixer, if any. Canvas shadow rendering is
6842 this.activate = function () {
6843 // When the color mixer panel is active, the color picker uses the same
6845 if (colormixer && colormixer.targetInput) {
6846 targetInput = gui.colorInputs[colormixer.targetInput.id];
6850 gui.statusShow('cpicker_' + targetInput.id);
6852 gui.statusShow('cpickerNormal');
6855 app.shadowDisallow();
6859 * The <code>deactivate</code> event handler. This method allows shadow
6860 * rendering again, and resets the color input values if the user did not
6861 * accept the new color.
6863 this.deactivate = function () {
6864 if (!colorAccepted && targetInput && prevColor) {
6865 updateColor(null, true);
6872 * The <code>mousedown</code> event handler. This method starts the color
6873 * picking operation.
6875 * @param {Event} ev The DOM Event object.
6877 this.mousedown = function (ev) {
6878 // We check again, because the user might have opened/closed the color
6880 if (colormixer && colormixer.targetInput) {
6881 targetInput = gui.colorInputs[colormixer.targetInput.id];
6885 colormixerActive = true;
6886 gui.statusShow('cpicker_' + targetInput.id);
6888 colormixerActive = false;
6889 gui.statusShow('cpickerNormal');
6891 // The context menu (right-click). This is unsupported by Opera.
6892 // Also allow Shift+Click for changing the stroke color (making it easier for Opera users).
6893 if (ev.button === 2 || ev.shiftKey) {
6894 targetInput = gui.colorInputs.strokeStyle;
6896 targetInput = gui.colorInputs.fillStyle;
6902 _self.mousemove = updateColor;
6909 * Perform color update. This function updates the target input or the Color
6910 * Mixer to hold the color value under the mouse - it actually performs the
6911 * color picking operation.
6913 * <p>This function is also the <code>mousemove</code> event handler for this
6916 * @param {Event} ev The DOM Event object.
6917 * @param {Boolean} [usePrevColor=false] Tells the function to use the
6918 * previous color values we have stored. This is used when the user cancels
6919 * the color picking operation.
6921 function updateColor (ev, usePrevColor) {
6926 var p = usePrevColor ? prevColor :
6927 context.getImageData(mouse.x, mouse.y, 1, 1),
6929 red: p.data[0] / 255,
6930 green: p.data[1] / 255,
6931 blue: p.data[2] / 255,
6932 alpha: (p.data[3] / 255).toFixed(3)
6935 if (colormixerActive) {
6936 colormixer.color.red = color.red;
6937 colormixer.color.green = color.green;
6938 colormixer.color.blue = color.blue;
6939 colormixer.color.alpha = color.alpha;
6940 colormixer.update_color('rgb');
6943 targetInput.updateColor(color);
6948 * The <code>mouseup</code> event handler. This method completes the color
6949 * picking operation, and activates the previous tool.
6951 * <p>The {@link pwlib.appEvent.configChange} application event is also
6952 * dispatched for the configuration property associated to the target input.
6954 * @param {Event} ev The DOM Event object.
6956 this.mouseup = function (ev) {
6961 delete _self.mousemove;
6963 colorAccepted = true;
6965 if (!colormixerActive) {
6966 var color = targetInput.color,
6967 configProperty = targetInput.configProperty,
6968 configGroup = targetInput.configGroup,
6969 configGroupRef = targetInput.configGroupRef,
6970 prevVal = configGroupRef[configProperty],
6971 newVal = 'rgba(' + MathRound(color.red * 255) + ',' +
6972 MathRound(color.green * 255) + ',' +
6973 MathRound(color.blue * 255) + ',' +
6976 if (prevVal !== newVal) {
6977 configGroupRef[configProperty] = newVal;
6978 app.events.dispatch(new pwlib.appEvent.configChange(newVal, prevVal,
6979 configProperty, configGroup, configGroupRef));
6984 app.toolActivate(prevTool, ev);
6991 * The <code>keydown</code> event handler. This method allows the user to
6992 * press the <kbd>Escape</kbd> key to cancel the color picking operation. By
6993 * doing so, the original color values are restored.
6995 * @param {Event} ev The DOM Event object.
6996 * @returns {Boolean} True if the keyboard shortcut was recognized, or false
6999 this.keydown = function (ev) {
7000 if (!prevTool || ev.kid_ !== 'Escape') {
7004 mouse.buttonDown = false;
7005 app.toolActivate(prevTool, ev);
7011 * The <code>contextmenu</code> event handler. This method only cancels the
7014 // Unfortunately, the contextmenu event is unsupported by Opera.
7015 this.contextmenu = function () {
7020 * Store the color values from the target color input, before this tool
7021 * changes the colors. The previous color values are used when the user
7022 * decides to cancel the color picking operation.
7025 function updatePrevColor () {
7026 // If the color mixer panel is visible, then we store the color values from
7027 // the color mixer, instead of those from the color input object.
7028 var color = colormixerActive ? colormixer.color : targetInput.color;
7034 MathRound(color.red * 255),
7035 MathRound(color.green * 255),
7036 MathRound(color.blue * 255),
7043 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
7046 * Copyright (C) 2008, 2009 Mihai Şucan
7048 * This file is part of PaintWeb.
7050 * PaintWeb is free software: you can redistribute it and/or modify
7051 * it under the terms of the GNU General Public License as published by
7052 * the Free Software Foundation, either version 3 of the License, or
7053 * (at your option) any later version.
7055 * PaintWeb is distributed in the hope that it will be useful,
7056 * but WITHOUT ANY WARRANTY; without even the implied warranty of
7057 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
7058 * GNU General Public License for more details.
7060 * You should have received a copy of the GNU General Public License
7061 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
7063 * $URL: http://code.google.com/p/paintweb $
7064 * $Date: 2009-07-01 18:46:31 +0300 $
7068 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
7069 * @fileOverview Holds the eraser tool implementation.
7073 * @class The eraser tool.
7075 * @param {PaintWeb} app Reference to the main paint application object.
7077 pwlib.tools.eraser = function (app) {
7079 clearInterval = app.win.clearInterval,
7080 config = app.config,
7081 context = app.buffer.context,
7083 layerContext = app.layer.context,
7085 setInterval = app.win.setInterval;
7088 * The interval ID used for running the pencil drawing operation every few
7092 * @see PaintWeb.config.toolDrawDelay
7097 * Holds the points needed to be drawn. Each point is added by the
7098 * <code>mousemove</code> event handler.
7106 * Holds the starting point on the <var>x</var> axis of the image, for the
7107 * current drawing operation.
7115 * Holds the starting point on the <var>y</var> axis of the image, for the
7116 * current drawing operation.
7123 var strokeStyle_ = null;
7126 * The tool deactivation event handler. This function clears timers, clears
7127 * the canvas and allows shadows to be rendered again.
7129 this.deactivate = function () {
7131 clearInterval(timer);
7135 if (mouse.buttonDown) {
7136 context.clearRect(0, 0, image.width, image.height);
7141 // Allow Canvas shadows.
7146 * The tool activation event handler. This is run after the tool construction
7147 * and after the deactivation of the previous tool. This function simply
7148 * disallows the rendering of shadows.
7150 this.activate = function () {
7151 // Do not allow Canvas shadows.
7152 app.shadowDisallow();
7156 * Initialize the drawing operation.
7158 this.mousedown = function () {
7159 // The mousedown event remembers the current strokeStyle and sets a white
7160 // colored stroke (same as the background), such that the user gets live
7161 // feedback of what he/she erases.
7163 strokeStyle_ = context.strokeStyle;
7164 context.strokeStyle = config.backgroundColor;
7171 timer = setInterval(_self.draw, config.toolDrawDelay);
7178 * Save the mouse coordinates in the array.
7180 this.mousemove = function () {
7181 if (mouse.buttonDown) {
7182 points.push(mouse.x, mouse.y);
7187 * Draw the points in the stack. This function is called every few
7190 * @see PaintWeb.config.toolDrawDelay
7192 this.draw = function () {
7193 var i = 0, n = points.length;
7198 context.beginPath();
7199 context.moveTo(x0, y0);
7204 context.lineTo(x0, y0);
7208 context.closePath();
7214 * End the drawing operation, once the user releases the mouse button.
7216 this.mouseup = function () {
7217 // The mouseup event handler changes the globalCompositeOperation to
7218 // destination-out such that the white pencil path drawn by the user cuts
7219 // out/clears the destination image
7221 if (mouse.x == x0 && mouse.y == y0) {
7222 points.push(x0+1, y0+1);
7226 clearInterval(timer);
7231 var op = layerContext.globalCompositeOperation;
7232 layerContext.globalCompositeOperation = 'destination-out';
7236 layerContext.globalCompositeOperation = op;
7237 context.strokeStyle = strokeStyle_;
7243 * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
7245 * @param {Event} ev The DOM Event object.
7247 * @returns {Boolean} True if the drawing operation was cancelled, or false if
7250 this.keydown = function (ev) {
7251 if (!mouse.buttonDown || ev.kid_ != 'Escape') {
7256 clearInterval(timer);
7260 context.clearRect(0, 0, image.width, image.height);
7261 context.strokeStyle = strokeStyle_;
7262 mouse.buttonDown = false;
7269 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
7273 * Copyright (C) 2008, 2009 Mihai Şucan
7275 * This file is part of PaintWeb.
7277 * PaintWeb is free software: you can redistribute it and/or modify
7278 * it under the terms of the GNU General Public License as published by
7279 * the Free Software Foundation, either version 3 of the License, or
7280 * (at your option) any later version.
7282 * PaintWeb is distributed in the hope that it will be useful,
7283 * but WITHOUT ANY WARRANTY; without even the implied warranty of
7284 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
7285 * GNU General Public License for more details.
7287 * You should have received a copy of the GNU General Public License
7288 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
7290 * $URL: http://code.google.com/p/paintweb $
7291 * $Date: 2009-07-09 14:26:21 +0300 $
7295 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
7296 * @fileOverview Holds the implementation of the Color Mixer dialog.
7299 // For the implementation of this extension I used the following references:
7300 // - Wikipedia articles on each subject.
7301 // - the great brucelindbloom.com Web site - lots of information.
7304 * @class The Color Mixer extension.
7306 * @param {PaintWeb} app Reference to the main paint application object.
7308 pwlib.extensions.colormixer = function (app) {
7310 config = app.config.colormixer,
7313 lang = app.lang.colormixer,
7314 MathFloor = Math.floor,
7318 MathRound = Math.round,
7319 resScale = app.resolution.scale;
7322 * Holds references to various DOM elements.
7329 * Reference to the element which holds Canvas controls (the dot on the
7330 * Canvas, and the slider).
7336 * Reference to the dot element that is rendered on top of the color space
7343 * Reference to the slider element.
7349 * Reference to the input element that allows the user to pick the color
7350 * palette to be displayed.
7353 'cpaletteInput': null,
7356 * The container element which holds the colors of the currently selected
7360 'cpaletteOutput': null,
7363 * Reference to the element which displays the current color.
7366 "colorActive": null,
7369 * Reference to the element which displays the old color.
7376 * Reference to the Color Mixer floating panel GUI component object.
7379 * @type pwlib.guiFloatingPanel
7384 * Reference to the Color Mixer tab panel GUI component object which holds the
7388 * @type pwlib.guiTabPanel
7390 this.panelInputs = null;
7393 * Reference to the Color Mixer tab panel GUI component object which holds the
7394 * Canvas used for color space visualisation and the color palettes selector.
7397 * @type pwlib.guiTabPanel
7399 this.panelSelector = null;
7402 * Holds a reference to the 2D context of the color mixer Canvas element. This
7403 * is where the color chart and the slider are both drawn.
7406 * @type CanvasRenderingContext2D
7408 this.context2d = false;
7411 * Target input hooks. This object must hold two methods:
7414 * <li><code>show()</code> which is invoked by this extension when the Color
7415 * Mixer panel shows up on screen.
7417 * <li><code>hide()</code> which is invoked when the Color Mixer panel is
7418 * hidden from the screen.
7421 * <p>The object must also hold information about the associated configuration
7422 * property: <var>configProperty</var>, <var>configGroup</var> and
7423 * <var>configGroupRef</var>.
7427 this.targetInput = null;
7430 * Holds the current color in several formats: RGB, HEX, HSV, CIE Lab, and
7431 * CMYK. Except for 'hex', all the values should be from 0 to 1.
7462 * Holds references to all the DOM input fields, for each color channel.
7490 * The "absolute maximum" value is determined based on the min/max values.
7491 * For min -100 and max 100, the abs_max is 200.
7497 // The hue spectrum used by the HSV charts.
7499 [255, 0, 0], // 0, Red, 0°
7500 [255, 255, 0], // 1, Yellow, 60°
7501 [ 0, 255, 0], // 2, Green, 120°
7502 [ 0, 255, 255], // 3, Cyan, 180°
7503 [ 0, 0, 255], // 4, Blue, 240°
7504 [255, 0, 255], // 5, Magenta, 300°
7505 [255, 0, 0] // 6, Red, 360°
7508 // The active color key (input) determines how the color chart works.
7509 this.ckey_active = 'red';
7511 // Given a group of the inputs: red, green and blue, when one of them is active, the ckey_adjoint is set to an array of the other two input IDs.
7512 this.ckey_adjoint = false;
7513 this.ckey_active_group = false;
7515 this.ckey_grouping = {
7534 // These values are automatically calculated when the color mixer is
7537 this.sliderWidth = 0;
7538 this.sliderHeight = 0;
7539 this.sliderSpacing = 0;
7540 this.chartWidth = 0;
7541 this.chartHeight = 0;
7544 * Register the Color Mixer extension.
7546 * @returns {Boolean} True if the extension can be registered properly, or
7549 this.extensionRegister = function (ev) {
7550 if (!gui.elems || !gui.elems.colormixer_canvas || !gui.floatingPanels ||
7551 !gui.floatingPanels.colormixer || !gui.tabPanels ||
7552 !gui.tabPanels.colormixer_inputs || !gui.tabPanels.colormixer_selector
7553 || !_self.init_lab()) {
7557 _self.panel = gui.floatingPanels.colormixer;
7558 _self.panelSelector = gui.tabPanels.colormixer_selector;
7559 _self.panelInputs = gui.tabPanels.colormixer_inputs;
7561 // Initialize the color mixer Canvas element.
7562 _self.context2d = gui.elems.colormixer_canvas.getContext('2d');
7563 if (!_self.context2d) {
7567 // Setup the color mixer inputs.
7568 var elem, label, labelElem,
7569 inputValues = config.inputValues,
7570 form = _self.panelInputs.container;
7575 for (var i in _self.inputs) {
7576 elem = form.elements.namedItem('ckey_' + i) || gui.inputs['ckey_' + i];
7581 if (i === 'hex' || i === 'alpha') {
7582 label = lang.inputs[i];
7584 label = lang.inputs[_self.ckey_grouping[i] + '_' + i];
7587 labelElem = elem.parentNode;
7588 labelElem.replaceChild(doc.createTextNode(label), labelElem.firstChild);
7590 elem.addEventListener('input', _self.ev_input_change, false);
7591 elem.addEventListener('change', _self.ev_input_change, false);
7594 elem.setAttribute('step', inputValues[i][2]);
7596 elem.setAttribute('max', MathRound(inputValues[i][1]));
7597 elem.setAttribute('min', MathRound(inputValues[i][0]));
7598 _self.abs_max[i] = inputValues[i][1] - inputValues[i][0];
7601 // Store the color key, which is used by the event handler.
7603 _self.inputs[i] = elem;
7606 // Setup the ckey inputs of type=radio.
7607 var ckey = form.ckey;
7611 for (var i = 0, n = ckey.length; i < n; i++) {
7613 if (_self.ckey_grouping[elem.value] === 'lab' &&
7614 !_self.context2d.putImageData) {
7615 elem.disabled = true;
7619 elem.addEventListener('change', _self.ev_change_ckey_active, false);
7621 if (elem.value === _self.ckey_active) {
7622 elem.checked = true;
7623 _self.update_ckey_active(_self.ckey_active, true);
7627 // Prepare the color preview elements.
7628 _self.elems.colorActive = gui.elems.colormixer_colorActive.firstChild;
7629 _self.elems.colorOld = gui.elems.colormixer_colorOld.firstChild;
7630 _self.elems.colorOld.addEventListener('click', _self.ev_click_color, false);
7632 // Make sure the buttons work properly.
7633 var anchor, btn, buttons = ['accept', 'cancel', 'saveColor', 'pickColor'];
7634 for (var i = 0, n = buttons.length; i < n; i++) {
7635 btn = gui.elems['colormixer_btn_' + buttons[i]];
7640 anchor = doc.createElement('a');
7642 anchor.appendChild(doc.createTextNode(lang.buttons[buttons[i]]));
7643 anchor.addEventListener('click', _self['ev_click_' + buttons[i]], false);
7645 btn.replaceChild(anchor, btn.firstChild);
7648 // Prepare the canvas "controls" (the chart "dot" and the slider).
7649 var id, elems = ['controls', 'chartDot', 'slider'];
7650 for (var i = 0, n = elems.length; i < n; i++) {
7652 elem = gui.elems['colormixer_' + id];
7657 elem.addEventListener('mousedown', _self.ev_canvas, false);
7658 elem.addEventListener('mousemove', _self.ev_canvas, false);
7659 elem.addEventListener('mouseup', _self.ev_canvas, false);
7661 _self.elems[id] = elem;
7664 // The color palette <select>.
7665 _self.elems.cpaletteInput = gui.inputs.colormixer_cpaletteInput;
7666 _self.elems.cpaletteInput.addEventListener('change',
7667 _self.ev_change_cpalette, false);
7669 // Add the list of color palettes into the <select>.
7671 for (var i in config.colorPalettes) {
7672 palette = config.colorPalettes[i];
7673 elem = doc.createElement('option');
7675 if (i === config.paletteDefault) {
7676 elem.selected = true;
7679 elem.appendChild( doc.createTextNode(lang.colorPalettes[i]) );
7680 _self.elems.cpaletteInput.appendChild(elem);
7683 // This is the ordered list where we add each color (list item).
7684 _self.elems.cpaletteOutput = gui.elems.colormixer_cpaletteOutput;
7685 _self.elems.cpaletteOutput.addEventListener('click', _self.ev_click_color,
7688 _self.cpalette_load(config.paletteDefault);
7690 // Make sure the Canvas element scale is in sync with the application.
7691 app.events.add('canvasSizeChange', _self.update_dimensions);
7693 _self.panelSelector.events.add('guiTabActivate', _self.ev_tabActivate);
7695 // Make sure the Color Mixer is properly closed when the floating panel is
7697 _self.panel.events.add('guiFloatingPanelStateChange',
7698 _self.ev_panel_stateChange);
7704 * This function calculates lots of values used by the other CIE Lab-related
7708 * @returns {Boolean} True if the initialization was successful, or false if
7711 this.init_lab = function () {
7712 var cfg = config.lab;
7717 // Chromaticity coordinates for the RGB primaries.
7725 // The reference white point (xyY to XYZ).
7726 w_x = cfg.ref_x / cfg.ref_y,
7728 w_z = (1 - cfg.ref_x - cfg.ref_y) / cfg.ref_y;
7734 // Again, xyY to XYZ for each RGB primary. Y=1.
7735 var x_r = x0_r / y0_r,
7737 z_r = (1 - x0_r - y0_r) / y0_r,
7740 z_g = (1 - x0_g - y0_g) / y0_g,
7743 z_b = (1 - x0_b - y0_b) / y0_b,
7747 m_i = _self.calc_m3inv(m),
7748 s = _self.calc_m1x3([w_x, w_y, w_z], m_i);
7750 // The 3x3 matrix used by rgb2xyz().
7751 m = [s[0] * m[0], s[0] * m[1], s[0] * m[2],
7752 s[1] * m[3], s[1] * m[4], s[1] * m[5],
7753 s[2] * m[6], s[2] * m[7], s[2] * m[8]];
7755 // The matrix inverse, used by xyz2rgb();
7756 cfg.m_i = _self.calc_m3inv(m);
7759 // Now determine the min/max values for a and b.
7761 var xyz = _self.rgb2xyz([0, 1, 0]), // green gives the minimum value for a
7762 lab = _self.xyz2lab(xyz),
7763 values = config.inputValues;
7764 values.cie_a[0] = lab[1];
7766 xyz = _self.rgb2xyz([1, 0, 1]); // magenta gives the maximum value for a
7767 lab = _self.xyz2lab(xyz);
7768 values.cie_a[1] = lab[1];
7770 xyz = _self.rgb2xyz([0, 0, 1]); // blue gives the minimum value for b
7771 lab = _self.xyz2lab(xyz);
7772 values.cie_b[0] = lab[2];
7774 xyz = _self.rgb2xyz([1, 1, 0]); // yellow gives the maximum value for b
7775 lab = _self.xyz2lab(xyz);
7776 values.cie_b[1] = lab[2];
7782 * The <code>guiTabActivate</code> event handler for the tab panel which holds
7783 * the color mixer and the color palettes. When switching back to the color
7784 * mixer, this method updates the Canvas.
7787 * @param {pwlib.appEvent.guiTabActivate} ev The application event object.
7789 this.ev_tabActivate = function (ev) {
7790 if (ev.tabId === 'mixer' && _self.update_canvas_needed) {
7791 _self.update_canvas(null, true);
7796 * The <code>click</code> event handler for the Accept button. This method
7797 * dispatches the {@link pwlib.appEvent.configChange} application event for
7798 * the configuration property associated to the target input, and hides the
7799 * Color Mixer floating panel.
7802 * @param {Event} ev The DOM Event object.
7804 this.ev_click_accept = function (ev) {
7805 ev.preventDefault();
7807 var configProperty = _self.targetInput.configProperty,
7808 configGroup = _self.targetInput.configGroup,
7809 configGroupRef = _self.targetInput.configGroupRef,
7810 prevVal = configGroupRef[configProperty],
7811 newVal = 'rgba(' + MathRound(_self.color.red * 255) + ',' +
7812 MathRound(_self.color.green * 255) + ',' +
7813 MathRound(_self.color.blue * 255) + ',' +
7814 _self.color.alpha + ')';
7818 if (prevVal !== newVal) {
7819 configGroupRef[configProperty] = newVal;
7820 app.events.dispatch(new pwlib.appEvent.configChange(newVal, prevVal,
7821 configProperty, configGroup, configGroupRef));
7826 * The <code>click</code> event handler for the Cancel button. This method
7827 * hides the Color Mixer floating panel.
7830 * @param {Event} ev The DOM Event object.
7832 this.ev_click_cancel = function (ev) {
7833 ev.preventDefault();
7838 * The <code>click</code> event handler for the "Save color" button. This
7839 * method adds the current color into the "_saved" color palette.
7842 * @param {Event} ev The DOM Event object.
7844 // TODO: provide a way to save the color palette permanently. This should use
7845 // some application event.
7846 this.ev_click_saveColor = function (ev) {
7847 ev.preventDefault();
7849 var color = [_self.color.red, _self.color.green, _self.color.blue],
7850 saved = config.colorPalettes._saved;
7852 saved.colors.push(color);
7854 _self.elems.cpaletteInput.value = '_saved';
7855 _self.cpalette_load('_saved');
7856 _self.panelSelector.tabActivate('cpalettes');
7862 * The <code>click</code> event handler for the "Pick color" button. This
7863 * method activates the color picker tool.
7866 * @param {Event} ev The DOM Event object.
7868 this.ev_click_pickColor = function (ev) {
7869 ev.preventDefault();
7870 app.toolActivate('cpicker', ev);
7874 * The <code>change</code> event handler for the color palette input element.
7875 * This loads the color palette the user selected.
7878 * @param {Event} ev The DOM Event object.
7880 this.ev_change_cpalette = function (ev) {
7881 _self.cpalette_load(this.value);
7885 * Load a color palette. Loading is performed asynchronously.
7887 * @param {String} id The color palette ID.
7889 * @returns {Boolean} True if the load was successful, or false if not.
7891 this.cpalette_load = function (id) {
7892 if (!id || !(id in config.colorPalettes)) {
7896 var palette = config.colorPalettes[id];
7899 pwlib.xhrLoad(PaintWeb.baseFolder + palette.file, this.cpalette_loaded);
7903 } else if (palette.colors) {
7904 return this.cpalette_show(palette.colors);
7912 * The <code>onreadystatechange</code> event handler for the color palette
7913 * XMLHttpRequest object.
7916 * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
7918 this.cpalette_loaded = function (xhr) {
7919 if (!xhr || xhr.readyState !== 4) {
7923 if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
7924 alert(lang.failedColorPaletteLoad);
7928 var colors = JSON.parse(xhr.responseText);
7930 _self.cpalette_show(colors);
7934 * Show a color palette. This method adds all the colors in the DOM as
7935 * individual anchor elements which users can click on.
7939 * @param {Array} colors The array which holds each color in the palette.
7941 * @returns {Boolean} True if the operation was successful, or false if not.
7943 this.cpalette_show = function (colors) {
7944 if (!colors || !(colors instanceof Array)) {
7948 var color, anchor, rgbValue,
7949 frag = doc.createDocumentFragment(),
7950 dest = this.elems.cpaletteOutput;
7952 dest.style.display = 'none';
7953 while (dest.hasChildNodes()) {
7954 dest.removeChild(dest.firstChild);
7957 for (var i = 0, n = colors.length; i < n; i++) {
7960 // Do not allow values higher than 1.
7961 color[0] = MathMin(1, color[0]);
7962 color[1] = MathMin(1, color[1]);
7963 color[2] = MathMin(1, color[2]);
7965 rgbValue = 'rgb(' + MathRound(color[0] * 255) + ',' +
7966 MathRound(color[1] * 255) + ',' +
7967 MathRound(color[2] * 255) + ')';
7969 anchor = doc.createElement('a');
7971 anchor._color = color;
7972 anchor.style.backgroundColor = rgbValue;
7973 anchor.appendChild(doc.createTextNode(rgbValue));
7975 frag.appendChild(anchor);
7978 dest.appendChild(frag);
7979 dest.style.display = 'block';
7981 colors = frag = null;
7987 * The <code>click</code> event handler for colors in the color palette list.
7988 * This event handler is also used for the "old color" element. This method
7989 * updates the color mixer to use the color the user picked.
7992 * @param {Event} ev The DOM Event object.
7994 this.ev_click_color = function (ev) {
7995 var color = ev.target._color;
8000 ev.preventDefault();
8002 _self.color.red = color[0];
8003 _self.color.green = color[1];
8004 _self.color.blue = color[2];
8006 if (typeof(color[3]) !== 'undefined') {
8007 _self.color.alpha = color[3];
8010 _self.update_color('rgb');
8014 * Recalculate the dimensions and coordinates for the slider and for the color
8015 * space visualisation within the Canvas element.
8017 * <p>This method is an event handler for the {@link
8018 * pwlib.appEvent.canvasSizeChange} application event.
8022 this.update_dimensions = function () {
8023 if (resScale === app.resolution.scale) {
8027 resScale = app.resolution.scale;
8029 var canvas = _self.context2d.canvas,
8030 width = canvas.width,
8031 height = canvas.height,
8032 sWidth = width / resScale,
8033 sHeight = height / resScale,
8036 _self.sliderWidth = MathRound(width * config.sliderWidth);
8037 _self.sliderHeight = height - 1;
8038 _self.sliderSpacing = MathRound(width * config.sliderSpacing);
8039 _self.sliderX = width - _self.sliderWidth - 2;
8040 _self.chartWidth = _self.sliderX - _self.sliderSpacing;
8041 _self.chartHeight = height;
8043 style = _self.elems.controls.style;
8044 style.width = sWidth + 'px';
8045 style.height = sHeight + 'px';
8047 style = _self.elems.slider.style;
8048 style.width = (_self.sliderWidth / resScale) + 'px';
8049 style.left = (_self.sliderX / resScale) + 'px';
8051 style = canvas.style;
8052 style.width = sWidth + 'px';
8053 style.height = sHeight + 'px';
8055 if (_self.panel.state !== _self.panel.STATE_HIDDEN) {
8056 _self.update_canvas();
8061 * Calculate the product of two matrices.
8063 * <p>Matrices are one-dimensional arrays of the form <code>[a0, a1, a2, ...,
8064 * b0, b1, b2, ...]</code> where each element from the matrix is given in
8065 * order, from the left to the right, row by row from the top to the bottom.
8067 * @param {Array} a The first matrix must be one row and three columns.
8068 * @param {Array} b The second matrix must be three rows and three columns.
8070 * @returns {Array} The matrix product, one row and three columns.
8072 // Note: for obvious reasons, this method is not a full-fledged matrix product
8073 // calculator. It's as simple as possible - fitting only the very specific
8074 // needs of the color mixer.
8075 this.calc_m1x3 = function (a, b) {
8076 if (!(a instanceof Array) || !(b instanceof Array)) {
8080 a[0] * b[0] + a[1] * b[3] + a[2] * b[6], // x
8081 a[0] * b[1] + a[1] * b[4] + a[2] * b[7], // y
8082 a[0] * b[2] + a[1] * b[5] + a[2] * b[8] // z
8088 * Calculate the matrix inverse.
8090 * <p>Matrices are one-dimensional arrays of the form <code>[a0, a1, a2, ...,
8091 * b0, b1, b2, ...]</code> where each element from the matrix is given in
8092 * order, from the left to the right, row by row from the top to the bottom.
8096 * @param {Array} m The square matrix which must have three rows and three
8099 * @returns {Array|false} The computed matrix inverse, or false if the matrix
8100 * determinant was 0 - the given matrix is not invertible.
8102 // Note: for obvious reasons, this method is not a full-fledged matrix inverse
8103 // calculator. It's as simple as possible - fitting only the very specific
8104 // needs of the color mixer.
8105 this.calc_m3inv = function (m) {
8106 if (!(m instanceof Array)) {
8110 var d = (m[0]*m[4]*m[8] + m[1]*m[5]*m[6] + m[2]*m[3]*m[7]) -
8111 (m[2]*m[4]*m[6] + m[5]*m[7]*m[0] + m[8]*m[1]*m[3]);
8113 // Matrix determinant is 0: the matrix is not invertible.
8119 m[4]*m[8] - m[5]*m[7], -m[3]*m[8] + m[5]*m[6], m[3]*m[7] - m[4]*m[6],
8120 -m[1]*m[8] + m[2]*m[7], m[0]*m[8] - m[2]*m[6], -m[0]*m[7] + m[1]*m[6],
8121 m[1]*m[5] - m[2]*m[4], -m[0]*m[5] + m[2]*m[3], m[0]*m[4] - m[1]*m[3]
8124 i = [1/d * i[0], 1/d * i[3], 1/d * i[6],
8125 1/d * i[1], 1/d * i[4], 1/d * i[7],
8126 1/d * i[2], 1/d * i[5], 1/d * i[8]];
8132 * The <code>change</code> event handler for the Color Mixer inputs of
8133 * type=radio. This method allows users to change the active color key - used
8134 * for the color space visualisation.
8137 this.ev_change_ckey_active = function () {
8138 if (this.value && this.value !== _self.ckey_active) {
8139 _self.update_ckey_active(this.value);
8144 * Update the active color key. This method updates the Canvas accordingly.
8148 * @param {String} ckey The color key you want to be active.
8149 * @param {Boolean} [only_vars] Tells if you want only the variables to be
8150 * updated - no Canvas updates. This is true only during the Color Mixer
8153 * @return {Boolean} True if the operation was successful, or false if not.
8155 this.update_ckey_active = function (ckey, only_vars) {
8156 if (!_self.inputs[ckey]) {
8160 _self.ckey_active = ckey;
8162 var adjoint = [], group = _self.ckey_grouping[ckey];
8164 // Determine the adjoint color keys. For example, if red is active, then adjoint = ['green', 'blue'].
8165 for (var i in _self.ckey_grouping) {
8166 if (_self.ckey_grouping[i] === group && i !== ckey) {
8171 _self.ckey_active_group = group;
8172 _self.ckey_adjoint = adjoint;
8175 if (_self.panelSelector.tabId !== 'mixer') {
8176 _self.update_canvas_needed = true;
8177 _self.panelSelector.tabActivate('mixer');
8179 _self.update_canvas();
8182 if (_self.panelInputs.tabId !== group) {
8183 _self.panelInputs.tabActivate(group);
8191 * Show the Color Mixer.
8193 * @param {Object} target The target input object.
8195 * @param {Object} color The color you want to set before the Color Mixer is
8196 * shown. The object must have four properties: <var>red</var>,
8197 * <var>green</var>, <var>blue</var> and <var>alpha</var>. All the values must
8198 * be between 0 and 1. This color becomes the "active color" and the "old
8201 * @see this.targetInput for more information about the <var>target</var>
8204 this.show = function (target, color) {
8205 var styleActive = _self.elems.colorActive.style,
8206 colorOld = _self.elems.colorOld,
8207 styleOld = colorOld.style;
8210 if (_self.targetInput) {
8211 _self.targetInput.hide();
8214 _self.targetInput = target;
8215 _self.targetInput.show();
8219 _self.color.red = color.red;
8220 _self.color.green = color.green;
8221 _self.color.blue = color.blue;
8222 _self.color.alpha = color.alpha;
8224 _self.update_color('rgb');
8226 styleOld.backgroundColor = styleActive.backgroundColor;
8227 styleOld.opacity = styleActive.opacity;
8228 colorOld._color = [color.red, color.green, color.blue, color.alpha];
8235 * Hide the Color Mixer floating panel. This method invokes the
8236 * <code>hide()</code> method provided by the target input.
8238 this.hide = function () {
8240 _self.ev_canvas_mode = false;
8244 * The <code>guiFloatingPanelStateChange</code> event handler for the Color
8245 * Mixer panel. This method ensures the Color Mixer is properly closed.
8247 * @param {pwlib.appEvent.guiFloatingPanelStateChange} ev The application
8250 this.ev_panel_stateChange = function (ev) {
8251 if (ev.state === ev.STATE_HIDDEN) {
8252 if (_self.targetInput) {
8253 _self.targetInput.hide();
8254 _self.targetInput = null;
8256 _self.ev_canvas_mode = false;
8261 * The <code>input</code> and <code>change</code> event handler for all the
8262 * Color Mixer inputs.
8265 this.ev_input_change = function () {
8270 // Validate and restrict the possible values.
8271 // If the input is unchanged, or if the new value is invalid, the function
8273 // The hexadecimal input is checked with a simple regular expression.
8275 if ((this._ckey === 'hex' && !/^\#[a-f0-9]{6}$/i.test(this.value))) {
8279 if (this.getAttribute('type') === 'number') {
8280 var val = parseInt(this.value),
8281 min = this.getAttribute('min'),
8282 max = this.getAttribute('max');
8290 } else if (val > max) {
8294 if (val != this.value) {
8299 // Update the internal color value.
8300 if (this._ckey === 'hex') {
8301 _self.color[this._ckey] = this.value;
8302 } else if (_self.ckey_grouping[this._ckey] === 'lab') {
8303 _self.color[this._ckey] = parseInt(this.value);
8305 _self.color[this._ckey] = parseInt(this.value)
8306 / config.inputValues[this._ckey][1];
8309 _self.update_color(this._ckey);
8313 * Update the current color. Once a color value is updated, this method is
8314 * called to keep the rest of the color mixer in sync: for example, when a RGB
8315 * value is updated, it needs to be converted to HSV, CMYK and all of the
8316 * other formats. Additionally, this method updates the color preview, the
8317 * controls on the Canvas and the input values.
8319 * <p>You need to call this function whenever you update the color manually.
8321 * @param {String} ckey The color key that was updated.
8323 this.update_color = function (ckey) {
8324 var group = _self.ckey_grouping[ckey] || ckey;
8362 _self.update_preview();
8363 _self.update_inputs();
8365 if (ckey !== 'alpha') {
8366 _self.update_canvas(ckey);
8371 * Update the color preview.
8374 this.update_preview = function () {
8375 var red = MathRound(_self.color.red * 255),
8376 green = MathRound(_self.color.green * 255),
8377 blue = MathRound(_self.color.blue * 255),
8378 style = _self.elems.colorActive.style;
8380 style.backgroundColor = 'rgb(' + red + ',' + green + ',' + blue + ')';
8381 style.opacity = _self.color.alpha;
8385 * Update the color inputs. This method takes the internal color values and
8386 * shows them in the DOM input elements.
8389 this.update_inputs = function () {
8391 for (var i in _self.inputs) {
8392 input = _self.inputs[i];
8393 input._old_value = input.value;
8394 if (input._ckey === 'hex') {
8395 input.value = _self.color[i];
8396 } else if (_self.ckey_grouping[input._ckey] === 'lab') {
8397 input.value = MathRound(_self.color[i]);
8399 input.value = MathRound(_self.color[i] * config.inputValues[i][1]);
8405 * Convert RGB to CMYK. This uses the current color RGB values and updates the
8406 * CMYK values accordingly.
8409 // Quote from Wikipedia:
8410 // "Since RGB and CMYK spaces are both device-dependent spaces, there is no
8411 // simple or general conversion formula that converts between them.
8412 // Conversions are generally done through color management systems, using
8413 // color profiles that describe the spaces being converted. Nevertheless, the
8414 // conversions cannot be exact, since these spaces have very different
8416 // Translation: this is just a simple RGB to CMYK conversion function.
8417 this.rgb2cmyk = function () {
8418 var color = _self.color,
8419 cyan, magenta, yellow, black,
8421 green = color.green,
8425 magenta = 1 - green;
8428 black = MathMin(cyan, magenta, yellow, 1);
8431 cyan = magenta = yellow = 0;
8434 cyan = (cyan - black) / w;
8435 magenta = (magenta - black) / w;
8436 yellow = (yellow - black) / w;
8440 color.magenta = magenta;
8441 color.yellow = yellow;
8442 color.black = black;
8446 * Convert CMYK to RGB (internally).
8449 this.cmyk2rgb = function () {
8450 var color = _self.color,
8451 w = 1 - color.black;
8453 color.red = 1 - color.cyan * w - color.black;
8454 color.green = 1 - color.magenta * w - color.black;
8455 color.blue = 1 - color.yellow * w - color.black;
8459 * Convert RGB to HSV (internally).
8462 this.rgb2hsv = function () {
8463 var hue, sat, val, // HSV
8464 red = _self.color.red,
8465 green = _self.color.green,
8466 blue = _self.color.blue,
8467 min = MathMin(red, green, blue),
8468 max = MathMax(red, green, blue),
8472 // This is gray (red==green==blue)
8479 hue = (green - blue) / delta;
8480 } else if (max === green) {
8481 hue = (blue - red) / delta + 2;
8482 } else if (max === blue) {
8483 hue = (red - green) / delta + 4;
8492 _self.color.hue = hue;
8493 _self.color.sat = sat;
8494 _self.color.val = val;
8498 * Convert HSV to RGB.
8502 * @param {Boolean} [no_update] Tells the function to not update the internal
8504 * @param {Array} [hsv] The array holding the HSV values you want to convert
8505 * to RGB. This array must have three elements ordered as: <var>hue</var>,
8506 * <var>saturation</var> and <var>value</var> - all between 0 and 1. If you do
8507 * not provide the array, then the internal HSV color values are used.
8509 * @returns {Array} The RGB values converted from HSV. The array has three
8510 * elements ordered as: <var>red</var>, <var>green</var> and <var>blue</var>
8511 * - all with values between 0 and 1.
8513 this.hsv2rgb = function (no_update, hsv) {
8514 var color = _self.color,
8515 red, green, blue, hue, sat, val;
8517 // Use custom HSV values or the current color.
8528 // achromatic (grey)
8530 red = green = blue = val;
8533 var i = MathFloor(h);
8534 var t1 = val * ( 1 - sat ),
8535 t2 = val * ( 1 - sat * ( h - i ) ),
8536 t3 = val * ( 1 - sat * ( 1 - (h - i) ) );
8538 if (i === 0 || i === 6) { // 0° Red
8539 red = val; green = t3; blue = t1;
8540 } else if (i === 1) { // 60° Yellow
8541 red = t2; green = val; blue = t1;
8542 } else if (i === 2) { // 120° Green
8543 red = t1; green = val; blue = t3;
8544 } else if (i === 3) { // 180° Cyan
8545 red = t1; green = t2; blue = val;
8546 } else if (i === 4) { // 240° Blue
8547 red = t3; green = t1; blue = val;
8548 } else if (i === 5) { // 300° Magenta
8549 red = val; green = t1; blue = t2;
8555 color.green = green;
8559 return [red, green, blue];
8563 * Convert RGB to hexadecimal representation (internally).
8566 this.rgb2hex = function () {
8567 var hex = '#', rgb = ['red', 'green', 'blue'], i, val,
8568 color = _self.color;
8570 for (i = 0; i < 3; i++) {
8571 val = MathRound(color[rgb[i]] * 255).toString(16);
8572 if (val.length === 1) {
8582 * Convert the hexadecimal representation of color to RGB values (internally).
8585 this.hex2rgb = function () {
8586 var rgb = ['red', 'green', 'blue'], i, val,
8587 color = _self.color,
8590 hex = hex.substr(1);
8591 if (hex.length !== 6) {
8595 for (i = 0; i < 3; i++) {
8596 val = hex.substr(i*2, 2);
8597 color[rgb[i]] = parseInt(val, 16)/255;
8602 * Convert RGB to CIE Lab (internally).
8605 this.rgb2lab = function () {
8606 var color = _self.color,
8607 lab = _self.xyz2lab(_self.rgb2xyz([color.red, color.green,
8610 color.cie_l = lab[0];
8611 color.cie_a = lab[1];
8612 color.cie_b = lab[2];
8616 * Convert CIE Lab values to RGB values (internally).
8619 this.lab2rgb = function () {
8620 var color = _self.color,
8621 rgb = _self.xyz2rgb(_self.lab2xyz(color.cie_l, color.cie_a,
8625 color.green = rgb[1];
8626 color.blue = rgb[2];
8630 * Convert XYZ color values into CIE Lab values.
8634 * @param {Array} xyz The array holding the XYZ color values in order:
8635 * <var>X</var>, <var>Y</var> and <var>Z</var>.
8637 * @returns {Array} An array holding the CIE Lab values in order:
8638 * <var>L</var>, <var>a</var> and <var>b</var>.
8640 this.xyz2lab = function (xyz) {
8641 var cfg = config.lab,
8643 // 216/24389 or (6/29)^3 (both = 0.008856...)
8654 xyz[0] = MathPow(xyz[0], 1/3);
8656 xyz[0] = (k*xyz[0] + 16)/116;
8660 xyz[1] = MathPow(xyz[1], 1/3);
8662 xyz[1] = (k*xyz[1] + 16)/116;
8666 xyz[2] = MathPow(xyz[2], 1/3);
8668 xyz[2] = (k*xyz[2] + 16)/116;
8671 var cie_l = 116 * xyz[1] - 16,
8672 cie_a = 500 * (xyz[0] - xyz[1]),
8673 cie_b = 200 * (xyz[1] - xyz[2]);
8675 return [cie_l, cie_a, cie_b];
8679 * Convert CIE Lab values to XYZ color values.
8683 * @param {Number} cie_l The color lightness value.
8684 * @param {Number} cie_a The a* color opponent.
8685 * @param {Number} cie_b The b* color opponent.
8687 * @returns {Array} An array holding the XYZ color values in order:
8688 * <var>X</var>, <var>Y</var> and <var>Z</var>.
8690 this.lab2xyz = function (cie_l, cie_a, cie_b) {
8691 var y = (cie_l + 16) / 116,
8692 x = y + cie_a / 500,
8693 z = y - cie_b / 200,
8699 k = 1/3 * MathPow(29/6, 2),
8731 * Convert XYZ color values to RGB.
8735 * @param {Array} xyz The array holding the XYZ color values in order:
8736 * <var>X</var>, <var>Y</var> and <var>Z</var>
8738 * @returns {Array} An array holding the RGB values in order: <var>red</var>,
8739 * <var>green</var> and <var>blue</var>.
8741 this.xyz2rgb = function (xyz) {
8742 var rgb = _self.calc_m1x3(xyz, config.lab.m_i);
8744 if (rgb[0] > 0.0031308) {
8745 rgb[0] = 1.055 * MathPow(rgb[0], 1 / 2.4) - 0.055;
8750 if (rgb[1] > 0.0031308) {
8751 rgb[1] = 1.055 * MathPow(rgb[1], 1 / 2.4) - 0.055;
8756 if (rgb[2] > 0.0031308) {
8757 rgb[2] = 1.055 * MathPow(rgb[2], 1 / 2.4) - 0.055;
8764 } else if (rgb[0] > 1) {
8770 } else if (rgb[1] > 1) {
8776 } else if (rgb[2] > 1) {
8784 * Convert RGB values to XYZ color values.
8788 * @param {Array} rgb The array holding the RGB values in order:
8789 * <var>red</var>, <var>green</var> and <var>blue</var>.
8791 * @returns {Array} An array holding the XYZ color values in order:
8792 * <var>X</var>, <var>Y</var> and <var>Z</var>.
8794 this.rgb2xyz = function (rgb) {
8795 if (rgb[0] > 0.04045) {
8796 rgb[0] = MathPow(( rgb[0] + 0.055 ) / 1.055, 2.4);
8801 if (rgb[1] > 0.04045) {
8802 rgb[1] = MathPow(( rgb[1] + 0.055 ) / 1.055, 2.4);
8807 if (rgb[2] > 0.04045) {
8808 rgb[2] = MathPow(( rgb[2] + 0.055 ) / 1.055, 2.4);
8813 return _self.calc_m1x3(rgb, config.lab.m);
8817 * Update the color space visualisation. This method updates the color chart
8818 * and/or the color slider, and the associated controls, each as needed when
8819 * a color key is updated.
8823 * @param {String} updated_ckey The color key that was updated.
8824 * @param {Boolean} [force=false] Tells the function to force an update. The
8825 * Canvas is not updated when the color mixer panel is not visible.
8827 * @returns {Boolean} If the operation was successful, or false if not.
8829 this.update_canvas = function (updated_ckey, force) {
8830 if (_self.panelSelector.tabId !== 'mixer' && !force) {
8831 _self.update_canvas_needed = true;
8835 _self.update_canvas_needed = false;
8837 var slider = _self.elems.slider.style,
8838 chart = _self.elems.chartDot.style,
8839 color = _self.color,
8840 ckey = _self.ckey_active,
8841 group = _self.ckey_active_group,
8842 adjoint = _self.ckey_adjoint,
8843 width = _self.chartWidth / resScale,
8844 height = _self.chartHeight / resScale,
8847 // Update the slider which shows the position of the active ckey.
8848 if (updated_ckey !== adjoint[0] && updated_ckey !== adjoint[1] &&
8849 _self.ev_canvas_mode !== 'chart') {
8850 if (group === 'lab') {
8851 sy = (color[ckey] - config.inputValues[ckey][0]) / _self.abs_max[ckey];
8856 if (ckey !== 'hue' && group !== 'lab') {
8860 slider.top = MathRound(sy * height) + 'px';
8863 // Update the chart dot.
8864 if (updated_ckey !== ckey) {
8865 if (group === 'lab') {
8866 mx = (color[adjoint[0]] - config.inputValues[adjoint[0]][0])
8867 / _self.abs_max[adjoint[0]];
8868 my = (color[adjoint[1]] - config.inputValues[adjoint[1]][0])
8869 / _self.abs_max[adjoint[1]];
8871 mx = color[adjoint[0]];
8872 my = 1 - color[adjoint[1]];
8875 chart.top = MathRound(my * height) + 'px';
8876 chart.left = MathRound(mx * width) + 'px';
8879 if (!_self.draw_chart(updated_ckey) || !_self.draw_slider(updated_ckey)) {
8887 * The mouse events handler for the Canvas controls. This method determines
8888 * the region the user is using, and it also updates the color values for the
8889 * active color key. The Canvas and all the inputs in the color mixer are
8890 * updated as needed.
8893 * @param {Event} ev The DOM Event object.
8895 this.ev_canvas = function (ev) {
8896 ev.preventDefault();
8898 // Initialize color picking only on mousedown.
8899 if (ev.type === 'mousedown' && !_self.ev_canvas_mode) {
8900 _self.ev_canvas_mode = true;
8901 doc.addEventListener('mouseup', _self.ev_canvas, false);
8904 if (!_self.ev_canvas_mode) {
8908 // The mouseup event stops the effect of any further mousemove events.
8909 if (ev.type === 'mouseup') {
8910 _self.ev_canvas_mode = false;
8911 doc.removeEventListener('mouseup', _self.ev_canvas, false);
8914 var elems = _self.elems;
8916 // If the user is on top of the 'controls' element, determine the mouse coordinates and the 'mode' for this function: the user is either working with the slider, or he/she is working with the color chart itself.
8917 if (ev.target === elems.controls) {
8919 width = _self.context2d.canvas.width,
8920 height = _self.context2d.canvas.height;
8922 // Get the mouse position, relative to the event target.
8923 if (ev.layerX || ev.layerX === 0) { // Firefox
8924 mx = ev.layerX * resScale;
8925 my = ev.layerY * resScale;
8926 } else if (ev.offsetX || ev.offsetX === 0) { // Opera
8927 mx = ev.offsetX * resScale;
8928 my = ev.offsetY * resScale;
8931 if (mx >= 0 && mx <= _self.chartWidth) {
8933 } else if (mx >= _self.sliderX && mx <= width) {
8937 // The user might have clicked on the chart dot, or on the slider graphic
8939 // If yes, then determine the mode based on this.
8940 if (ev.target === elems.chartDot) {
8942 } else if (ev.target === elems.slider) {
8947 // Update the ev_canvas_mode value to include the mode name, if it's simply
8948 // the true boolean.
8949 // This ensures that the continuous mouse movements do not go from one mode
8950 // to another when the user moves out from the slider to the chart (and
8952 if (mode && _self.ev_canvas_mode === true) {
8953 _self.ev_canvas_mode = mode;
8956 // Do not continue if the mode wasn't determined (the mouse is not on the
8957 // slider, nor on the chart).
8958 // Also don't continue if the mouse is not in the same place (different
8960 if (!mode || _self.ev_canvas_mode !== mode || ev.target !== elems.controls)
8965 var color = _self.color,
8966 val_x = mx / _self.chartWidth,
8967 val_y = my / height;
8969 if (mode === 'slider') {
8970 if (_self.ckey_active === 'hue') {
8971 color[_self.ckey_active] = val_y;
8972 } else if (_self.ckey_active_group === 'lab') {
8973 color[_self.ckey_active] = _self.abs_max[_self.ckey_active] * val_y
8974 + config.inputValues[_self.ckey_active][0];
8976 color[_self.ckey_active] = 1 - val_y;
8979 return _self.update_color(_self.ckey_active);
8981 } else if (mode === 'chart') {
8986 if (_self.ckey_active_group === 'lab') {
8987 val_x = _self.abs_max[_self.ckey_adjoint[0]] * val_x
8988 + config.inputValues[_self.ckey_adjoint[0]][0];
8989 val_y = _self.abs_max[_self.ckey_adjoint[1]] * val_y
8990 + config.inputValues[_self.ckey_adjoint[1]][0];
8995 color[_self.ckey_adjoint[0]] = val_x;
8996 color[_self.ckey_adjoint[1]] = val_y;
8998 return _self.update_color(_self.ckey_active_group);
9005 * Draw the color space visualisation.
9009 * @param {String} updated_ckey The color key that was updated. This is used
9010 * to determine if the Canvas needs to be updated or not.
9012 this.draw_chart = function (updated_ckey) {
9013 var context = _self.context2d,
9014 gradient, color, opacity, i;
9016 if (updated_ckey === _self.ckey_adjoint[0] || updated_ckey ===
9017 _self.ckey_adjoint[1] || (_self.ev_canvas_mode === 'chart' &&
9018 updated_ckey === _self.ckey_active_group)) {
9022 var w = _self.chartWidth,
9023 h = _self.chartHeight;
9025 context.clearRect(0, 0, w, h);
9027 if (_self.ckey_active === 'sat') {
9028 // In saturation mode the user has the slider which allows him/her to
9029 // change the saturation (hSv) of the current color.
9030 // The chart shows the hue spectrum on the X axis, while the Y axis gives
9033 if (_self.color.sat > 0) {
9034 // Draw the hue spectrum gradient on the X axis.
9035 gradient = context.createLinearGradient(0, 0, w, 0);
9036 for (i = 0; i <= 6; i++) {
9037 color = 'rgb(' + hueSpectrum[i][0] + ', ' +
9038 hueSpectrum[i][1] + ', ' +
9039 hueSpectrum[i][2] + ')';
9040 gradient.addColorStop(i * 1/6, color);
9042 context.fillStyle = gradient;
9043 context.fillRect(0, 0, w, h);
9045 // Draw the gradient which darkens the hue spectrum on the Y axis.
9046 gradient = context.createLinearGradient(0, 0, 0, h);
9047 gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
9048 gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
9049 context.fillStyle = gradient;
9050 context.fillRect(0, 0, w, h);
9053 if (_self.color.sat < 1) {
9054 // Draw the white to black gradient. This is used for creating the
9055 // saturation effect. Lowering the saturation value makes the gradient
9056 // more visible, hence the hue colors desaturate.
9057 opacity = 1 - _self.color.sat;
9058 gradient = context.createLinearGradient(0, 0, 0, h);
9059 gradient.addColorStop(0, 'rgba(255, 255, 255, ' + opacity + ')');
9060 gradient.addColorStop(1, 'rgba( 0, 0, 0, ' + opacity + ')');
9061 context.fillStyle = gradient;
9062 context.fillRect(0, 0, w, h);
9065 } else if (_self.ckey_active === 'val') {
9066 // In value mode the user has the slider which allows him/her to change the value (hsV) of the current color.
9067 // The chart shows the hue spectrum on the X axis, while the Y axis gives the saturation (hSv).
9069 if (_self.color.val > 0) {
9070 // Draw the hue spectrum gradient on the X axis.
9071 gradient = context.createLinearGradient(0, 0, w, 0);
9072 for (i = 0; i <= 6; i++) {
9073 color = 'rgb(' + hueSpectrum[i][0] + ', ' +
9074 hueSpectrum[i][1] + ', ' +
9075 hueSpectrum[i][2] + ')';
9076 gradient.addColorStop(i * 1/6, color);
9078 context.fillStyle = gradient;
9079 context.fillRect(0, 0, w, h);
9081 // Draw the gradient which lightens the hue spectrum on the Y axis.
9082 gradient = context.createLinearGradient(0, 0, 0, h);
9083 gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
9084 gradient.addColorStop(1, 'rgba(255, 255, 255, 1)');
9085 context.fillStyle = gradient;
9086 context.fillRect(0, 0, w, h);
9089 if (_self.color.val < 1) {
9090 // Draw a solid black color on top. This is used for darkening the hue colors gradient when the user reduces the Value (hsV).
9091 context.fillStyle = 'rgba(0, 0, 0, ' + (1 - _self.color.val) +')';
9092 context.fillRect(0, 0, w, h);
9095 } else if (_self.ckey_active === 'hue') {
9096 // In hue mode the user has the slider which allows him/her to change the hue (Hsv) of the current color.
9097 // The chart shows the current color in the background. The X axis gives the saturation (hSv), and the Y axis gives the value (hsV).
9099 if (_self.color.sat === 1 && _self.color.val === 1) {
9100 color = [_self.color.red, _self.color.green, _self.color.blue];
9102 // Determine the RGB values for the current color which has the same hue, but maximum saturation and value (hSV).
9103 color = _self.hsv2rgb(true, [_self.color.hue, 1, 1]);
9105 for (i = 0; i < 3; i++) {
9106 color[i] = MathRound(color[i] * 255);
9109 context.fillStyle = 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')';
9110 context.fillRect(0, 0, w, h);
9112 // Draw the white gradient for saturation (X axis, hSv).
9113 gradient = context.createLinearGradient(0, 0, w, 0);
9114 gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
9115 gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
9116 context.fillStyle = gradient;
9117 context.fillRect(0, 0, w, h);
9119 // Draw the black gradient for value (Y axis, hsV).
9120 gradient = context.createLinearGradient(0, 0, 0, h);
9121 gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
9122 gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
9123 context.fillStyle = gradient;
9124 context.fillRect(0, 0, w, h);
9126 } else if (_self.ckey_active_group === 'rgb') {
9127 // In any red/green/blue mode the background color becomes the one of the ckey_active. Say, for ckey_active=red the background color would be the current red value (green and blue are both set to 0).
9128 // On the X/Y axes the other two colors are shown. E.g. for red the X axis gives the green gradient, and the Y axis gives the blue gradient. The two gradients are drawn on top of the red background using a global composite operation (lighter) - to create the color addition effect.
9131 color = {'red' : 0, 'green' : 0, 'blue' : 0};
9132 color[_self.ckey_active] = MathRound(_self.color[_self.ckey_active]
9135 color2 = {'red' : 0, 'green' : 0, 'blue' : 0};
9136 color2[_self.ckey_adjoint[1]] = 255;
9138 color3 = {'red' : 0, 'green' : 0, 'blue' : 0};
9139 color3[_self.ckey_adjoint[0]] = 255;
9142 context.fillStyle = 'rgb(' + color.red + ',' + color.green + ',' + color.blue + ')';
9143 context.fillRect(0, 0, w, h);
9145 // This doesn't work in Opera 9.2 and older versions.
9146 var op = context.globalCompositeOperation;
9147 context.globalCompositeOperation = 'lighter';
9149 // The Y axis gradient.
9150 gradient = context.createLinearGradient(0, 0, 0, h);
9151 gradient.addColorStop(0, 'rgba(' + color2.red + ',' + color2.green + ',' + color2.blue + ', 1)');
9152 gradient.addColorStop(1, 'rgba(' + color2.red + ',' + color2.green + ',' + color2.blue + ', 0)');
9153 context.fillStyle = gradient;
9154 context.fillRect(0, 0, w, h);
9156 // The X axis gradient.
9157 gradient = context.createLinearGradient(0, 0, w, 0);
9158 gradient.addColorStop(0, 'rgba(' + color3.red + ',' + color3.green + ',' + color3.blue + ', 0)');
9159 gradient.addColorStop(1, 'rgba(' + color3.red + ',' + color3.green + ',' + color3.blue + ', 1)');
9160 context.fillStyle = gradient;
9161 context.fillRect(0, 0, w, h);
9163 context.globalCompositeOperation = op;
9165 } else if (_self.ckey_active_group === 'lab') {
9166 // The chart plots the CIE Lab colors. The non-active color keys give the X/Y axes. For example, if cie_l (lightness) is active, then the cie_a values give the X axis, and the Y axis is given by the values of cie_b.
9167 // The chart is drawn manually, pixel-by-pixel, due to the special way CIE Lab works. This is very slow in today's UAs.
9171 if (context.createImageData) {
9172 imgd = context.createImageData(w, h);
9173 } else if (context.getImageData) {
9174 imgd = context.getImageData(0, 0, w, h);
9179 'data' : new Array(w*h*4)
9183 var pix = imgd.data,
9184 n = imgd.data.length - 1,
9185 i = -1, p = 0, inc_x, inc_y, xyz = [], rgb = [], cie_x, cie_y;
9187 cie_x = _self.ckey_adjoint[0];
9188 cie_y = _self.ckey_adjoint[1];
9191 'cie_l' : _self.color.cie_l,
9192 'cie_a' : _self.color.cie_a,
9193 'cie_b' : _self.color.cie_b
9196 inc_x = _self.abs_max[cie_x] / w;
9197 inc_y = _self.abs_max[cie_y] / h;
9199 color[cie_x] = config.inputValues[cie_x][0];
9200 color[cie_y] = config.inputValues[cie_y][0];
9203 xyz = _self.lab2xyz(color.cie_l, color.cie_a, color.cie_b);
9204 rgb = _self.xyz2rgb(xyz);
9206 pix[++i] = MathRound(rgb[0]*255);
9207 pix[++i] = MathRound(rgb[1]*255);
9208 pix[++i] = MathRound(rgb[2]*255);
9212 color[cie_x] += inc_x;
9214 if ((p % w) === 0) {
9215 color[cie_x] = config.inputValues[cie_x][0];
9216 color[cie_y] += inc_y;
9220 context.putImageData(imgd, 0, 0);
9227 * Draw the color slider on the Canvas element.
9231 * @param {String} updated_ckey The color key that was updated. This is used
9232 * to determine if the Canvas needs to be updated or not.
9234 this.draw_slider = function (updated_ckey) {
9235 if (_self.ckey_active === updated_ckey) {
9239 var context = _self.context2d,
9240 slider_w = _self.sliderWidth,
9241 slider_h = _self.sliderHeight,
9242 slider_x = _self.sliderX,
9246 gradient = context.createLinearGradient(slider_x, slider_y, slider_x, slider_h);
9248 if (_self.ckey_active === 'hue') {
9249 // Draw the hue spectrum gradient.
9250 for (i = 0; i <= 6; i++) {
9251 color = 'rgb(' + hueSpectrum[i][0] + ', ' +
9252 hueSpectrum[i][1] + ', ' +
9253 hueSpectrum[i][2] + ')';
9254 gradient.addColorStop(i * 1/6, color);
9256 context.fillStyle = gradient;
9257 context.fillRect(slider_x, slider_y, slider_w, slider_h);
9259 if (_self.color.sat < 1) {
9260 context.fillStyle = 'rgba(255, 255, 255, ' +
9261 (1 - _self.color.sat) + ')';
9262 context.fillRect(slider_x, slider_y, slider_w, slider_h);
9264 if (_self.color.val < 1) {
9265 context.fillStyle = 'rgba(0, 0, 0, ' + (1 - _self.color.val) + ')';
9266 context.fillRect(slider_x, slider_y, slider_w, slider_h);
9269 } else if (_self.ckey_active === 'sat') {
9270 // Draw the saturation gradient for the slider.
9271 // The start color is the current color with maximum saturation. The bottom gradient color is the same "color" without saturation.
9272 // The slider allows you to desaturate the current color.
9274 // Determine the RGB values for the current color which has the same hue and value (HsV), but maximum saturation (hSv).
9275 if (_self.color.sat === 1) {
9276 color = [_self.color.red, _self.color.green, _self.color.blue];
9278 color = _self.hsv2rgb(true, [_self.color.hue, 1, _self.color.val]);
9281 for (i = 0; i < 3; i++) {
9282 color[i] = MathRound(color[i] * 255);
9285 var gray = MathRound(_self.color.val * 255);
9286 gradient.addColorStop(0, 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')');
9287 gradient.addColorStop(1, 'rgb(' + gray + ', ' + gray + ', ' + gray + ')');
9288 context.fillStyle = gradient;
9289 context.fillRect(slider_x, slider_y, slider_w, slider_h);
9291 } else if (_self.ckey_active === 'val') {
9292 // Determine the RGB values for the current color which has the same hue and saturation, but maximum value (hsV).
9293 if (_self.color.val === 1) {
9294 color = [_self.color.red, _self.color.green, _self.color.blue];
9296 color = _self.hsv2rgb(true, [_self.color.hue, _self.color.sat, 1]);
9299 for (i = 0; i < 3; i++) {
9300 color[i] = MathRound(color[i] * 255);
9303 gradient.addColorStop(0, 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')');
9304 gradient.addColorStop(1, 'rgb(0, 0, 0)');
9305 context.fillStyle = gradient;
9306 context.fillRect(slider_x, slider_y, slider_w, slider_h);
9308 } else if (_self.ckey_active_group === 'rgb') {
9309 var red = MathRound(_self.color.red * 255),
9310 green = MathRound(_self.color.green * 255),
9311 blue = MathRound(_self.color.blue * 255);
9318 color[_self.ckey_active] = 255;
9325 color2[_self.ckey_active] = 0;
9327 gradient.addColorStop(0, 'rgb(' + color.red + ',' + color.green + ',' + color.blue + ')');
9328 gradient.addColorStop(1, 'rgb(' + color2.red + ',' + color2.green + ',' + color2.blue + ')');
9329 context.fillStyle = gradient;
9330 context.fillRect(slider_x, slider_y, slider_w, slider_h);
9332 } else if (_self.ckey_active_group === 'lab') {
9333 // The slider shows a gradient with the current color key going from the minimum to the maximum value. The gradient is calculated pixel by pixel, due to the special way CIE Lab is defined.
9337 if (context.createImageData) {
9338 imgd = context.createImageData(1, slider_h);
9339 } else if (context.getImageData) {
9340 imgd = context.getImageData(0, 0, 1, slider_h);
9344 'height' : slider_h,
9345 'data' : new Array(slider_h*4)
9349 var pix = imgd.data,
9350 n = imgd.data.length - 1,
9351 ckey = _self.ckey_active,
9352 i = -1, inc, xyz, rgb;
9355 'cie_l' : _self.color.cie_l,
9356 'cie_a' : _self.color.cie_a,
9357 'cie_b' : _self.color.cie_b
9360 color[ckey] = config.inputValues[ckey][0];
9361 inc = _self.abs_max[ckey] / slider_h;
9364 xyz = _self.lab2xyz(color.cie_l, color.cie_a, color.cie_b);
9365 rgb = _self.xyz2rgb(xyz);
9366 pix[++i] = MathRound(rgb[0]*255);
9367 pix[++i] = MathRound(rgb[1]*255);
9368 pix[++i] = MathRound(rgb[2]*255);
9374 for (i = 0; i <= slider_w; i++) {
9375 context.putImageData(imgd, slider_x+i, slider_y);
9379 context.strokeStyle = '#6d6d6d';
9380 context.strokeRect(slider_x, slider_y, slider_w, slider_h);
9386 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
9389 * Copyright (C) 2009 Mihai Şucan
9391 * This file is part of PaintWeb.
9393 * PaintWeb is free software: you can redistribute it and/or modify
9394 * it under the terms of the GNU General Public License as published by
9395 * the Free Software Foundation, either version 3 of the License, or
9396 * (at your option) any later version.
9398 * PaintWeb is distributed in the hope that it will be useful,
9399 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9400 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9401 * GNU General Public License for more details.
9403 * You should have received a copy of the GNU General Public License
9404 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
9406 * $URL: http://code.google.com/p/paintweb $
9407 * $Date: 2009-07-28 18:49:37 +0300 $
9411 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
9412 * @fileOverview Holds the integration code for PaintWeb inside <a
9413 * href="http://www.moodle.org">Moodle</a>.
9417 * @class The Moodle extension for PaintWeb. This extension handles the Moodle
9418 * integration inside the PaintWeb code.
9420 * @param {PaintWeb} app Reference to the main paint application object.
9422 pwlib.extensions.moodle = function (app) {
9424 appEvent = pwlib.appEvent,
9425 config = app.config,
9427 lang = app.lang.moodle;
9429 // Holds properties related to Moodle.
9431 // Holds the URL of the image the user is saving.
9434 // The class name for the element which holds the textarea buttons (toggle
9436 textareaButtons: 'textareaicons',
9438 // The image save handler script on the server-side. The path is relative to
9439 // the PaintWeb base folder.
9440 imageSaveHandler: '../ext/moodle/imagesave.php'
9444 * The <code>extensionRegister</code> event handler.
9446 * @returns {Boolean} True if the extension initialized successfully, or false
9449 this.extensionRegister = function () {
9450 // Register application events.
9451 app.events.add('guiShow', this.guiShow);
9452 app.events.add('guiHide', this.guiHide);
9453 app.events.add('imageSave', this.imageSave);
9459 * The <code>extensionUnregister</code> event handler.
9461 this.extensionUnregister = function () {
9466 * The <code>imageSave</code> application event handler. When the user
9467 * attempts to save an image, this extension handles the event by sending the
9468 * image data to the Moodle server, to perform the actual save operation.
9471 * @param {pwlib.appEvent.imageSave} ev The application event object.
9473 this.imageSave = function (ev) {
9478 ev.preventDefault();
9480 moodle.imageURL = config.imageLoad.src;
9481 if (!moodle.imageURL || moodle.imageURL.substr(0, 5) === 'data:') {
9482 moodle.imageURL = '-';
9485 if (!moodle.imageSaveHandler || config.moodleSaveMethod === 'dataURL') {
9486 app.events.dispatch(new appEvent.imageSaveResult(true, moodle.imageURL,
9490 var handlerURL = PaintWeb.baseFolder + moodle.imageSaveHandler,
9491 send = 'url=' + encodeURIComponent(moodle.imageURL) +
9492 '&dataURL=' + encodeURIComponent(ev.dataURL),
9493 headers = {'Content-Type': 'application/x-www-form-urlencoded'};
9495 pwlib.xhrLoad(handlerURL, imageSaveReady, 'POST', send, headers);
9500 * The image save <code>onreadystatechange</code> event handler for the
9501 * <code>XMLHttpRequest</code> which performs the image save. This function
9502 * uses the reply to determine if the image save operation is successful or
9505 * <p>The {@link pwlib.appEvent.imageSaveResult} application event is
9508 * <p>The server-side script must reply with a JSON object with the following
9512 * <li><var>successful</var> which tells if the image save operation was
9513 * successful or not;
9515 * <li><var>url</var> which must tell the same URL as the image we just
9516 * saved (sanity/security check);
9518 * <li><var>urlNew</var> is optional. This allows the server-side script to
9519 * change the image URL;
9521 * <li><var>errorMessage</var> is optional. When the image save was not
9522 * successful, an error message can be displayed.
9526 * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
9528 function imageSaveReady (xhr) {
9529 if (!xhr || xhr.readyState !== 4) {
9533 var result = {successful: false, url: moodle.imageURL};
9535 if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
9536 alert(lang.xhrRequestFailed);
9538 app.events.dispatch(new appEvent.imageSaveResult(false, result.url, null,
9539 lang.xhrRequestFailed));
9545 result = JSON.parse(xhr.responseText);
9547 result.errorMessage = lang.jsonParseFailed + "\n" + err;
9548 alert(result.errorMessage);
9551 if (result.successful) {
9552 if (result.url !== moodle.imageURL) {
9553 alert(pwlib.strf(lang.urlMismatch, {
9554 url: moodle.imageURL,
9555 urlServer: result.url || 'null'}));
9558 if (result.errorMessage) {
9559 alert(lang.imageSaveFailed + "\n" + result.errorMessage);
9561 alert(lang.imageSaveFailed);
9565 app.events.dispatch(new appEvent.imageSaveResult(result.successful,
9566 result.url, result.urlNew, result.errorMessage));
9570 * The <code>guiShow</code> application event handler. When the PaintWeb GUI
9571 * is shown, we must hide the textarea icons for the current textarea element,
9572 * inside a Moodle page.
9575 this.guiShow = function () {
9576 var pNode = config.guiPlaceholder.parentNode,
9577 elem = pNode.getElementsByClassName(moodle.textareaButtons)[0];
9580 elem.style.display = 'none';
9585 * The <code>guiHide</code> application event handler. When the PaintWeb GUI
9586 * is hidden, we must show again the textarea icons for the current textarea
9587 * element, inside a Moodle page.
9590 this.guiHide = function () {
9591 var pNode = config.guiPlaceholder.parentNode,
9592 elem = pNode.getElementsByClassName(moodle.textareaButtons)[0];
9595 elem.style.display = '';
9600 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
9603 * Copyright (C) 2008, 2009 Mihai Şucan
9605 * This file is part of PaintWeb.
9607 * PaintWeb is free software: you can redistribute it and/or modify
9608 * it under the terms of the GNU General Public License as published by
9609 * the Free Software Foundation, either version 3 of the License, or
9610 * (at your option) any later version.
9612 * PaintWeb is distributed in the hope that it will be useful,
9613 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9614 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9615 * GNU General Public License for more details.
9617 * You should have received a copy of the GNU General Public License
9618 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
9620 * $URL: http://code.google.com/p/paintweb $
9621 * $Date: 2009-07-26 17:56:47 +0300 $
9625 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
9626 * @fileOverview The default PaintWeb interface code.
9630 * @class The default PaintWeb interface.
9632 * @param {PaintWeb} app Reference to the main paint application object.
9634 pwlib.gui = function (app) {
9636 config = app.config,
9639 MathRound = Math.round,
9640 pwlib = window.pwlib,
9641 appEvent = pwlib.appEvent,
9645 this.idPrefix = 'paintweb' + app.UID + '_',
9646 this.classPrefix = 'paintweb_';
9649 * Holds references to DOM elements.
9655 * Holds references to input elements associated to the PaintWeb configuration
9662 * Holds references to DOM elements associated to configuration values.
9665 this.inputValues = {};
9668 * Holds references to DOM elements associated to color configuration
9672 * @see pwlib.guiColorInput
9674 this.colorInputs = {};
9677 * Holds references to DOM elements associated to each tool registered in the
9678 * current PaintWeb application instance.
9686 * Holds references to DOM elements associated to PaintWeb commands.
9694 * Holds references to floating panels GUI components.
9697 * @see pwlib.guiFloatingPanel
9699 this.floatingPanels = {zIndex_: 0};
9702 * Holds references to tab panel GUI components.
9705 * @see pwlib.guiTabPanel
9707 this.tabPanels = {};
9710 * Holds an instance of the guiResizer object attached to the Canvas.
9713 * @type pwlib.guiResizer
9715 this.canvasResizer = null;
9718 * Holds tab configuration information for most drawing tools.
9723 this.toolTabConfig = {
9728 lineWidthLabel: lang.inputs.borderWidth,
9735 lineWidthLabel: lang.inputs.borderWidth
9741 lineWidthLabel: lang.inputs.borderWidth,
9748 lineWidthLabel: lang.inputs.borderWidth,
9756 lineWidthLabel: lang.inputs.eraserSize,
9764 lineWidthLabel: lang.inputs.pencilSize,
9772 lineWidthLabel: lang.inputs.line.lineWidth,
9779 lineTabLabel: lang.tabs.main.textBorder,
9782 lineWidthLabel: lang.inputs.borderWidth
9787 * Initialize the PaintWeb interface.
9789 * @param {Document|String} markup The interface markup loaded and parsed as
9790 * DOM Document object. Optionally, the value can be a string holding the
9791 * interface markup (this is used when PaintWeb is packaged).
9793 * @returns {Boolean} True if the initialization was successful, or false if
9796 this.init = function (markup) {
9797 // Make sure the user nicely waits for PaintWeb to load, without seeing
9799 var placeholder = config.guiPlaceholder,
9800 placeholderStyle = placeholder.style;
9802 placeholderStyle.display = 'none';
9803 placeholderStyle.height = '1px';
9804 placeholderStyle.overflow = 'hidden';
9805 placeholderStyle.position = 'absolute';
9806 placeholderStyle.visibility = 'hidden';
9808 placeholder.className += ' ' + this.classPrefix + 'placeholder';
9809 if (!placeholder.tabIndex || placeholder.tabIndex == -1) {
9810 placeholder.tabIndex = 1;
9813 if (!this.initImportDoc(markup)) {
9814 app.initError(lang.guiMarkupImportFailed);
9819 if (!this.initParseMarkup()) {
9820 app.initError(lang.guiMarkupParseFailed);
9824 if (!this.initCanvas() ||
9825 !this.initImageZoom() ||
9826 !this.initSelectionTool() ||
9827 !this.initTextTool() ||
9828 !this.initKeyboardShortcuts()) {
9832 // Setup the main tabbed panel.
9833 var panel = this.tabPanels.main;
9835 app.initError(lang.noMainTabPanel);
9839 // Hide the "Shadow" tab if the drawing of shadows is not supported.
9840 if (!app.shadowSupported && 'shadow' in panel.tabs) {
9841 panel.tabHide('shadow');
9844 // Setup the viewport height.
9845 if ('viewport' in this.elems) {
9846 this.elems.viewport.style.height = config.viewportHeight + 'px';
9849 // Setup the Canvas resizer.
9850 var resizeHandle = this.elems.canvasResizer;
9851 if (!resizeHandle) {
9852 app.initError(lang.missingCanvasResizer);
9855 resizeHandle.title = lang.guiCanvasResizer;
9856 resizeHandle.replaceChild(doc.createTextNode(lang.guiCanvasResizer),
9857 resizeHandle.firstChild);
9858 resizeHandle.addEventListener('mouseover', this.item_mouseover, false);
9859 resizeHandle.addEventListener('mouseout', this.item_mouseout, false);
9861 this.canvasResizer = new pwlib.guiResizer(this, resizeHandle,
9862 this.elems.canvasContainer);
9864 this.canvasResizer.events.add('guiResizeStart', this.canvasResizeStart);
9865 this.canvasResizer.events.add('guiResizeEnd', this.canvasResizeEnd);
9867 if ('statusMessage' in this.elems) {
9868 this.elems.statusMessage._prevText = false;
9871 // Update the version string in Help.
9872 if ('version' in this.elems) {
9873 this.elems.version.appendChild(doc.createTextNode(app.toString()));
9876 // Update the image dimensions in the GUI.
9877 var imageSize = this.elems.imageSize;
9879 imageSize.replaceChild(doc.createTextNode(app.image.width + 'x'
9880 + app.image.height), imageSize.firstChild);
9883 // Add application-wide event listeners.
9884 app.events.add('canvasSizeChange', this.canvasSizeChange);
9885 app.events.add('commandRegister', this.commandRegister);
9886 app.events.add('commandUnregister', this.commandUnregister);
9887 app.events.add('configChange', this.configChangeHandler);
9888 app.events.add('imageSizeChange', this.imageSizeChange);
9889 app.events.add('imageZoom', this.imageZoom);
9890 app.events.add('appInit', this.appInit);
9891 app.events.add('shadowAllow', this.shadowAllow);
9892 app.events.add('toolActivate', this.toolActivate);
9893 app.events.add('toolRegister', this.toolRegister);
9894 app.events.add('toolUnregister', this.toolUnregister);
9896 // Make sure the historyUndo and historyRedo command elements are
9897 // synchronized with the application history state.
9898 if ('historyUndo' in this.commands && 'historyRedo' in this.commands) {
9899 app.events.add('historyUpdate', this.historyUpdate);
9902 app.commandRegister('about', this.commandAbout);
9908 * Initialize the Canvas elements.
9911 * @returns {Boolean} True if the initialization was successful, or false if
9914 this.initCanvas = function () {
9915 var canvasContainer = this.elems.canvasContainer,
9916 layerCanvas = app.layer.canvas,
9917 layerContext = app.layer.context,
9918 layerStyle = layerCanvas.style,
9919 bufferCanvas = app.buffer.canvas;
9921 if (!canvasContainer) {
9922 app.initError(lang.missingCanvasContainer);
9926 var containerStyle = canvasContainer.style;
9928 canvasContainer.className = this.classPrefix + 'canvasContainer';
9929 layerCanvas.className = this.classPrefix + 'layerCanvas';
9930 bufferCanvas.className = this.classPrefix + 'bufferCanvas';
9932 containerStyle.width = layerStyle.width;
9933 containerStyle.height = layerStyle.height;
9935 canvasContainer.appendChild(layerCanvas);
9936 canvasContainer.appendChild(bufferCanvas);
9937 canvasContainer.style.backgroundColor = config.backgroundColor;
9939 // Make sure the selection transparency input checkbox is disabled if the
9940 // putImageData and getImageData methods are unsupported.
9941 if ('selection_transparent' in this.inputs && (!layerContext.putImageData ||
9942 !layerContext.getImageData)) {
9943 this.inputs.selection_transparent.disabled = true;
9944 this.inputs.selection_transparent.checked = true;
9951 * Import the DOM nodes from the interface DOM document. All the nodes are
9952 * inserted into the {@link PaintWeb.config.guiPlaceholder} element.
9954 * <p>Elements which have the ID attribute will have the attribute renamed to
9955 * <code>data-pwId</code>.
9957 * <p>Input elements which have the ID attribute will have their attribute
9958 * updated to be unique for the current PaintWeb instance.
9962 * @param {Document|String} markup The source DOM document to import the nodes
9963 * from. Optionally, this parameter can be a string which holds the interface
9966 * @returns {Boolean} True if the initialization was successful, or false if
9969 this.initImportDoc = function (markup) {
9970 // I could use some XPath here, but for the sake of compatibility I don't.
9971 var destElem = config.guiPlaceholder,
9972 elType = Node.ELEMENT_NODE,
9973 elem, root, nodes, n, tag, isInput;
9975 if (typeof markup === 'string') {
9976 elem = doc.createElement('div');
9977 elem.innerHTML = markup;
9978 root = elem.firstChild;
9980 root = markup.documentElement;
9984 nodes = root.getElementsByTagName('*');
9987 // Change all the id attributes to be data-pwId attributes.
9988 // Input elements have their ID updated to be unique for the current
9989 // PaintWeb instance.
9990 for (var i = 0; i < n; i++) {
9992 if (elem.nodeType !== elType) {
9995 tag = elem.tagName.toLowerCase();
9996 isInput = tag === 'input' || tag === 'select' || tag === 'textarea';
9999 elem.setAttribute('data-pwId', elem.id);
10002 elem.id = this.idPrefix + elem.id;
10004 elem.removeAttribute('id');
10008 // label elements have their "for" attribute updated as well.
10009 if (tag === 'label' && elem.htmlFor) {
10010 elem.htmlFor = this.idPrefix + elem.htmlFor;
10014 // Import all the nodes.
10015 n = root.childNodes.length;
10016 for (var i = 0; i < n; i++) {
10017 destElem.appendChild(doc.importNode(root.childNodes[i], true));
10024 * Parse the interface markup. The layout file can have custom
10025 * PaintWeb-specific attributes.
10027 * <p>Elements with the <code>data-pwId</code> attribute are added to the
10028 * {@link pwlib.gui#elems} object.
10030 * <p>Elements having the <code>data-pwCommand</code> attribute are added to
10031 * the {@link pwlib.gui#commands} object.
10033 * <p>Elements having the <code>data-pwTool</code> attribute are added to the
10034 * {@link pwlib.gui#tools} object.
10036 * <p>Elements having the <code>data-pwTabPanel</code> attribute are added to
10037 * the {@link pwlib.gui#tabPanels} object. These become interactive GUI
10038 * components (see {@link pwlib.guiTabPanel}).
10040 * <p>Elements having the <code>data-pwFloatingPanel</code> attribute are
10041 * added to the {@link pwlib.gui#floatingPanels} object. These become
10042 * interactive GUI components (see {@link pwlib.guiFloatingPanel}).
10044 * <p>Elements having the <code>data-pwConfig</code> attribute are added to
10045 * the {@link pwlib.gui#inputs} object. These become interactive GUI
10046 * components which allow users to change configuration options.
10048 * <p>Elements having the <code>data-pwConfigValue</code> attribute are added
10049 * to the {@link pwlib.gui#inputValues} object. These can only be child nodes
10050 * of elements which have the <code>data-pwConfig</code> attribute. Each such
10051 * element is considered an icon. Anchor elements are appended to ensure
10052 * keyboard accessibility.
10054 * <p>Elements having the <code>data-pwConfigToggle</code> attribute are added
10055 * to the {@link pwlib.gui#inputs} object. These become interactive GUI
10056 * components which toggle the boolean value of the configuration property
10057 * they are associated to.
10059 * <p>Elements having the <code>data-pwColorInput</code> attribute are added
10060 * to the {@link pwlib.gui#colorInputs} object. These become color picker
10061 * inputs which are associated to the configuration property given as the
10062 * attribute value. (see {@link pwlib.guiColorInput})
10064 * @returns {Boolean} True if the parsing was successful, or false if not.
10066 this.initParseMarkup = function () {
10067 var nodes = config.guiPlaceholder.getElementsByTagName('*'),
10068 elType = Node.ELEMENT_NODE,
10069 elem, tag, isInput, tool, tabPanel, floatingPanel, cmd, id, cfgAttr,
10072 // Store references to important elements and parse PaintWeb-specific
10074 for (var i = 0; i < nodes.length; i++) {
10076 if (elem.nodeType !== elType) {
10079 tag = elem.tagName.toLowerCase();
10080 isInput = tag === 'input' || tag === 'select' || tag === 'textarea';
10082 // Store references to commands.
10083 cmd = elem.getAttribute('data-pwCommand');
10084 if (cmd && !(cmd in this.commands)) {
10085 elem.className += ' ' + this.classPrefix + 'command';
10086 this.commands[cmd] = elem;
10089 // Store references to tools.
10090 tool = elem.getAttribute('data-pwTool');
10091 if (tool && !(tool in this.tools)) {
10092 elem.className += ' ' + this.classPrefix + 'tool';
10093 this.tools[tool] = elem;
10096 // Create tab panels.
10097 tabPanel = elem.getAttribute('data-pwTabPanel');
10099 this.tabPanels[tabPanel] = new pwlib.guiTabPanel(this, elem);
10102 // Create floating panels.
10103 floatingPanel = elem.getAttribute('data-pwFloatingPanel');
10104 if (floatingPanel) {
10105 this.floatingPanels[floatingPanel] = new pwlib.guiFloatingPanel(this,
10109 cfgAttr = elem.getAttribute('data-pwConfig');
10112 this.initConfigInput(elem, cfgAttr);
10114 this.initConfigIcons(elem, cfgAttr);
10118 cfgAttr = elem.getAttribute('data-pwConfigToggle');
10120 this.initConfigToggle(elem, cfgAttr);
10123 // elem.hasAttribute() fails in webkit (tested with chrome and safari 4)
10124 if (elem.getAttribute('data-pwColorInput')) {
10125 colorInput = new pwlib.guiColorInput(this, elem);
10126 this.colorInputs[colorInput.id] = colorInput;
10129 id = elem.getAttribute('data-pwId');
10131 elem.className += ' ' + this.classPrefix + id;
10133 // Store a reference to the element.
10134 if (isInput && !cfgAttr) {
10135 this.inputs[id] = elem;
10136 } else if (!isInput) {
10137 this.elems[id] = elem;
10146 * Initialize an input element associated to a configuration property.
10150 * @param {Element} elem The DOM element which is associated to the
10151 * configuration property.
10153 * @param {String} cfgAttr The configuration attribute. This tells the
10154 * configuration group and property to which the DOM element is attached to.
10156 this.initConfigInput = function (input, cfgAttr) {
10157 var cfgNoDots = cfgAttr.replace('.', '_'),
10158 cfgArray = cfgAttr.split('.'),
10159 cfgProp = cfgArray.pop(),
10160 cfgGroup = cfgArray.join('.'),
10161 cfgGroupRef = config,
10162 langGroup = lang.inputs,
10163 labelElem = input.parentNode;
10165 for (var i = 0, n = cfgArray.length; i < n; i++) {
10166 cfgGroupRef = cfgGroupRef[cfgArray[i]];
10167 langGroup = langGroup[cfgArray[i]];
10170 input._pwConfigProperty = cfgProp;
10171 input._pwConfigGroup = cfgGroup;
10172 input._pwConfigGroupRef = cfgGroupRef;
10173 input.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
10174 input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots;
10176 this.inputs[cfgNoDots] = input;
10178 if (labelElem.tagName.toLowerCase() !== 'label') {
10179 labelElem = labelElem.getElementsByTagName('label')[0];
10182 if (input.type === 'checkbox' || labelElem.htmlFor) {
10183 labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]),
10184 labelElem.lastChild);
10186 labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]),
10187 labelElem.firstChild);
10190 if (input.type === 'checkbox') {
10191 input.checked = cfgGroupRef[cfgProp];
10193 input.value = cfgGroupRef[cfgProp];
10196 input.addEventListener('input', this.configInputChange, false);
10197 input.addEventListener('change', this.configInputChange, false);
10201 * Initialize an HTML element associated to a configuration property, and all
10202 * of its own sub-elements associated to configuration values. Each element
10203 * that has the <var>data-pwConfigValue</var> attribute is considered an icon.
10207 * @param {Element} elem The DOM element which is associated to the
10208 * configuration property.
10210 * @param {String} cfgAttr The configuration attribute. This tells the
10211 * configuration group and property to which the DOM element is attached to.
10213 this.initConfigIcons = function (input, cfgAttr) {
10214 var cfgNoDots = cfgAttr.replace('.', '_'),
10215 cfgArray = cfgAttr.split('.'),
10216 cfgProp = cfgArray.pop(),
10217 cfgGroup = cfgArray.join('.'),
10218 cfgGroupRef = config,
10219 langGroup = lang.inputs;
10221 for (var i = 0, n = cfgArray.length; i < n; i++) {
10222 cfgGroupRef = cfgGroupRef[cfgArray[i]];
10223 langGroup = langGroup[cfgArray[i]];
10226 input._pwConfigProperty = cfgProp;
10227 input._pwConfigGroup = cfgGroup;
10228 input._pwConfigGroupRef = cfgGroupRef;
10229 input.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
10230 input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots;
10232 this.inputs[cfgNoDots] = input;
10234 var labelElem = input.getElementsByTagName('p')[0];
10235 labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]),
10236 labelElem.firstChild);
10238 var elem, anchor, val,
10239 className = ' ' + this.classPrefix + 'configActive';
10240 nodes = input.getElementsByTagName('*'),
10241 elType = Node.ELEMENT_NODE;
10243 for (var i = 0; i < nodes.length; i++) {
10245 if (elem.nodeType !== elType) {
10249 val = elem.getAttribute('data-pwConfigValue');
10254 anchor = doc.createElement('a');
10256 anchor.title = langGroup[cfgProp + '_' + val];
10257 anchor.appendChild(doc.createTextNode(anchor.title));
10259 elem.className += ' ' + this.classPrefix + cfgProp + '_' + val
10260 + ' ' + this.classPrefix + 'icon';
10261 elem._pwConfigParent = input;
10263 if (cfgGroupRef[cfgProp] == val) {
10264 elem.className += className;
10267 anchor.addEventListener('click', this.configValueClick, false);
10268 anchor.addEventListener('mouseover', this.item_mouseover, false);
10269 anchor.addEventListener('mouseout', this.item_mouseout, false);
10271 elem.replaceChild(anchor, elem.firstChild);
10273 this.inputValues[cfgGroup + '_' + cfgProp + '_' + val] = elem;
10278 * Initialize an HTML element associated to a boolean configuration property.
10282 * @param {Element} elem The DOM element which is associated to the
10283 * configuration property.
10285 * @param {String} cfgAttr The configuration attribute. This tells the
10286 * configuration group and property to which the DOM element is attached to.
10288 this.initConfigToggle = function (input, cfgAttr) {
10289 var cfgNoDots = cfgAttr.replace('.', '_'),
10290 cfgArray = cfgAttr.split('.'),
10291 cfgProp = cfgArray.pop(),
10292 cfgGroup = cfgArray.join('.'),
10293 cfgGroupRef = config,
10294 langGroup = lang.inputs;
10296 for (var i = 0, n = cfgArray.length; i < n; i++) {
10297 cfgGroupRef = cfgGroupRef[cfgArray[i]];
10298 langGroup = langGroup[cfgArray[i]];
10301 input._pwConfigProperty = cfgProp;
10302 input._pwConfigGroup = cfgGroup;
10303 input._pwConfigGroupRef = cfgGroupRef;
10304 input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots
10305 + ' ' + this.classPrefix + 'icon';
10307 if (cfgGroupRef[cfgProp]) {
10308 input.className += ' ' + this.classPrefix + 'configActive';
10311 var anchor = doc.createElement('a');
10313 anchor.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
10314 anchor.appendChild(doc.createTextNode(langGroup[cfgProp]));
10316 anchor.addEventListener('click', this.configToggleClick, false);
10317 anchor.addEventListener('mouseover', this.item_mouseover, false);
10318 anchor.addEventListener('mouseout', this.item_mouseout, false);
10320 input.replaceChild(anchor, input.firstChild);
10322 this.inputs[cfgNoDots] = input;
10326 * Initialize the image zoom input.
10329 * @returns {Boolean} True if the initialization was successful, or false if
10332 this.initImageZoom = function () {
10333 var input = this.inputs.imageZoom;
10335 return true; // allow layouts without the zoom input
10339 input._old_value = 100;
10341 // Override the attributes, based on the settings.
10342 input.setAttribute('step', config.imageZoomStep * 100);
10343 input.setAttribute('max', config.imageZoomMax * 100);
10344 input.setAttribute('min', config.imageZoomMin * 100);
10346 var changeFn = function () {
10347 app.imageZoomTo(parseInt(this.value) / 100);
10350 input.addEventListener('change', changeFn, false);
10351 input.addEventListener('input', changeFn, false);
10353 // Update some language strings
10355 var label = input.parentNode;
10356 if (label.tagName.toLowerCase() === 'label') {
10357 label.replaceChild(doc.createTextNode(lang.imageZoomLabel),
10361 var elem = this.elems.statusZoom;
10366 elem.title = lang.imageZoomTitle;
10372 * Initialize GUI elements associated to selection tool options and commands.
10375 * @returns {Boolean} True if the initialization was successful, or false if
10378 this.initSelectionTool = function () {
10379 var classDisabled = ' ' + this.classPrefix + 'disabled',
10380 cut = this.commands.selectionCut,
10381 copy = this.commands.selectionCopy,
10382 paste = this.commands.clipboardPaste;
10385 app.events.add('clipboardUpdate', this.clipboardUpdate);
10386 paste.className += classDisabled;
10391 app.events.add('selectionChange', this.selectionChange);
10392 cut.className += classDisabled;
10393 copy.className += classDisabled;
10396 var selTab_cmds = ['selectionCut', 'selectionCopy', 'clipboardPaste'],
10399 for (var i = 0, n = selTab_cmds.length; i < n; i++) {
10400 cmd = selTab_cmds[i];
10401 elem = this.elems['selTab_' + cmd];
10406 anchor = doc.createElement('a');
10407 anchor.title = lang.commands[cmd];
10409 anchor.appendChild(doc.createTextNode(anchor.title));
10410 anchor.addEventListener('click', this.commandClick, false);
10412 elem.className += classDisabled + ' ' + this.classPrefix + 'command'
10413 + ' ' + this.classPrefix + 'cmd_' + cmd;
10414 elem.setAttribute('data-pwCommand', cmd);
10415 elem.replaceChild(anchor, elem.firstChild);
10418 var selCrop = this.commands.selectionCrop,
10419 selFill = this.commands.selectionFill,
10420 selDelete = this.commands.selectionDelete;
10422 selCrop.className += classDisabled;
10423 selFill.className += classDisabled;
10424 selDelete.className += classDisabled;
10430 * Initialize GUI elements associated to text tool options.
10433 * @returns {Boolean} True if the initialization was successful, or false if
10436 this.initTextTool = function () {
10437 if ('textString' in this.inputs) {
10438 this.inputs.textString.value = lang.inputs.text.textString_value;
10441 if (!('text_fontFamily' in this.inputs) || !('text' in config) ||
10442 !('fontFamilies' in config.text)) {
10446 var option, input = this.inputs.text_fontFamily;
10447 for (var i = 0, n = config.text.fontFamilies.length; i < n; i++) {
10448 option = doc.createElement('option');
10449 option.value = config.text.fontFamilies[i];
10450 option.appendChild(doc.createTextNode(option.value));
10451 input.appendChild(option);
10453 if (option.value === config.text.fontFamily) {
10454 input.selectedIndex = i;
10455 input.value = option.value;
10459 option = doc.createElement('option');
10460 option.value = '+';
10461 option.appendChild(doc.createTextNode(lang.inputs.text.fontFamily_add));
10462 input.appendChild(option);
10468 * Initialize the keyboard shortcuts. Basically, this updates various strings
10469 * to ensure the user interface is informational.
10472 * @returns {Boolean} True if the initialization was successful, or false if
10475 this.initKeyboardShortcuts = function () {
10476 var kid = null, kobj = null;
10478 for (kid in config.keys) {
10479 kobj = config.keys[kid];
10481 if ('toolActivate' in kobj && kobj.toolActivate in lang.tools) {
10482 lang.tools[kobj.toolActivate] += ' [ ' + kid + ' ]';
10485 if ('command' in kobj && kobj.command in lang.commands) {
10486 lang.commands[kobj.command] += ' [ ' + kid + ' ]';
10494 * The <code>appInit</code> event handler. This method is invoked once
10495 * PaintWeb completes all the loading.
10497 * <p>This method dispatches the {@link pwlib.appEvent.guiShow} application
10501 * @param {pwlib.appEvent.appInit} ev The application event object.
10503 this.appInit = function (ev) {
10504 // Initialization was not successful ...
10505 if (ev.state !== PaintWeb.INIT_DONE) {
10509 // Make PaintWeb visible.
10510 var placeholder = config.guiPlaceholder,
10511 placeholderStyle = placeholder.style,
10512 cs = win.getComputedStyle(placeholder, null);
10514 // We do not reset the display property. We leave this for the stylesheet.
10515 placeholderStyle.height = '';
10516 placeholderStyle.overflow = '';
10517 placeholderStyle.position = '';
10518 placeholderStyle.visibility = '';
10520 // Do not allow the static positioning for the PaintWeb placeholder.
10521 // Usually, the GUI requires absolute/relative positioning.
10522 if (cs.position === 'static') {
10523 placeholderStyle.position = 'relative';
10526 placeholder.focus();
10528 app.events.dispatch(new appEvent.guiShow());
10532 * The <code>guiResizeStart</code> event handler for the Canvas resize
10536 this.canvasResizeStart = function () {
10537 this.resizeHandle.style.visibility = 'hidden';
10540 this.timeout_ = setTimeout(function () {
10541 _self.statusShow('guiCanvasResizerActive', true);
10542 clearTimeout(_self.canvasResizer.timeout_);
10543 delete _self.canvasResizer.timeout_;
10548 * The <code>guiResizeEnd</code> event handler for the Canvas resize
10552 * @param {pwlib.appEvent.guiResizeEnd} ev The application event object.
10554 this.canvasResizeEnd = function (ev) {
10555 this.resizeHandle.style.visibility = '';
10557 app.imageCrop(0, 0, MathRound(ev.width / app.image.canvasScale),
10558 MathRound(ev.height / app.image.canvasScale));
10560 if (this.timeout_) {
10561 clearTimeout(this.timeout_);
10562 delete this.timeout_;
10564 _self.statusShow(-1);
10569 * The <code>mouseover</code> event handler for all tools, commands and icons.
10570 * This simply shows the title / text content of the element in the GUI status
10573 * @see pwlib.gui#statusShow The method used for displaying the message in the
10576 this.item_mouseover = function () {
10577 if (this.title || this.textConent) {
10578 _self.statusShow(this.title || this.textContent, true);
10583 * The <code>mouseout</code> event handler for all tools, commands and icons.
10584 * This method simply resets the GUI status bar to the previous message it was
10585 * displaying before the user hovered the current element.
10587 * @see pwlib.gui#statusShow The method used for displaying the message in the
10590 this.item_mouseout = function () {
10591 _self.statusShow(-1);
10595 * Show a message in the status bar.
10597 * @param {String|Number} msg The message ID you want to display. The ID
10598 * should be available in the {@link PaintWeb.lang.status} object. If the
10599 * value is -1 then the previous non-temporary message will be displayed. If
10600 * the ID is not available in the language file, then the string is shown
10603 * @param {Boolean} [temporary=false] Tells if the message is temporary or
10606 this.statusShow = function (msg, temporary) {
10607 var elem = this.elems.statusMessage;
10608 if (msg === -1 && elem._prevText === false) {
10613 msg = elem._prevText;
10616 if (msg in lang.status) {
10617 msg = lang.status[msg];
10621 elem._prevText = msg;
10624 if (elem.firstChild) {
10625 elem.removeChild(elem.firstChild);
10631 elem.appendChild(doc.createTextNode(msg));
10636 * The "About" command. This method displays the "About" panel.
10638 this.commandAbout = function () {
10639 _self.floatingPanels.about.toggle();
10643 * The <code>click</code> event handler for the tool DOM elements.
10647 * @param {Event} ev The DOM Event object.
10649 * @see PaintWeb#toolActivate to activate a drawing tool.
10651 this.toolClick = function (ev) {
10652 app.toolActivate(this.parentNode.getAttribute('data-pwTool'), ev);
10653 ev.preventDefault();
10657 * The <code>toolActivate</code> application event handler. This method
10658 * provides visual feedback for the activation of a new drawing tool.
10662 * @param {pwlib.appEvent.toolActivate} ev The application event object.
10664 * @see PaintWeb#toolActivate the method which allows you to activate
10667 this.toolActivate = function (ev) {
10669 tabActive = _self.tools[ev.id],
10670 tabConfig = _self.toolTabConfig[ev.id] || {},
10671 tabPanel = _self.tabPanels.main,
10672 lineTab = tabPanel.tabs.line,
10673 shapeType = _self.inputs.shapeType,
10674 lineWidth = _self.inputs.line_lineWidth,
10675 lineCap = _self.inputs.line_lineCap,
10676 lineJoin = _self.inputs.line_lineJoin,
10677 miterLimit = _self.inputs.line_miterLimit,
10678 lineWidthLabel = null;
10680 tabActive.className += ' ' + _self.classPrefix + 'toolActive';
10681 tabActive.firstChild.focus();
10683 if ((ev.id + 'Active') in lang.status) {
10684 _self.statusShow(ev.id + 'Active');
10687 // show/hide the shapeType input config.
10689 if (tabConfig.shapeType) {
10690 shapeType.style.display = '';
10692 shapeType.style.display = 'none';
10697 var prevTab = _self.tools[ev.prevId],
10698 prevTabConfig = _self.toolTabConfig[ev.prevId] || {};
10700 prevTab.className = prevTab.className.
10701 replace(' ' + _self.classPrefix + 'toolActive', '');
10703 // hide the line tab
10704 if (prevTabConfig.lineTab && lineTab) {
10705 tabPanel.tabHide('line');
10706 lineTab.container.className = lineTab.container.className.
10707 replace(' ' + _self.classPrefix + 'main_line_' + ev.prevId,
10708 ' ' + _self.classPrefix + 'main_line');
10711 // hide the tab for the current tool.
10712 if (ev.prevId in tabPanel.tabs) {
10713 tabPanel.tabHide(ev.prevId);
10717 // Change the label of the lineWidth input element.
10718 if (tabConfig.lineWidthLabel) {
10719 lineWidthLabel = lineWidth.parentNode;
10720 lineWidthLabel.replaceChild(doc.createTextNode(tabConfig.lineWidthLabel),
10721 lineWidthLabel.firstChild);
10726 if (tabConfig.lineJoin) {
10727 lineJoin.style.display = '';
10729 lineJoin.style.display = 'none';
10734 if (tabConfig.lineCap) {
10735 lineCap.style.display = '';
10737 lineCap.style.display = 'none';
10742 if (tabConfig.miterLimit) {
10743 miterLimit.parentNode.parentNode.style.display = '';
10745 miterLimit.parentNode.parentNode.style.display = 'none';
10750 if (tabConfig.lineWidth) {
10751 lineWidth.parentNode.parentNode.style.display = '';
10753 lineWidth.parentNode.parentNode.style.display = 'none';
10757 // show the line tab, if configured
10758 if (tabConfig.lineTab && 'line' in tabPanel.tabs) {
10759 tabAnchor = lineTab.button.firstChild;
10760 tabAnchor.title = tabConfig.lineTabLabel || lang.tabs.main[ev.id];
10761 tabAnchor.replaceChild(doc.createTextNode(tabAnchor.title),
10762 tabAnchor.firstChild);
10764 if (ev.id !== 'line') {
10765 lineTab.container.className = lineTab.container.className.
10766 replace(' ' + _self.classPrefix + 'main_line', ' ' + _self.classPrefix
10767 + 'main_line_' + ev.id);
10770 tabPanel.tabShow('line');
10773 // show the tab for the current tool, if there's one.
10774 if (ev.id in tabPanel.tabs) {
10775 tabPanel.tabShow(ev.id);
10780 * The <code>toolRegister</code> application event handler. This method adds
10781 * the new tool into the GUI.
10785 * @param {pwlib.appEvent.toolRegister} ev The application event object.
10787 * @see PaintWeb#toolRegister the method which allows you to register new
10790 this.toolRegister = function (ev) {
10791 var attr = null, elem = null, anchor = null;
10793 if (ev.id in _self.tools) {
10794 elem = _self.tools[ev.id];
10795 attr = elem.getAttribute('data-pwTool');
10796 if (attr && attr !== ev.id) {
10799 delete _self.tools[ev.id];
10803 // Create a new element if there's none already associated to the tool ID.
10805 elem = doc.createElement('li');
10809 elem.setAttribute('data-pwTool', ev.id);
10812 elem.className += ' ' + _self.classPrefix + 'tool_' + ev.id;
10814 // Append an anchor element which holds the locale string.
10815 anchor = doc.createElement('a');
10816 anchor.title = lang.tools[ev.id];
10818 anchor.appendChild(doc.createTextNode(anchor.title));
10820 elem.replaceChild(anchor, elem.firstChild);
10822 anchor.addEventListener('click', _self.toolClick, false);
10823 anchor.addEventListener('mouseover', _self.item_mouseover, false);
10824 anchor.addEventListener('mouseout', _self.item_mouseout, false);
10826 if (!(ev.id in _self.tools)) {
10827 _self.tools[ev.id] = elem;
10828 _self.elems.tools.appendChild(elem);
10831 // Disable the text tool icon if the Canvas Text API is not supported.
10832 if (ev.id === 'text' && !app.layer.context.fillText &&
10833 !app.layer.context.mozPathText && elem) {
10834 elem.className += ' ' + _self.classPrefix + 'disabled';
10835 anchor.title = lang.tools.textUnsupported;
10837 anchor.removeEventListener('click', _self.toolClick, false);
10838 anchor.addEventListener('click', function (ev) {
10839 ev.preventDefault();
10845 * The <code>toolUnregister</code> application event handler. This method the
10846 * tool element from the GUI.
10848 * @param {pwlib.appEvent.toolUnregister} ev The application event object.
10850 * @see PaintWeb#toolUnregister the method which allows you to unregister
10853 this.toolUnregister = function (ev) {
10854 if (ev.id in _self.tools) {
10855 _self.elems.tools.removeChild(_self.tools[ev.id]);
10856 delete _self.tools[ev.id];
10863 * The <code>click</code> event handler for the command DOM elements.
10867 * @param {Event} ev The DOM Event object.
10869 * @see PaintWeb#commandRegister to register a new command.
10871 this.commandClick = function (ev) {
10872 var cmd = this.parentNode.getAttribute('data-pwCommand');
10873 if (cmd && cmd in app.commands) {
10874 app.commands[cmd].call(this, ev);
10876 ev.preventDefault();
10881 * The <code>commandRegister</code> application event handler. GUI elements
10882 * associated to commands are updated to ensure proper user interaction.
10886 * @param {pwlib.appEvent.commandRegister} ev The application event object.
10888 * @see PaintWeb#commandRegister the method which allows you to register new
10891 this.commandRegister = function (ev) {
10892 var elem = _self.commands[ev.id],
10898 elem.className += ' ' + _self.classPrefix + 'cmd_' + ev.id;
10900 anchor = doc.createElement('a');
10901 anchor.title = lang.commands[ev.id];
10903 anchor.appendChild(doc.createTextNode(anchor.title));
10905 // Remove the text content and append the locale string associated to
10906 // current command inside an anchor element (for better keyboard
10908 if (elem.firstChild) {
10909 elem.removeChild(elem.firstChild);
10911 elem.appendChild(anchor);
10913 anchor.addEventListener('click', _self.commandClick, false);
10914 anchor.addEventListener('mouseover', _self.item_mouseover, false);
10915 anchor.addEventListener('mouseout', _self.item_mouseout, false);
10919 * The <code>commandUnregister</code> application event handler. This method
10920 * simply removes all the user interactivity from the GUI element associated
10921 * to the command being unregistered.
10923 * @param {pwlib.appEvent.commandUnregister} ev The application event object.
10925 * @see PaintWeb#commandUnregister the method which allows you to unregister
10928 this.commandUnregister = function (ev) {
10929 var elem = _self.commands[ev.id],
10935 elem.className = elem.className.replace(' ' + _self.classPrefix + 'cmd_'
10938 anchor = elem.firstChild;
10939 anchor.removeEventListener('click', this.commands[ev.id], false);
10940 anchor.removeEventListener('mouseover', _self.item_mouseover, false);
10941 anchor.removeEventListener('mouseout', _self.item_mouseout, false);
10943 elem.removeChild(anchor);
10947 * The <code>historyUpdate</code> application event handler. GUI elements
10948 * associated to the <code>historyUndo</code> and to the
10949 * <code>historyRedo</code> commands are updated such that they are either
10950 * enabled or disabled, depending on the current history position.
10952 * @param {pwlib.appEvent.historyUpdate} ev The application event object.
10953 * @see PaintWeb#historyGoto the method which allows you to go to different
10956 this.historyUpdate = function (ev) {
10957 var undoElem = _self.commands.historyUndo,
10959 redoElem = _self.commands.historyRedo,
10961 className = ' ' + _self.classPrefix + 'disabled',
10962 undoElemState = undoElem.className.indexOf(className) === -1,
10963 redoElemState = redoElem.className.indexOf(className) === -1;
10965 if (ev.currentPos > 1) {
10968 if (ev.currentPos < ev.states) {
10972 if (undoElemState !== undoState) {
10974 undoElem.className = undoElem.className.replace(className, '');
10976 undoElem.className += className;
10980 if (redoElemState !== redoState) {
10982 redoElem.className = redoElem.className.replace(className, '');
10984 redoElem.className += className;
10990 * The <code>imageSizeChange</code> application event handler. The GUI element
10991 * which displays the image dimensions is updated to display the new image
10994 * <p>Image size refers strictly to the dimensions of the image being edited
10995 * by the user, that's width and height.
10997 * @param {pwlib.appEvent.imageSizeChange} ev The application event object.
10999 this.imageSizeChange = function (ev) {
11000 var imageSize = _self.elems.imageSize;
11002 imageSize.replaceChild(doc.createTextNode(ev.width + 'x' + ev.height),
11003 imageSize.firstChild);
11008 * The <code>canvasSizeChange</code> application event handler. The Canvas
11009 * container element dimensions are updated to the new values and the Hand
11010 * tool is enabled/disabled as necessary.
11012 * <p>Canvas size refers strictly to the dimensions of the Canvas elements in
11013 * the browser, changed with CSS style properties, width and height. Scaling
11014 * of the Canvas elements is applied when the user zooms the image or when the
11015 * browser changes the render DPI / zoom.
11017 * @param {pwlib.appEvent.canvasSizeChange} ev The application event object.
11019 this.canvasSizeChange = function (ev) {
11020 var canvasContainer = _self.elems.canvasContainer,
11021 canvasResizer = _self.canvasResizer,
11022 className = ' ' + _self.classPrefix + 'disabled',
11023 hand = _self.tools.hand,
11024 resizeHandle = canvasResizer.resizeHandle,
11025 viewport = _self.elems.viewport;
11027 // Update the Canvas container to be the same size as the Canvas elements.
11028 canvasContainer.style.width = ev.width + 'px';
11029 canvasContainer.style.height = ev.height + 'px';
11031 if (resizeHandle) {
11032 resizeHandle.style.top = ev.height + 'px';
11033 resizeHandle.style.left = ev.width + 'px';
11036 if (!hand || !viewport) {
11040 // Update Hand tool state.
11041 var cs = win.getComputedStyle(viewport, null),
11042 vwidth = parseInt(cs.width),
11043 vheight = parseInt(cs.height),
11044 enableHand = false,
11045 handState = hand.className.indexOf(className) === -1;
11047 if (vheight < ev.height || vwidth < ev.width) {
11051 if (enableHand && !handState) {
11052 hand.className = hand.className.replace(className, '');
11053 } else if (!enableHand && handState) {
11054 hand.className += className;
11057 if (!enableHand && app.tool && app.tool._id === 'hand' && 'prevTool' in
11059 app.toolActivate(app.tool.prevTool);
11064 * The <code>imageZoom</code> application event handler. The GUI input element
11065 * which displays the image zoom level is updated to display the new value.
11067 * @param {pwlib.appEvent.imageZoom} ev The application event object.
11069 this.imageZoom = function (ev) {
11070 var elem = _self.inputs.imageZoom,
11071 val = MathRound(ev.zoom * 100);
11072 if (elem && elem.value != val) {
11078 * The <code>configChange</code> application event handler. This method
11079 * ensures the GUI input elements stay up-to-date when some PaintWeb
11080 * configuration is modified.
11082 * @param {pwlib.appEvent.configChange} ev The application event object.
11084 this.configChangeHandler = function (ev) {
11085 var cfg = '', input;
11087 cfg = ev.group.replace('.', '_') + '_';
11090 input = _self.inputs[cfg];
11092 // Handle changes for color inputs.
11093 if (!input && (input = _self.colorInputs[cfg])) {
11094 var color = ev.value.replace(/\s+/g, '').
11095 replace(/^rgba\(/, '').replace(/\)$/, '');
11097 color = color.split(',');
11098 input.updateColor({
11099 red: color[0] / 255,
11100 green: color[1] / 255,
11101 blue: color[2] / 255,
11112 var tag = input.tagName.toLowerCase(),
11113 isInput = tag === 'select' || tag === 'input' || tag === 'textarea';
11116 if (input.type === 'checkbox' && input.checked !== ev.value) {
11117 input.checked = ev.value;
11119 if (input.type !== 'checkbox' && input.value !== ev.value) {
11120 input.value = ev.value;
11126 var classActive = ' ' + _self.className + 'configActive';
11128 if (input.hasAttribute('data-pwConfigToggle')) {
11129 var inputActive = input.className.indexOf(classActive) !== -1;
11131 if (ev.value && !inputActive) {
11132 input.className += classActive;
11133 } else if (!ev.value && inputActive) {
11134 input.className = input.className.replace(classActive, '');
11138 var classActive = ' ' + _self.className + 'configActive',
11139 prevValElem = _self.inputValues[cfg + '_' + ev.previousValue],
11140 valElem = _self.inputValues[cfg + '_' + ev.value];
11142 if (prevValElem && prevValElem.className.indexOf(classActive) !== -1) {
11143 prevValElem.className = prevValElem.className.replace(classActive, '');
11146 if (valElem && valElem.className.indexOf(classActive) === -1) {
11147 valElem.className += classActive;
11152 * The <code>click</code> event handler for DOM elements associated to
11153 * PaintWeb configuration values. These elements rely on parent elements which
11154 * are associated to configuration properties.
11156 * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
11158 * @param {Event} ev The DOM Event object.
11160 this.configValueClick = function (ev) {
11161 var pNode = this.parentNode,
11162 input = pNode._pwConfigParent,
11163 val = pNode.getAttribute('data-pwConfigValue');
11165 if (!input || !input._pwConfigProperty) {
11169 ev.preventDefault();
11171 var className = ' ' + _self.classPrefix + 'configActive',
11172 groupRef = input._pwConfigGroupRef,
11173 group = input._pwConfigGroup,
11174 prop = input._pwConfigProperty,
11175 prevVal = groupRef[prop],
11176 prevValElem = _self.inputValues[group.replace('.', '_') + '_' + prop
11179 if (prevVal == val) {
11183 if (prevValElem && prevValElem.className.indexOf(className) !== -1) {
11184 prevValElem.className = prevValElem.className.replace(className, '');
11187 groupRef[prop] = val;
11189 if (pNode.className.indexOf(className) === -1) {
11190 pNode.className += className;
11193 app.events.dispatch(new appEvent.configChange(val, prevVal, prop, group,
11198 * The <code>change</code> event handler for input elements associated to
11199 * PaintWeb configuration properties.
11201 * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
11203 this.configInputChange = function () {
11204 if (!this._pwConfigProperty) {
11208 var val = this.type === 'checkbox' ? this.checked : this.value,
11209 groupRef = this._pwConfigGroupRef,
11210 group = this._pwConfigGroup,
11211 prop = this._pwConfigProperty,
11212 prevVal = groupRef[prop];
11214 if (this.getAttribute('type') === 'number') {
11215 val = parseInt(val);
11216 if (val != this.value) {
11221 if (val == prevVal) {
11225 groupRef[prop] = val;
11227 app.events.dispatch(new appEvent.configChange(val, prevVal, prop, group,
11232 * The <code>click</code> event handler for DOM elements associated to boolean
11233 * configuration properties. These elements only toggle the true/false value
11234 * of the configuration property.
11236 * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
11238 * @param {Event} ev The DOM Event object.
11240 this.configToggleClick = function (ev) {
11241 var className = ' ' + _self.classPrefix + 'configActive',
11242 pNode = this.parentNode,
11243 groupRef = pNode._pwConfigGroupRef,
11244 group = pNode._pwConfigGroup,
11245 prop = pNode._pwConfigProperty,
11246 elemActive = pNode.className.indexOf(className) !== -1;
11248 ev.preventDefault();
11250 groupRef[prop] = !groupRef[prop];
11252 if (groupRef[prop] && !elemActive) {
11253 pNode.className += className;
11254 } else if (!groupRef[prop] && elemActive) {
11255 pNode.className = pNode.className.replace(className, '');
11258 app.events.dispatch(new appEvent.configChange(groupRef[prop],
11259 !groupRef[prop], prop, group, groupRef));
11263 * The <code>shadowAllow</code> application event handler. This method
11264 * shows/hide the shadow tab when shadows are allowed/disallowed.
11266 * @param {pwlib.appEvent.shadowAllow} ev The application event object.
11268 this.shadowAllow = function (ev) {
11269 if ('shadow' in _self.tabPanels.main.tabs) {
11271 _self.tabPanels.main.tabShow('shadow');
11273 _self.tabPanels.main.tabHide('shadow');
11279 * The <code>clipboardUpdate</code> application event handler. The GUI element
11280 * associated to the <code>clipboardPaste</code> command is updated to be
11281 * disabled/enabled depending on the event.
11283 * @param {pwlib.appEvent.clipboardUpdate} ev The application event object.
11285 this.clipboardUpdate = function (ev) {
11286 var classDisabled = ' ' + _self.classPrefix + 'disabled',
11288 elems = [_self.commands.clipboardPaste,
11289 _self.elems.selTab_clipboardPaste];
11291 for (var i = 0, n = elems.length; i < n; i++) {
11297 elemEnabled = elem.className.indexOf(classDisabled) === -1;
11299 if (!ev.data && elemEnabled) {
11300 elem.className += classDisabled;
11301 } else if (ev.data && !elemEnabled) {
11302 elem.className = elem.className.replace(classDisabled, '');
11308 * The <code>selectionChange</code> application event handler. The GUI
11309 * elements associated to the <code>selectionCut</code> and
11310 * <code>selectionCopy</code> commands are updated to be disabled/enabled
11311 * depending on the event.
11313 * @param {pwlib.appEvent.selectionChange} ev The application event object.
11315 this.selectionChange = function (ev) {
11316 var classDisabled = ' ' + _self.classPrefix + 'disabled',
11318 elems = [_self.commands.selectionCut, _self.commands.selectionCopy,
11319 _self.elems.selTab_selectionCut, _self.elems.selTab_selectionCopy,
11320 _self.commands.selectionDelete, _self.commands.selectionFill,
11321 _self.commands.selectionCrop];
11323 for (var i = 0, n = elems.length; i < n; i++) {
11329 elemEnabled = elem.className.indexOf(classDisabled) === -1;
11331 if (ev.state === ev.STATE_NONE && elemEnabled) {
11332 elem.className += classDisabled;
11333 } else if (ev.state === ev.STATE_SELECTED && !elemEnabled) {
11334 elem.className = elem.className.replace(classDisabled, '');
11340 * Show the graphical user interface.
11342 * <p>This method dispatches the {@link pwlib.appEvent.guiShow} application
11345 this.show = function () {
11346 var placeholder = config.guiPlaceholder,
11347 className = this.classPrefix + 'placeholder',
11348 re = new RegExp('\\b' + className);
11350 if (!re.test(placeholder.className)) {
11351 placeholder.className += ' ' + className;
11354 placeholder.focus();
11356 app.events.dispatch(new appEvent.guiShow());
11360 * Hide the graphical user interface.
11362 * <p>This method dispatches the {@link pwlib.appEvent.guiHide} application
11365 this.hide = function () {
11366 var placeholder = config.guiPlaceholder,
11367 re = new RegExp('\\b' + this.classPrefix + 'placeholder', 'g');
11369 placeholder.className = placeholder.className.replace(re, '');
11371 app.events.dispatch(new appEvent.guiHide());
11375 * The application destroy event handler. This method is invoked by the main
11376 * PaintWeb application when the instance is destroyed, for the purpose of
11377 * cleaning-up the GUI-related things from the document add by the current
11382 this.destroy = function () {
11383 var placeholder = config.guiPlaceholder;
11385 while(placeholder.hasChildNodes()) {
11386 placeholder.removeChild(placeholder.firstChild);
11392 * @class A floating panel GUI element.
11396 * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
11398 * @param {Element} container Reference to the DOM element you want to transform
11399 * into a floating panel.
11401 pwlib.guiFloatingPanel = function (gui, container) {
11403 appEvent = pwlib.appEvent,
11404 cStyle = container.style,
11406 guiPlaceholder = gui.app.config.guiPlaceholder,
11407 lang = gui.app.lang,
11408 panels = gui.floatingPanels,
11412 // These hold the mouse starting location during the drag operation.
11415 // These hold the panel starting location during the drag operation.
11419 * Panel state: hidden.
11422 this.STATE_HIDDEN = 0;
11425 * Panel state: visible.
11428 this.STATE_VISIBLE = 1;
11431 * Panel state: minimized.
11434 this.STATE_MINIMIZED = 3;
11437 * Panel state: the user is dragging the floating panel.
11440 this.STATE_DRAGGING = 4;
11443 * Tells the state of the floating panel: hidden/minimized/visible or if it's
11450 * Floating panel ID. This is the ID used in the
11451 * <var>data-pwFloatingPanel</var> element attribute.
11457 * Reference to the floating panel element.
11460 this.container = container;
11463 * The viewport element. This element is the first parent element which has
11464 * the style.overflow set to "auto" or "scroll".
11467 this.viewport = null;
11470 * Custom application events interface.
11471 * @type pwlib.appEvents
11473 this.events = null;
11476 * The panel content element.
11479 this.content = null;
11481 // The initial viewport scroll position.
11482 var vScrollLeft = 0, vScrollTop = 0,
11483 btn_close = null, btn_minimize = null;
11486 * Initialize the floating panel.
11490 _self.events = new pwlib.appEvents(_self);
11492 _self.id = _self.container.getAttribute('data-pwFloatingPanel');
11494 var ttl = _self.container.getElementsByTagName('h1')[0],
11495 content = _self.container.getElementsByTagName('div')[0],
11496 cs = win.getComputedStyle(_self.container, null),
11497 zIndex = parseInt(cs.zIndex);
11499 cStyle.zIndex = cs.zIndex;
11501 if (zIndex > panels.zIndex_) {
11502 panels.zIndex_ = zIndex;
11505 _self.container.className += ' ' + gui.classPrefix + 'floatingPanel ' +
11506 gui.classPrefix + 'floatingPanel_' + _self.id;
11509 content.className += ' ' + gui.classPrefix + 'floatingPanel_content';
11510 _self.content = content;
11512 // setup the title element
11513 ttl.className += ' ' + gui.classPrefix + 'floatingPanel_title';
11514 ttl.replaceChild(doc.createTextNode(lang.floatingPanels[_self.id]),
11517 ttl.addEventListener('mousedown', ev_mousedown, false);
11519 // allow auto-hide for the panel
11520 if (_self.container.getAttribute('data-pwPanelHide') === 'true') {
11523 _self.state = _self.STATE_VISIBLE;
11526 // Find the viewport parent element.
11527 var pNode = _self.container.parentNode,
11530 while (!found && pNode) {
11531 if (pNode.nodeName.toLowerCase() === 'html') {
11536 cs = win.getComputedStyle(pNode, null);
11537 if (cs && (cs.overflow === 'scroll' || cs.overflow === 'auto')) {
11540 pNode = pNode.parentNode;
11544 _self.viewport = found;
11546 // add the panel minimize button.
11547 btn_minimize = doc.createElement('a');
11548 btn_minimize.href = '#';
11549 btn_minimize.title = lang.floatingPanelMinimize;
11550 btn_minimize.className = gui.classPrefix + 'floatingPanel_minimize';
11551 btn_minimize.addEventListener('click', ev_minimize, false);
11552 btn_minimize.appendChild(doc.createTextNode(btn_minimize.title));
11554 _self.container.insertBefore(btn_minimize, content);
11556 // add the panel close button.
11557 btn_close = doc.createElement('a');
11558 btn_close.href = '#';
11559 btn_close.title = lang.floatingPanelClose;
11560 btn_close.className = gui.classPrefix + 'floatingPanel_close';
11561 btn_close.addEventListener('click', ev_close, false);
11562 btn_close.appendChild(doc.createTextNode(btn_close.title));
11564 _self.container.insertBefore(btn_close, content);
11566 // setup the panel resize handle.
11567 if (_self.container.getAttribute('data-pwPanelResizable') === 'true') {
11568 var resizeHandle = doc.createElement('div');
11569 resizeHandle.className = gui.classPrefix + 'floatingPanel_resizer';
11570 _self.container.appendChild(resizeHandle);
11571 _self.resizer = new pwlib.guiResizer(gui, resizeHandle, _self.container);
11576 * The <code>click</code> event handler for the panel Minimize button element.
11578 * <p>This method dispatches the {@link
11579 * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11582 * @param {Event} ev The DOM Event object.
11584 function ev_minimize (ev) {
11585 ev.preventDefault();
11588 var classMinimized = ' ' + gui.classPrefix + 'floatingPanel_minimized';
11590 if (_self.state === _self.STATE_MINIMIZED) {
11591 _self.state = _self.STATE_VISIBLE;
11593 this.title = lang.floatingPanelMinimize;
11594 this.className = gui.classPrefix + 'floatingPanel_minimize';
11595 this.replaceChild(doc.createTextNode(this.title), this.firstChild);
11597 if (_self.container.className.indexOf(classMinimized) !== -1) {
11598 _self.container.className
11599 = _self.container.className.replace(classMinimized, '');
11602 } else if (_self.state === _self.STATE_VISIBLE) {
11603 _self.state = _self.STATE_MINIMIZED;
11605 this.title = lang.floatingPanelRestore;
11606 this.className = gui.classPrefix + 'floatingPanel_restore';
11607 this.replaceChild(doc.createTextNode(this.title), this.firstChild);
11609 if (_self.container.className.indexOf(classMinimized) === -1) {
11610 _self.container.className += classMinimized;
11614 _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11616 _self.bringOnTop();
11620 * The <code>click</code> event handler for the panel Close button element.
11621 * This hides the floating panel.
11623 * <p>This method dispatches the {@link
11624 * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11627 * @param {Event} ev The DOM Event object.
11629 function ev_close (ev) {
11630 ev.preventDefault();
11632 guiPlaceholder.focus();
11636 * The <code>mousedown</code> event handler. This is invoked when you start
11637 * dragging the floating panel.
11639 * <p>This method dispatches the {@link
11640 * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11643 * @param {Event} ev The DOM Event object.
11645 function ev_mousedown (ev) {
11646 _self.state = _self.STATE_DRAGGING;
11651 var cs = win.getComputedStyle(_self.container, null);
11653 ptop = parseInt(cs.top);
11654 pleft = parseInt(cs.left);
11656 if (_self.viewport) {
11657 vScrollLeft = _self.viewport.scrollLeft;
11658 vScrollTop = _self.viewport.scrollTop;
11661 _self.bringOnTop();
11663 doc.addEventListener('mousemove', ev_mousemove, false);
11664 doc.addEventListener('mouseup', ev_mouseup, false);
11666 _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11668 if (ev.preventDefault) {
11669 ev.preventDefault();
11674 * The <code>mousemove</code> event handler. This performs the actual move of
11675 * the floating panel.
11678 * @param {Event} ev The DOM Event object.
11680 function ev_mousemove (ev) {
11681 var x = pleft + ev.clientX - mx,
11682 y = ptop + ev.clientY - my;
11684 if (_self.viewport) {
11685 if (_self.viewport.scrollLeft !== vScrollLeft) {
11686 x += _self.viewport.scrollLeft - vScrollLeft;
11688 if (_self.viewport.scrollTop !== vScrollTop) {
11689 y += _self.viewport.scrollTop - vScrollTop;
11693 cStyle.left = x + 'px';
11694 cStyle.top = y + 'px';
11698 * The <code>mouseup</code> event handler. This ends the panel drag operation.
11700 * <p>This method dispatches the {@link
11701 * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11704 * @param {Event} ev The DOM Event object.
11706 function ev_mouseup (ev) {
11707 if (_self.container.className.indexOf(' ' + gui.classPrefix
11708 + 'floatingPanel_minimized') !== -1) {
11709 _self.state = _self.STATE_MINIMIZED;
11711 _self.state = _self.STATE_VISIBLE;
11714 doc.removeEventListener('mousemove', ev_mousemove, false);
11715 doc.removeEventListener('mouseup', ev_mouseup, false);
11717 guiPlaceholder.focus();
11718 _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11722 * Bring the panel to the top. This method makes sure the current floating
11723 * panel is visible.
11725 this.bringOnTop = function () {
11726 panels.zIndex_ += zIndex_step;
11727 cStyle.zIndex = panels.zIndex_;
11733 * <p>This method dispatches the {@link
11734 * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11736 this.hide = function () {
11737 cStyle.display = 'none';
11738 _self.state = _self.STATE_HIDDEN;
11739 _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11745 * <p>This method dispatches the {@link
11746 * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11748 this.show = function () {
11749 if (_self.state === _self.STATE_VISIBLE) {
11753 cStyle.display = 'block';
11754 _self.state = _self.STATE_VISIBLE;
11756 var classMinimized = ' ' + gui.classPrefix + 'floatingPanel_minimized';
11758 if (_self.container.className.indexOf(classMinimized) !== -1) {
11759 _self.container.className
11760 = _self.container.className.replace(classMinimized, '');
11762 btn_minimize.className = gui.classPrefix + 'floatingPanel_minimize';
11763 btn_minimize.title = lang.floatingPanelMinimize;
11764 btn_minimize.replaceChild(doc.createTextNode(btn_minimize.title),
11765 btn_minimize.firstChild);
11768 _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11770 _self.bringOnTop();
11774 * Toggle the panel visibility.
11776 * <p>This method dispatches the {@link
11777 * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11779 this.toggle = function () {
11780 if (_self.state === _self.STATE_VISIBLE || _self.state ===
11781 _self.STATE_MINIMIZED) {
11792 * @class The state change event for the floating panel. This event is fired
11793 * when the floating panel changes its state. This event is not cancelable.
11795 * @augments pwlib.appEvent
11797 * @param {Number} state The floating panel state.
11799 pwlib.appEvent.guiFloatingPanelStateChange = function (state) {
11801 * Panel state: hidden.
11804 this.STATE_HIDDEN = 0;
11807 * Panel state: visible.
11810 this.STATE_VISIBLE = 1;
11813 * Panel state: minimized.
11816 this.STATE_MINIMIZED = 3;
11819 * Panel state: the user is dragging the floating panel.
11822 this.STATE_DRAGGING = 4;
11825 * The current floating panel state.
11828 this.state = state;
11830 pwlib.appEvent.call(this, 'guiFloatingPanelStateChange');
11834 * @class Resize handler.
11838 * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
11840 * @param {Element} resizeHandle Reference to the resize handle DOM element.
11841 * This is the element users will be able to drag to achieve the resize effect
11842 * on the <var>container</var> element.
11844 * @param {Element} container Reference to the container DOM element. This is
11845 * the element users will be able to resize using the <var>resizeHandle</var>
11848 pwlib.guiResizer = function (gui, resizeHandle, container) {
11850 cStyle = container.style,
11852 guiResizeEnd = pwlib.appEvent.guiResizeEnd,
11853 guiResizeStart = pwlib.appEvent.guiResizeStart,
11857 * Custom application events interface.
11858 * @type pwlib.appEvents
11860 this.events = null;
11863 * The resize handle DOM element.
11866 this.resizeHandle = resizeHandle;
11869 * The container DOM element. This is the element that's resized by the user
11870 * when he/she drags the resize handle.
11873 this.container = container;
11876 * The viewport element. This element is the first parent element which has
11877 * the style.overflow set to "auto" or "scroll".
11880 this.viewport = null;
11883 * Tells if the user resizing the container now.
11886 this.resizing = false;
11888 // The initial position of the mouse.
11889 var mx = 0, my = 0;
11891 // The initial container dimensions.
11892 var cWidth = 0, cHeight = 0;
11894 // The initial viewport scroll position.
11895 var vScrollLeft = 0, vScrollTop = 0;
11898 * Initialize the resize functionality.
11902 _self.events = new pwlib.appEvents(_self);
11903 resizeHandle.addEventListener('mousedown', ev_mousedown, false);
11905 // Find the viewport parent element.
11906 var cs, pNode = _self.container.parentNode,
11908 while (!found && pNode) {
11909 if (pNode.nodeName.toLowerCase() === 'html') {
11914 cs = win.getComputedStyle(pNode, null);
11915 if (cs && (cs.overflow === 'scroll' || cs.overflow === 'auto')) {
11918 pNode = pNode.parentNode;
11922 _self.viewport = found;
11926 * The <code>mousedown</code> event handler. This starts the resize operation.
11928 * <p>This function dispatches the {@link pwlib.appEvent.guiResizeStart}
11932 * @param {Event} ev The DOM Event object.
11934 function ev_mousedown (ev) {
11938 var cs = win.getComputedStyle(_self.container, null);
11939 cWidth = parseInt(cs.width);
11940 cHeight = parseInt(cs.height);
11942 var cancel = _self.events.dispatch(new guiResizeStart(mx, my, cWidth,
11949 if (_self.viewport) {
11950 vScrollLeft = _self.viewport.scrollLeft;
11951 vScrollTop = _self.viewport.scrollTop;
11954 _self.resizing = true;
11955 doc.addEventListener('mousemove', ev_mousemove, false);
11956 doc.addEventListener('mouseup', ev_mouseup, false);
11958 if (ev.preventDefault) {
11959 ev.preventDefault();
11962 if (ev.stopPropagation) {
11963 ev.stopPropagation();
11968 * The <code>mousemove</code> event handler. This performs the actual resizing
11969 * of the <var>container</var> element.
11972 * @param {Event} ev The DOM Event object.
11974 function ev_mousemove (ev) {
11975 var w = cWidth + ev.clientX - mx,
11976 h = cHeight + ev.clientY - my;
11978 if (_self.viewport) {
11979 if (_self.viewport.scrollLeft !== vScrollLeft) {
11980 w += _self.viewport.scrollLeft - vScrollLeft;
11982 if (_self.viewport.scrollTop !== vScrollTop) {
11983 h += _self.viewport.scrollTop - vScrollTop;
11987 cStyle.width = w + 'px';
11988 cStyle.height = h + 'px';
11992 * The <code>mouseup</code> event handler. This ends the resize operation.
11994 * <p>This function dispatches the {@link pwlib.appEvent.guiResizeEnd} event.
11997 * @param {Event} ev The DOM Event object.
11999 function ev_mouseup (ev) {
12000 var cancel = _self.events.dispatch(new guiResizeEnd(ev.clientX, ev.clientY,
12001 parseInt(cStyle.width), parseInt(cStyle.height)));
12007 _self.resizing = false;
12008 doc.removeEventListener('mousemove', ev_mousemove, false);
12009 doc.removeEventListener('mouseup', ev_mouseup, false);
12016 * @class The GUI element resize start event. This event is cancelable.
12018 * @augments pwlib.appEvent
12020 * @param {Number} x The mouse location on the x-axis.
12021 * @param {Number} y The mouse location on the y-axis.
12022 * @param {Number} width The element width.
12023 * @param {Number} height The element height.
12025 pwlib.appEvent.guiResizeStart = function (x, y, width, height) {
12027 * The mouse location on the x-axis.
12033 * The mouse location on the y-axis.
12039 * The element width.
12042 this.width = width;
12045 * The element height.
12048 this.height = height;
12050 pwlib.appEvent.call(this, 'guiResizeStart', true);
12054 * @class The GUI element resize end event. This event is cancelable.
12056 * @augments pwlib.appEvent
12058 * @param {Number} x The mouse location on the x-axis.
12059 * @param {Number} y The mouse location on the y-axis.
12060 * @param {Number} width The element width.
12061 * @param {Number} height The element height.
12063 pwlib.appEvent.guiResizeEnd = function (x, y, width, height) {
12065 * The mouse location on the x-axis.
12071 * The mouse location on the y-axis.
12077 * The element width.
12080 this.width = width;
12083 * The element height.
12086 this.height = height;
12088 pwlib.appEvent.call(this, 'guiResizeEnd', true);
12092 * @class The tabbed panel GUI component.
12096 * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
12098 * @param {Element} panel Reference to the panel DOM element.
12100 pwlib.guiTabPanel = function (gui, panel) {
12102 appEvent = pwlib.appEvent,
12104 lang = gui.app.lang;
12107 * Custom application events interface.
12108 * @type pwlib.appEvents
12110 this.events = null;
12113 * Panel ID. The ID is the same as the data-pwTabPanel attribute value of the
12114 * panel DOM element .
12121 * Holds references to the DOM element of each tab and tab button.
12127 * Reference to the tab buttons DOM element.
12130 this.tabButtons = null;
12133 * The panel container DOM element.
12136 this.container = panel;
12139 * Holds the ID of the currently active tab.
12145 * Holds the ID of the previously active tab.
12150 var prevTabId_ = null;
12153 * Initialize the toolbar functionality.
12157 _self.events = new pwlib.appEvents(_self);
12158 _self.id = _self.container.getAttribute('data-pwTabPanel');
12160 // Add two class names, the generic .paintweb_tabPanel and another class
12161 // name specific to the current tab panel: .paintweb_tabPanel_id.
12162 _self.container.className += ' ' + gui.classPrefix + 'tabPanel'
12163 + ' ' + gui.classPrefix + 'tabPanel_' + _self.id;
12165 var tabButtons = doc.createElement('ul'),
12167 tabDefault = _self.container.getAttribute('data-pwTabDefault') || null,
12168 childNodes = _self.container.childNodes,
12169 type = Node.ELEMENT_NODE,
12174 tabButtons.className = gui.classPrefix + 'tabsList';
12176 // Find all the tabs in the current panel container element.
12177 for (var i = 0; elem = childNodes[i]; i++) {
12178 if (elem.nodeType !== type) {
12182 // A tab is any element with a given data-pwTab attribute.
12183 tabId = elem.getAttribute('data-pwTab');
12188 // two class names, the generic .paintweb_tab and the tab-specific class
12189 // name .paintweb_tabPanelId_tabId.
12190 elem.className += ' ' + gui.classPrefix + 'tab ' + gui.classPrefix
12191 + _self.id + '_' + tabId;
12193 tabButton = doc.createElement('li');
12194 tabButton._pwTab = tabId;
12196 anchor = doc.createElement('a');
12198 anchor.addEventListener('click', ev_tabClick, false);
12200 if (_self.id in lang.tabs) {
12201 anchor.title = lang.tabs[_self.id][tabId + 'Title'] ||
12202 lang.tabs[_self.id][tabId];
12203 anchor.appendChild(doc.createTextNode(lang.tabs[_self.id][tabId]));
12206 if ((tabDefault && tabId === tabDefault) ||
12207 (!tabDefault && !_self.tabId)) {
12208 _self.tabId = tabId;
12209 tabButton.className = gui.classPrefix + 'tabActive';
12211 prevTabId_ = tabId;
12212 elem.style.display = 'none';
12215 // automatically hide the tab
12216 if (elem.getAttribute('data-pwTabHide') === 'true') {
12217 tabButton.style.display = 'none';
12220 _self.tabs[tabId] = {container: elem, button: tabButton};
12222 tabButton.appendChild(anchor);
12223 tabButtons.appendChild(tabButton);
12226 _self.tabButtons = tabButtons;
12227 _self.container.appendChild(tabButtons);
12231 * The <code>click</code> event handler for tab buttons. This function simply
12232 * activates the tab the user clicked.
12235 * @param {Event} ev The DOM Event object.
12237 function ev_tabClick (ev) {
12238 ev.preventDefault();
12239 _self.tabActivate(this.parentNode._pwTab);
12243 * Activate a tab by ID.
12245 * <p>This method dispatches the {@link pwlib.appEvent.guiTabActivate} event.
12247 * @param {String} tabId The ID of the tab you want to activate.
12248 * @returns {Boolean} True if the tab has been activated successfully, or
12251 this.tabActivate = function (tabId) {
12252 if (!tabId || !(tabId in this.tabs)) {
12254 } else if (tabId === this.tabId) {
12258 var ev = new appEvent.guiTabActivate(tabId, this.tabId),
12259 cancel = this.events.dispatch(ev),
12267 // Deactivate the currently active tab.
12268 if (this.tabId in this.tabs) {
12269 elem = this.tabs[this.tabId].container;
12270 elem.style.display = 'none';
12271 tabButton = this.tabs[this.tabId].button;
12272 tabButton.className = '';
12273 prevTabId_ = this.tabId;
12276 // Activate the new tab.
12277 elem = this.tabs[tabId].container;
12278 elem.style.display = '';
12279 tabButton = this.tabs[tabId].button;
12280 tabButton.className = gui.classPrefix + 'tabActive';
12281 tabButton.style.display = ''; // make sure the tab is not hidden
12282 tabButton.firstChild.focus();
12283 this.tabId = tabId;
12289 * Hide a tab by ID.
12291 * @param {String} tabId The ID of the tab you want to hide.
12292 * @returns {Boolean} True if the tab has been hidden successfully, or false
12295 this.tabHide = function (tabId) {
12296 if (!(tabId in this.tabs)) {
12300 if (this.tabId === tabId) {
12301 this.tabActivate(prevTabId_);
12304 this.tabs[tabId].button.style.display = 'none';
12310 * Show a tab by ID.
12312 * @param {String} tabId The ID of the tab you want to show.
12313 * @returns {Boolean} True if the tab has been displayed successfully, or
12316 this.tabShow = function (tabId) {
12317 if (!(tabId in this.tabs)) {
12321 this.tabs[tabId].button.style.display = '';
12330 * @class The GUI tab activation event. This event is cancelable.
12332 * @augments pwlib.appEvent
12334 * @param {String} tabId The ID of the tab being activated.
12335 * @param {String} prevTabId The ID of the previously active tab.
12337 pwlib.appEvent.guiTabActivate = function (tabId, prevTabId) {
12339 * The ID of the tab being activated.
12342 this.tabId = tabId;
12345 * The ID of the previously active tab.
12348 this.prevTabId = prevTabId;
12350 pwlib.appEvent.call(this, 'guiTabActivate', true);
12354 * @class The color input GUI component.
12358 * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
12360 * @param {Element} input Reference to the DOM input element. This can be
12361 * a span, a div, or any other tag.
12363 pwlib.guiColorInput = function (gui, input) {
12366 config = gui.app.config,
12368 MathRound = Math.round,
12369 lang = gui.app.lang;
12372 * Color input ID. The ID is the same as the data-pwColorInput attribute value
12373 * of the DOM input element .
12380 * The color input element DOM reference.
12384 this.input = input;
12387 * The configuration property to which this color input is attached to.
12390 this.configProperty = null;
12393 * The configuration group to which this color input is attached to.
12396 this.configGroup = null;
12399 * Reference to the configuration object which holds the color input value.
12402 this.configGroupRef = null;
12405 * Holds the current color displayed by the input.
12409 this.color = {red: 0, green: 0, blue: 0, alpha: 0};
12412 * Initialize the color input functionality.
12416 var cfgAttr = _self.input.getAttribute('data-pwColorInput'),
12417 cfgNoDots = cfgAttr.replace('.', '_'),
12418 cfgArray = cfgAttr.split('.'),
12419 cfgProp = cfgArray.pop(),
12420 cfgGroup = cfgArray.join('.'),
12421 cfgGroupRef = config,
12422 langGroup = lang.inputs,
12423 labelElem = _self.input.parentNode,
12424 anchor = doc.createElement('a'),
12427 for (var i = 0, n = cfgArray.length; i < n; i++) {
12428 cfgGroupRef = cfgGroupRef[cfgArray[i]];
12429 langGroup = langGroup[cfgArray[i]];
12432 _self.configProperty = cfgProp;
12433 _self.configGroup = cfgGroup;
12434 _self.configGroupRef = cfgGroupRef;
12436 _self.id = cfgNoDots;
12438 _self.input.className += ' ' + gui.classPrefix + 'colorInput'
12439 + ' ' + gui.classPrefix + _self.id;
12441 labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]),
12442 labelElem.firstChild);
12444 color = _self.configGroupRef[_self.configProperty];
12445 color = color.replace(/\s+/g, '').replace(/^rgba\(/, '').replace(/\)$/, '');
12446 color = color.split(',');
12447 _self.color.red = color[0] / 255;
12448 _self.color.green = color[1] / 255;
12449 _self.color.blue = color[2] / 255;
12450 _self.color.alpha = color[3];
12452 anchor.style.backgroundColor = 'rgb(' + color[0] + ',' + color[1] + ','
12454 anchor.style.opacity = color[3];
12457 anchor.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
12458 anchor.appendChild(doc.createTextNode(lang.inputs.colorInputAnchorContent));
12459 anchor.addEventListener('click', ev_input_click, false);
12461 _self.input.replaceChild(anchor, _self.input.firstChild);
12465 * The <code>click</code> event handler for the color input element. This
12466 * function shows/hides the Color Mixer panel.
12469 * @param {Event} ev The DOM Event object.
12471 function ev_input_click (ev) {
12472 ev.preventDefault();
12475 colormixer = gui.app.extensions.colormixer;
12478 if (!colormixer.targetInput || colormixer.targetInput.id !== _self.id) {
12481 configProperty: _self.configProperty,
12482 configGroup: _self.configGroup,
12483 configGroupRef: _self.configGroupRef,
12484 show: colormixer_show,
12485 hide: colormixer_hide
12494 * The color mixer <code>show</code> event handler. This function is invoked
12495 * when the color mixer is shown.
12498 function colormixer_show () {
12499 var classActive = ' ' + gui.classPrefix + 'colorInputActive',
12500 elemActive = _self.input.className.indexOf(classActive) !== -1;
12503 _self.input.className += classActive;
12508 * The color mixer <code>hide</code> event handler. This function is invoked
12509 * when the color mixer is hidden.
12512 function colormixer_hide () {
12513 var classActive = ' ' + gui.classPrefix + 'colorInputActive',
12514 elemActive = _self.input.className.indexOf(classActive) !== -1;
12517 _self.input.className = _self.input.className.replace(classActive, '');
12522 * Update color. This method allows the change of the color values associated
12523 * to the current color input.
12525 * <p>This method is used by the color picker tool and by the global GUI
12526 * <code>configChange</code> application event handler.
12528 * @param {Object} color The new color values. The object must have four
12529 * properties: <var>red</var>, <var>green</var>, <var>blue</var> and
12530 * <var>alpha</var>. All values must be between 0 and 1.
12532 this.updateColor = function (color) {
12533 var anchor = _self.input.firstChild.style;
12535 anchor.opacity = color.alpha;
12536 anchor.backgroundColor = 'rgb(' + MathRound(color.red * 255) + ',' +
12537 MathRound(color.green * 255) + ',' +
12538 MathRound(color.blue * 255) + ')';
12539 _self.color.red = color.red;
12540 _self.color.green = color.green;
12541 _self.color.blue = color.blue;
12542 _self.color.alpha = color.alpha;
12548 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
12550 pwlib.fileCache['interfaces/default/layout.xhtml'] =
12551 "<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\"> <h1 class=\"paintweb_appTitle\">PaintWeb<\/h1> <div data-pwTabPanel=\"main\" data-pwTabDefault=\"main\"> <div data-pwTab=\"main\"> <ul id=\"tools\"> <li data-pwCommand=\"historyUndo\">Undo<\/li> <li data-pwCommand=\"historyRedo\">Redo<\/li> <li class=\"paintweb_toolSeparator\"> <\/li> <li data-pwCommand=\"imageClear\">Clear image<\/li> <li data-pwCommand=\"imageSave\">Save image<\/li> <li data-pwTool=\"insertimg\">Insert image<\/li> <li class=\"paintweb_toolSeparator\"> <\/li> <li data-pwCommand=\"selectionCut\">Cut selection<\/li> <li data-pwCommand=\"selectionCopy\">Copy selection<\/li> <li data-pwCommand=\"clipboardPaste\">Clipboard paste<\/li> <li class=\"paintweb_toolSeparator\"> <\/li> <li data-pwTool=\"cpicker\">Color picker<\/li> <li class=\"paintweb_toolsWrap\"> <\/li> <li data-pwTool=\"selection\">Selection<\/li> <li data-pwTool=\"hand\">Hand<\/li> <li class=\"paintweb_toolSeparator\"> <\/li> <li data-pwTool=\"rectangle\">Rectangle<\/li> <li data-pwTool=\"ellipse\">Ellipse<\/li> <li data-pwTool=\"polygon\">Polygon<\/li> <li data-pwTool=\"line\">Line<\/li> <li data-pwTool=\"bcurve\">B\u00e9zier curve<\/li> <li data-pwTool=\"text\">Text<\/li> <li data-pwTool=\"pencil\">Pencil<\/li> <li class=\"paintweb_toolSeparator\"> <\/li> <li data-pwTool=\"eraser\">Eraser<\/li> <\/ul> <div class=\"paintweb_strokeFillStyles\"> <p class=\"paintweb_opt_fillStyle\">Fill <span data-pwColorInput=\"fillStyle\"> <\/span> <\/p> <p class=\"paintweb_opt_strokeStyle\">Stroke <span data-pwColorInput=\"strokeStyle\"> <\/span> <\/p> <\/div> <\/div> <div data-pwTab=\"line\" data-pwTabHide=\"true\"> <p class=\"paintweb_opt_lineWidth\"><label>Line width <input data-pwConfig=\"line.lineWidth\" type=\"number\" min=\"1\" value=\"1\" \/><\/label><\/p> <p class=\"paintweb_opt_miterLimit\"><label>Miter limit <input data-pwConfig=\"line.miterLimit\" type=\"number\" min=\"1\" value=\"10\" \/><\/label><\/p> <div data-pwConfig=\"line.lineCap\"> <p>Line cap<\/p> <div data-pwConfigValue=\"butt\">Butt<\/div> <div data-pwConfigValue=\"square\">Square<\/div> <div data-pwConfigValue=\"round\">Round<\/div> <\/div> <div data-pwConfig=\"line.lineJoin\"> <p>Line join<\/p> <div data-pwConfigValue=\"miter\">Miter<\/div> <div data-pwConfigValue=\"round\">Round<\/div> <div data-pwConfigValue=\"bevel\">Bevel<\/div> <\/div> <div data-pwConfig=\"shapeType\"> <p>Shape type<\/p> <div data-pwConfigValue=\"both\">Both<\/div> <div data-pwConfigValue=\"fill\">Fill<\/div> <div data-pwConfigValue=\"stroke\">Stroke<\/div> <\/div> <\/div> <div data-pwTab=\"selection\" data-pwTabHide=\"true\"> <p data-pwId=\"selTab_selectionCut\">Cut selection<\/p> <p data-pwId=\"selTab_selectionCopy\">Copy selection<\/p> <p data-pwId=\"selTab_clipboardPaste\">Clipboard paste<\/p> <p data-pwCommand=\"selectionCrop\">Crop selection<\/p> <p data-pwCommand=\"selectionDelete\">Delete selection<\/p> <p data-pwCommand=\"selectionFill\">Fill selection<\/p> <p class=\"paintweb_opt_selectionTransparent\"> <label><input data-pwConfig=\"selection.transparent\" type=\"checkbox\" value=\"1\" checked=\"checked\" \/> Transparent background<\/label> <\/p> <p class=\"paintweb_opt_selectionTransform\"> <label><input data-pwConfig=\"selection.transform\" type=\"checkbox\" value=\"1\" \/> Transformation mode<\/label> <\/p> <\/div> <div data-pwTab=\"text\" data-pwTabHide=\"true\"> <p class=\"paintweb_opt_fontFamily\"> <label for=\"fontFamily\">Font family:<\/label> <select id=\"fontFamily\" data-pwConfig=\"text.fontFamily\"><\/select> <\/p> <p class=\"paintweb_opt_fontSize\"> <label for=\"fontSize\">Font size:<\/label> <input id=\"fontSize\" data-pwConfig=\"text.fontSize\" type=\"number\" min=\"6\" value=\"12\" \/> <\/p> <div data-pwConfigToggle=\"text.bold\">Bold<\/div> <div data-pwConfigToggle=\"text.italic\">Italic<\/div> <div data-pwConfig=\"text.textAlign\"> <p>Text alignment<\/p> <div data-pwConfigValue=\"left\">Left<\/div> <div data-pwConfigValue=\"center\">Center<\/div> <div data-pwConfigValue=\"right\">Right<\/div> <\/div> <p class=\"paintweb_opt_textString\"> <label>String <textarea id=\"textString\" rows=\"2\" cols=\"4\">Hello world!<\/textarea><\/label> <\/p> <\/div> <div data-pwTab=\"shadow\"> <p class=\"paintweb_opt_shadowEnable\"><label><input data-pwConfig=\"shadow.enable\" type=\"checkbox\" value=\"1\" \/> Draw shadows<\/label><\/p> <p class=\"paintweb_opt_shadowColor\">Color <span data-pwColorInput=\"shadow.shadowColor\"> <\/span> <\/p> <p class=\"paintweb_opt_shadowOffsetX\"> <label>Offset X <input data-pwConfig=\"shadow.shadowOffsetX\" type=\"number\" value=\"5\" \/> <\/label> <\/p> <p class=\"paintweb_opt_shadowOffsetY\"> <label>Offset Y <input data-pwConfig=\"shadow.shadowOffsetY\" type=\"number\" value=\"5\" \/> <\/label> <\/p> <p class=\"paintweb_opt_shadowBlur\"> <label>Blur <input data-pwConfig=\"shadow.shadowBlur\" type=\"number\" value=\"5\" min=\"0\" \/> <\/label> <\/p> <\/div> <p data-pwCommand=\"about\">About<\/p> <\/div> <div id=\"viewport\"> <div id=\"canvasContainer\"> <\/div> <div id=\"canvasResizer\">Resize the image Canvas<\/div> <\/div> <div class=\"paintweb_statusbar\"> <p id=\"imageSize\">WxH<\/p> <p id=\"statusZoom\" title=\"Zoom image\"> <label>Zoom: <input id=\"imageZoom\" type=\"number\" min=\"20\" max=\"400\" value=\"100\" step=\"10\" \/><\/label> <\/p> <p id=\"statusMessage\">Status<\/p> <\/div> <div data-pwFloatingPanel=\"colormixer\" data-pwPanelHide=\"true\"> <h1>Color mixer<\/h1> <div> <ol class=\"paintweb_colormixer_preview\"> <li id=\"colormixer_colorActive\"><span> <\/span> Active<\/li> <li id=\"colormixer_colorOld\"><span> <\/span> Old<\/li> <\/ol> <ol class=\"paintweb_colormixer_actions\"> <li id=\"colormixer_btn_accept\">Close<\/li> <li id=\"colormixer_btn_cancel\">Cancel<\/li> <li id=\"colormixer_btn_saveColor\">Save color<\/li> <li id=\"colormixer_btn_pickColor\">Pick color<\/li> <\/ol> <div data-pwTabPanel=\"colormixer_selector\" data-pwTabDefault=\"mixer\"> <div data-pwTab=\"mixer\"> <canvas id=\"colormixer_canvas\" width=\"200\" height=\"195\">Your browser does not support Canvas.<\/canvas> <div id=\"colormixer_controls\"> <span id=\"colormixer_chartDot\"><\/span> <span id=\"colormixer_slider\"><\/span> <\/div> <\/div> <div data-pwTab=\"cpalettes\"> <select id=\"colormixer_cpaletteInput\"><\/select> <div id=\"colormixer_cpaletteOutput\"><\/div> <\/div> <\/div> <ol class=\"paintweb_colormixer_hexalpha\"> <li><label>HEX <input id=\"ckey_hex\" value=\"#RRGGBB\" type=\"text\" maxlength=\"7\" pattern=\"#[a-f0-9]{6}\" \/><\/label> <\/li> <li><label>Alpha <input id=\"ckey_alpha\" value=\"100\" type=\"number\" min=\"0\" max=\"100\" step=\"1\" \/><\/label> <\/li> <\/ol> <form data-pwTabPanel=\"colormixer_inputs\" data-pwTabDefault=\"rgb\"> <ol data-pwTab=\"rgb\"> <li> <input name=\"ckey\" value=\"red\" type=\"radio\" \/> <label>Red <input name=\"ckey_red\" value=\"0\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" \/><\/label> <\/li> <li> <input name=\"ckey\" value=\"green\" type=\"radio\" \/> <label>Green <input name=\"ckey_green\" value=\"0\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" \/><\/label> <\/li> <li> <input name=\"ckey\" value=\"blue\" type=\"radio\" \/> <label>Blue <input name=\"ckey_blue\" value=\"0\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" \/><\/label> <\/li> <\/ol> <ol data-pwTab=\"hsv\"> <li> <input name=\"ckey\" value=\"hue\" type=\"radio\" \/> <label>Hue <input name=\"ckey_hue\" value=\"0\" type=\"number\" min=\"0\" max=\"360\" step=\"1\" \/><\/label> <\/li> <li> <input name=\"ckey\" value=\"sat\" type=\"radio\" \/> <label>Saturation <input name=\"ckey_sat\" value=\"0\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" \/><\/label> <\/li> <li> <input name=\"ckey\" value=\"val\" type=\"radio\" \/> <label>Value <input name=\"ckey_val\" value=\"0\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" \/><\/label> <\/li> <\/ol> <ol data-pwTab=\"lab\"> <li> <input name=\"ckey\" value=\"cie_l\" type=\"radio\" \/> <label>Lightness <input name=\"ckey_cie_l\" value=\"0\" type=\"number\" min=\"0\" max=\"100\" step=\"1\" \/><\/label> <\/li> <li> <input name=\"ckey\" value=\"cie_a\" type=\"radio\" \/> <label>a* <input name=\"ckey_cie_a\" value=\"0\" type=\"number\" min=\"-85\" max=\"94\" step=\"1\" \/><\/label> <\/li> <li> <input name=\"ckey\" value=\"cie_b\" type=\"radio\" \/> <label>b* <input name=\"ckey_cie_b\" value=\"0\" type=\"number\" min=\"-109\" max=\"95\" step=\"1\" \/><\/label> <\/li> <\/ol> <ol data-pwTab=\"cmyk\"> <li> <label>Cyan <input name=\"ckey_cyan\" value=\"0\" type=\"number\" min=\"0\" max=\"100\" step=\"1\" \/><\/label> <\/li> <li> <label>Magenta <input name=\"ckey_magenta\" value=\"0\" type=\"number\" min=\"0\" max=\"100\" step=\"1\" \/><\/label> <\/li> <li> <label>Yellow <input name=\"ckey_yellow\" value=\"0\" type=\"number\" min=\"0\" max=\"100\" step=\"1\" \/><\/label> <\/li> <li> <label>Key (Black) <input name=\"ckey_black\" value=\"0\" type=\"number\" min=\"0\" max=\"100\" step=\"1\" \/><\/label> <\/li> <\/ol> <\/form> <\/div> <\/div> <div data-pwFloatingPanel=\"about\" data-pwPanelHide=\"true\"> <h1>About<\/h1> <div> <ul> <li id=\"version\"><strong>Version:<\/strong> <\/li> <li><strong>Authors:<\/strong> <a href=\"http:\/\/www.robodesign.ro\">Marius and Mihai \u015eucan (ROBO Design)<\/a><\/li> <li><strong>Project site:<\/strong> <a href=\"http:\/\/code.google.com\/p\/paintweb\">code.google.com\/p\/paintweb<\/a><\/li> <li><strong>Code license:<\/strong> <a href=\"http:\/\/www.gnu.org\/licenses\/gpl-3.0.html\" title=\"GNU General Public License, version 3\">GPLv3<\/a><\/li> <\/ul> <p>The project is currently undergoing heavy development for the purpose of integrating it into <a href=\"http:\/\/www.moodle.org\">Moodle<\/a>.<\/p> <p>For user and developer documentation please check out the <a href=\"http:\/\/code.google.com\/p\/paintweb\">project site<\/a>.<\/p> <\/div> <\/div> <\/div>";
12553 * Copyright (C) 2008, 2009 Mihai Şucan
12555 * This file is part of PaintWeb.
12557 * PaintWeb is free software: you can redistribute it and/or modify
12558 * it under the terms of the GNU General Public License as published by
12559 * the Free Software Foundation, either version 3 of the License, or
12560 * (at your option) any later version.
12562 * PaintWeb is distributed in the hope that it will be useful,
12563 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12564 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12565 * GNU General Public License for more details.
12567 * You should have received a copy of the GNU General Public License
12568 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>.
12570 * $URL: http://code.google.com/p/paintweb $
12571 * $Date: 2009-07-28 21:43:08 +0300 $
12575 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
12576 * @fileOverview The main PaintWeb application code.
12580 * @class The PaintWeb application object.
12582 * @param {Window} [win=window] The window object to use.
12583 * @param {Document} [doc=document] The document object to use.
12585 function PaintWeb (win, doc) {
12596 * PaintWeb version.
12599 this.version = 0.9; //!
12602 * PaintWeb build date (YYYYMMDD).
12605 this.build = 20090728;
12608 * Holds all the PaintWeb configuration.
12616 * Holds all language strings used within PaintWeb.
12618 // Here we include a minimal set of strings, used in case the language file will
12621 "noComputedStyle": "Error: window.getComputedStyle is not available.",
12622 "noXMLHttpRequest": "Error: window.XMLHttpRequest is not available.",
12623 "noCanvasSupport": "Error: Your browser does not support Canvas.",
12624 "guiPlaceholderWrong": "Error: The config.guiPlaceholder property must " +
12625 "reference a DOM element!",
12626 "initHandlerMustBeFunction": "The first argument must be a function.",
12627 "noConfigFile": "Error: You must point to a configuration file by " +
12628 "setting the config.configFile property!",
12629 "failedConfigLoad": "Error: Failed loading the configuration file.",
12630 "failedLangLoad": "Error: Failed loading the language file."
12634 * Holds the buffer canvas and context references.
12637 this.buffer = {canvas: null, context: null};
12640 * Holds the current layer ID, canvas and context references.
12643 this.layer = {id: null, canvas: null, context: null};
12646 * The instance of the active tool object.
12650 * @see PaintWeb.config.toolDefault holds the ID of the tool which is
12651 * activated when the application loads.
12652 * @see PaintWeb#toolActivate Activate a drawing tool by ID.
12653 * @see PaintWeb#toolRegister Register a new drawing tool.
12654 * @see PaintWeb#toolUnregister Unregister a drawing tool.
12655 * @see pwlib.tools holds the drawing tools.
12660 * Holds references to DOM elements.
12668 * Holds the last recorded mouse coordinates and the button state (if it's
12674 this.mouse = {x: 0, y: 0, buttonDown: false};
12677 * Holds all the PaintWeb extensions.
12680 * @see PaintWeb#extensionRegister Register a new extension.
12681 * @see PaintWeb#extensionUnregister Unregister an extension.
12682 * @see PaintWeb.config.extensions Holds the list of extensions to be loaded
12683 * automatically when PaintWeb is initialized.
12685 this.extensions = {};
12688 * Holds all the PaintWeb commands. Each property in this object must
12689 * reference a simple function which can be executed by keyboard shortcuts
12690 * and/or GUI elements.
12693 * @see PaintWeb#commandRegister Register a new command.
12694 * @see PaintWeb#commandUnregister Unregister a command.
12696 this.commands = {};
12699 * The graphical user interface object instance.
12705 * The document element PaintWeb is working with.
12709 * @default document
12714 * The window object PaintWeb is working with.
12723 * Holds image information: width and height.
12743 * Image zoom level. This property holds the current image zoom level used
12744 * by the user for viewing the image.
12752 * Image scaling. The canvas elements are scaled from CSS using this value
12753 * as the scaling factor. This value is dependant on the browser rendering
12754 * resolution and on the user-defined image zoom level.
12763 * Resolution information.
12767 this.resolution = {
12769 * The DOM element holding information about the current browser rendering
12770 * settings (zoom / DPI).
12778 * The ID of the DOM element holding information about the current browser
12779 * rendering settings (zoom / DPI).
12783 * @default 'paintweb_resInfo'
12785 elemId: 'paintweb_resInfo',
12788 * The styling necessary for the DOM element.
12793 cssText: '@media screen and (resolution:96dpi){' +
12794 '#paintweb_resInfo{width:96px}}' +
12795 '@media screen and (resolution:134dpi){' +
12796 '#paintweb_resInfo{width:134px}}' +
12797 '@media screen and (resolution:200dpi){' +
12798 '#paintweb_resInfo{width:200px}}' +
12799 '@media screen and (resolution:300dpi){' +
12800 '#paintweb_resInfo{width:300px}}' +
12801 '#paintweb_resInfo{' +
12805 'position:fixed;' +
12807 'visibility:hidden;' +
12811 * Optimal DPI for the canvas elements.
12820 * The current DPI used by the browser for rendering the entire page.
12828 * The current zoom level used by the browser for rendering the entire page.
12836 * The scaling factor used by the browser for rendering the entire page. For
12837 * example, on Gecko using DPI 200 the scale factor is 2.
12847 * The image history.
12854 * History position.
12862 * The ImageDatas for each history state.
12871 * Tells if the browser supports the Canvas Shadows API.
12876 this.shadowSupported = true;
12879 * Tells if the current tool allows the drawing of shadows.
12884 this.shadowAllowed = true;
12887 * Image in the clipboard. This is used when some selection is copy/pasted.
12891 this.clipboard = false;
12894 * Application initialization state. This property can be in one of the
12895 * following states:
12898 * <li>{@link PaintWeb.INIT_NOT_STARTED} - The initialization is not
12901 * <li>{@link PaintWeb.INIT_STARTED} - The initialization process is
12904 * <li>{@link PaintWeb.INIT_DONE} - The initialization process has completed
12907 * <li>{@link PaintWeb.INIT_ERROR} - The initialization process has failed.
12911 * @default PaintWeb.INIT_NOT_STARTED
12913 this.initialized = PaintWeb.INIT_NOT_STARTED;
12916 * Custom application events object.
12918 * @type pwlib.appEvents
12920 this.events = null;
12923 * Unique ID for the current PaintWeb instance.
12930 * List of Canvas context properties to save and restore.
12932 * <p>When the Canvas is resized the state is lost. Using context.save/restore
12933 * state does work only in Opera. In Firefox/Gecko and WebKit saved states are
12934 * lost after resize, so there's no state to restore. As such, PaintWeb has
12935 * its own simple state save/restore mechanism. The property values are saved
12936 * into a JavaScript object.
12941 * @see PaintWeb#stateSave to save the canvas context state.
12942 * @see PaintWeb#stateRestore to restore a canvas context state.
12944 this.stateProperties = ['strokeStyle', 'fillStyle', 'globalAlpha',
12945 'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', 'shadowOffsetX',
12946 'shadowOffsetY', 'shadowBlur', 'shadowColor', 'globalCompositeOperation',
12947 'font', 'textAlign', 'textBaseline'];
12950 * Holds the keyboard event listener object.
12953 * @type pwlib.dom.KeyboardEventListener
12954 * @see pwlib.dom.KeyboardEventListener The class dealing with the
12955 * cross-browser differences in the DOM keyboard events.
12957 var kbListener_ = null;
12960 * Holds temporary state information during PaintWeb initialization.
12965 var temp_ = {onInit: null, toolsLoadQueue: 0, extensionsLoadQueue: 0};
12967 // Avoid global scope lookup.
12968 var MathAbs = Math.abs,
12969 MathFloor = Math.floor,
12970 MathMax = Math.max,
12971 MathMin = Math.min,
12972 MathRound = Math.round,
12978 * PaintWeb pre-initialization code. This runs when the PaintWeb instance is
12982 function preInit() {
12983 var d = new Date();
12985 // If PaintWeb is running directly from the source code, then the build date
12986 // is always today.
12987 if (_self.build === -1) {
12988 var dateArr = [d.getFullYear(), d.getMonth()+1, d.getDate()];
12990 if (dateArr[1] < 10) {
12991 dateArr[1] = '0' + dateArr[1];
12993 if (dateArr[2] < 10) {
12994 dateArr[2] = '0' + dateArr[2];
12997 _self.build = dateArr.join('');
13000 _self.UID = d.getMilliseconds() * MathRound(Math.random() * 100);
13001 _self.elems.head = doc.getElementsByTagName('head')[0] || doc.body;
13005 * Initialize PaintWeb.
13007 * <p>This method is asynchronous, meaning that it will return much sooner
13008 * before the application initialization is completed.
13010 * @param {Function} [handler] The <code>appInit</code> event handler. Your
13011 * event handler will be invoked automatically when PaintWeb completes
13012 * loading, or when an error occurs.
13014 * @returns {Boolean} True if the initialization has been started
13015 * successfully, or false if not.
13017 this.init = function (handler) {
13018 if (this.initialized === PaintWeb.INIT_DONE) {
13022 this.initialized = PaintWeb.INIT_STARTED;
13024 if (handler && typeof handler !== 'function') {
13025 throw new TypeError(lang.initHandlerMustBeFunction);
13028 temp_.onInit = handler;
13030 // Check Canvas support.
13031 if (!doc.createElement('canvas').getContext) {
13032 this.initError(lang.noCanvasSupport);
13036 // Basic functionality used within the Web application.
13037 if (!window.getComputedStyle) {
13038 this.initError(lang.noComputedStyle);
13042 if (!window.XMLHttpRequest) {
13043 this.initError(lang.noXMLHttpRequest);
13047 if (!this.config.configFile) {
13048 this.initError(lang.noConfigFile);
13052 if (typeof this.config.guiPlaceholder !== 'object' ||
13053 this.config.guiPlaceholder.nodeType !== Node.ELEMENT_NODE) {
13054 this.initError(lang.guiPlaceholderWrong);
13058 // Silently ignore any wrong value for the config.imageLoad property.
13059 if (typeof this.config.imageLoad !== 'object' ||
13060 this.config.imageLoad.nodeType !== Node.ELEMENT_NODE) {
13061 this.config.imageLoad = null;
13064 // JSON parser and serializer.
13065 if (!window.JSON) {
13066 this.scriptLoad(PaintWeb.baseFolder + 'includes/json2.js',
13067 this.jsonlibReady);
13069 this.jsonlibReady();
13076 * The <code>load</code> event handler for the JSON library script.
13079 this.jsonlibReady = function () {
13080 if (window.pwlib) {
13081 _self.pwlibReady();
13083 _self.scriptLoad(PaintWeb.baseFolder + 'includes/lib.js',
13089 * The <code>load</code> event handler for the PaintWeb library script.
13092 this.pwlibReady = function () {
13093 pwlib = window.pwlib;
13094 appEvent = pwlib.appEvent;
13096 // Create the custom application events object.
13097 _self.events = new pwlib.appEvents(_self);
13099 // Add the init event handler.
13100 if (typeof temp_.onInit === 'function') {
13101 _self.events.add('appInit', temp_.onInit);
13102 delete temp_.onInit;
13105 _self.configLoad();
13109 * Report an initialization error.
13111 * <p>This method dispatches the {@link pwlib.appEvent.appInit} event.
13115 * @param {String} msg The error message.
13117 * @see pwlib.appEvent.appInit
13119 this.initError = function (msg) {
13120 switch (this.initialized) {
13121 case PaintWeb.INIT_ERROR:
13122 case PaintWeb.INIT_DONE:
13123 case PaintWeb.INIT_NOT_STARTED:
13127 this.initialized = PaintWeb.INIT_ERROR;
13131 if (this.events && 'dispatch' in this.events &&
13132 appEvent && 'appInit' in appEvent) {
13134 ev = new appEvent.appInit(this.initialized, msg);
13135 this.events.dispatch(ev);
13137 } else if (typeof temp_.onInit === 'function') {
13138 // fake an event dispatch.
13139 ev = {type: 'appInit', state: this.initialized, errorMessage: msg};
13140 temp_.onInit.call(this, ev);
13143 if (this.config.showErrors) {
13145 } else if (window.console && console.log) {
13151 * Asynchronously load the configuration file. This method issues an
13152 * XMLHttpRequest to load the JSON file.
13156 * @see PaintWeb.config.configFile The configuration file.
13157 * @see pwlib.xhrLoad The library function being used for creating the
13158 * XMLHttpRequest object.
13160 this.configLoad = function () {
13161 pwlib.xhrLoad(PaintWeb.baseFolder + this.config.configFile,
13166 * The configuration reader. This is the event handler for the XMLHttpRequest
13167 * object, for the <code>onreadystatechange</code> event.
13171 * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
13173 * @see PaintWeb#configLoad The method which issues the XMLHttpRequest request
13174 * for loading the configuration file.
13176 this.configReady = function (xhr) {
13178 * readyState values:
13179 * 0 UNINITIALIZED open() has not been called yet.
13180 * 1 LOADING send() has not been called yet.
13181 * 2 LOADED send() has been called, headers and status are available.
13182 * 3 INTERACTIVE Downloading, responseText holds the partial data.
13183 * 4 COMPLETED Finished with all operations.
13185 if (!xhr || xhr.readyState !== 4) {
13189 if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
13190 _self.initError(lang.failedConfigLoad);
13194 var config = pwlib.jsonParse(xhr.responseText);
13195 pwlib.extend(_self.config, config);
13201 * Asynchronously load the language file. This method issues an XMLHttpRequest
13202 * to load the JSON file.
13206 * @see PaintWeb.config.lang The language you want for the PaintWeb user
13208 * @see pwlib.xhrLoad The library function being used for creating the
13209 * XMLHttpRequest object.
13211 this.langLoad = function () {
13212 var id = this.config.lang,
13213 file = PaintWeb.baseFolder;
13215 // If the language is not available, always fallback to English.
13216 if (!(id in this.config.languages)) {
13217 id = this.config.lang = 'en';
13220 if ('file' in this.config.languages[id]) {
13221 file += this.config.languages[id].file;
13223 file += this.config.langFolder + '/' + id + '.json';
13226 pwlib.xhrLoad(file, this.langReady);
13230 * The language file reader. This is the event handler for the XMLHttpRequest
13231 * object, for the <code>onreadystatechange</code> event.
13235 * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
13237 * @see PaintWeb#langLoad The method which issues the XMLHttpRequest request
13238 * for loading the language file.
13240 this.langReady = function (xhr) {
13241 if (!xhr || xhr.readyState !== 4) {
13245 if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
13246 _self.initError(lang.failedLangLoad);
13250 pwlib.extend(_self.lang, pwlib.jsonParse(xhr.responseText));
13252 if (_self.initCanvas() && _self.initContext()) {
13253 // Start GUI load now.
13256 _self.initError(lang.errorInitCanvas);
13261 * Initialize the PaintWeb commands.
13264 * @returns {Boolean} True if the initialization was successful, or false if
13267 this.initCommands = function () {
13268 if (this.commandRegister('historyUndo', this.historyUndo) &&
13269 this.commandRegister('historyRedo', this.historyRedo) &&
13270 this.commandRegister('selectAll', this.selectAll) &&
13271 this.commandRegister('selectionCut', this.selectionCut) &&
13272 this.commandRegister('selectionCopy', this.selectionCopy) &&
13273 this.commandRegister('clipboardPaste', this.clipboardPaste) &&
13274 this.commandRegister('imageSave', this.imageSave) &&
13275 this.commandRegister('imageClear', this.imageClear) &&
13276 this.commandRegister('swapFillStroke', this.swapFillStroke) &&
13277 this.commandRegister('imageZoomIn', this.imageZoomIn) &&
13278 this.commandRegister('imageZoomOut', this.imageZoomOut) &&
13279 this.commandRegister('imageZoomReset', this.imageZoomReset)) {
13282 this.initError(lang.errorInitCommands);
13288 * Load th PaintWeb GUI. This method loads the GUI markup file, the stylesheet
13293 * @see PaintWeb.config.guiStyle The interface style file.
13294 * @see PaintWeb.config.guiScript The interface script file.
13295 * @see pwlib.gui The interface object.
13297 this.guiLoad = function () {
13298 var cfg = this.config,
13299 gui = this.config.gui,
13300 base = PaintWeb.baseFolder + cfg.interfacesFolder + '/' + gui + '/',
13301 style = base + cfg.guiStyle,
13302 script = base + cfg.guiScript;
13304 this.styleLoad(gui + 'style', style);
13307 this.guiScriptReady();
13309 this.scriptLoad(script, this.guiScriptReady);
13314 * The <code>load</code> event handler for the PaintWeb GUI script. This
13315 * method creates an instance of the GUI object that just loaded and starts
13316 * loading the GUI markup.
13320 * @see PaintWeb.config.guiScript The interface script file.
13321 * @see PaintWeb.config.guiMarkup The interface markup file.
13322 * @see pwlib.gui The interface object.
13323 * @see pwlib.xhrLoad The library function being used for creating the
13324 * XMLHttpRequest object.
13326 this.guiScriptReady = function () {
13327 var cfg = _self.config,
13328 gui = _self.config.gui,
13329 base = cfg.interfacesFolder + '/' + gui + '/',
13330 markup = base + cfg.guiMarkup;
13332 _self.gui = new pwlib.gui(_self);
13334 // Check if the interface markup is cached already.
13335 if (markup in pwlib.fileCache) {
13336 if (_self.gui.init(pwlib.fileCache[markup])) {
13339 _self.initError(lang.errorInitGUI);
13343 pwlib.xhrLoad(PaintWeb.baseFolder + markup, _self.guiMarkupReady);
13348 * The GUI markup reader. This is the event handler for the XMLHttpRequest
13349 * object, for the <code>onreadystatechange</code> event.
13353 * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
13355 * @see PaintWeb#guiScriptReady The method which issues the XMLHttpRequest
13356 * request for loading the interface markup file.
13358 this.guiMarkupReady = function (xhr) {
13359 if (!xhr || xhr.readyState !== 4) {
13363 if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseXML) {
13364 _self.initError(lang.failedMarkupLoad);
13368 if (_self.gui.init(xhr.responseXML)) {
13371 _self.initError(lang.errorInitGUI);
13376 * Initialize the Canvas elements. This method creates the elements and
13377 * sets-up their dimensions.
13379 * <p>If {@link PaintWeb.config.imageLoad} is defined, then the image element
13380 * is inserted into the Canvas image.
13382 * <p>All the Canvas event listeners are also attached to the buffer Canvas
13386 * @returns {Boolean} True if the initialization was successful, or false if
13389 * @see PaintWeb#ev_canvas The global Canvas events handler.
13391 this.initCanvas = function () {
13392 var cfg = this.config,
13393 res = this.resolution,
13394 resInfo = doc.getElementById(res.elemId),
13395 layerCanvas = doc.createElement('canvas'),
13396 bufferCanvas = doc.createElement('canvas'),
13397 layerContext = layerCanvas.getContext('2d'),
13398 bufferContext = bufferCanvas.getContext('2d'),
13399 width = cfg.imageWidth,
13400 height = cfg.imageHeight,
13401 imageLoad = cfg.imageLoad;
13404 var style = doc.createElement('style');
13405 style.type = 'text/css';
13406 style.appendChild(doc.createTextNode(res.cssText));
13407 _self.elems.head.appendChild(style);
13409 resInfo = doc.createElement('div');
13410 resInfo.id = res.elemId;
13411 doc.body.appendChild(resInfo);
13415 this.initError(lang.errorInitCanvas);
13418 if (!layerCanvas || !bufferCanvas || !layerContext || !bufferContext) {
13419 this.initError(lang.noCanvasSupport);
13423 if (!pwlib.isSameHost(imageLoad.src, win.location.host)) {
13424 cfg.imageLoad = imageLoad = null;
13425 alert(lang.imageLoadDifferentHost);
13429 width = parseInt(imageLoad.width);
13430 height = parseInt(imageLoad.height);
13433 res.elem = resInfo;
13435 this.image.width = layerCanvas.width = bufferCanvas.width = width;
13436 this.image.height = layerCanvas.height = bufferCanvas.height = height;
13438 this.layer.canvas = layerCanvas;
13439 this.layer.context = layerContext;
13440 this.buffer.canvas = bufferCanvas;
13441 this.buffer.context = bufferContext;
13444 layerContext.drawImage(imageLoad, 0, 0);
13448 * Setup the event listeners for the canvas element.
13450 * The event handler (ev_canvas) calls the event handlers associated with
13451 * the active tool (e.g. tool.mousemove).
13453 var events = ['dblclick', 'click', 'mousedown', 'mouseup', 'mousemove',
13457 for (var i = 0; i < n; i++) {
13458 bufferCanvas.addEventListener(events[i], this.ev_canvas, false);
13465 * Initialize the Canvas buffer context. This method updates the context
13466 * properties to reflect the values defined in the PaintWeb configuration
13469 * <p>Shadows support is also determined. The {@link PaintWeb#shadowSupported}
13470 * value is updated accordingly.
13473 * @returns {Boolean} True if the initialization was successful, or false if
13476 this.initContext = function () {
13477 var bufferContext = this.buffer.context;
13479 // Opera does not render shadows, at the moment.
13480 if (!pwlib.browser.opera && bufferContext.shadowColor && 'shadowOffsetX' in
13481 bufferContext && 'shadowOffsetY' in bufferContext && 'shadowBlur' in
13483 this.shadowSupported = true;
13485 this.shadowSupported = false;
13488 var cfg = this.config,
13490 fillStyle: cfg.fillStyle,
13491 font: cfg.text.fontSize + 'px ' + cfg.text.fontFamily,
13492 lineCap: cfg.line.lineCap,
13493 lineJoin: cfg.line.lineJoin,
13494 lineWidth: cfg.line.lineWidth,
13495 miterLimit: cfg.line.miterLimit,
13496 strokeStyle: cfg.strokeStyle,
13497 textAlign: cfg.text.textAlign,
13498 textBaseline: cfg.text.textBaseline
13501 if (cfg.text.bold) {
13502 props.font = 'bold ' + props.font;
13505 if (cfg.text.italic) {
13506 props.font = 'italic ' + props.font;
13509 // Support Gecko 1.9.0
13510 if (!bufferContext.fillText && 'mozTextStyle' in bufferContext) {
13511 props.mozTextStyle = props.font;
13514 for (var prop in props) {
13515 bufferContext[prop] = props[prop];
13518 // shadows are only for the layer context.
13519 if (cfg.shadow.enable && this.shadowSupported) {
13520 var layerContext = this.layer.context;
13521 layerContext.shadowColor = cfg.shadow.shadowColor;
13522 layerContext.shadowBlur = cfg.shadow.shadowBlur;
13523 layerContext.shadowOffsetX = cfg.shadow.shadowOffsetX;
13524 layerContext.shadowOffsetY = cfg.shadow.shadowOffsetY;
13531 * Initialization procedure which runs after the configuration, language and
13532 * GUI files have loaded.
13534 * <p>This method dispatches the {@link pwlib.appEvent.appInit} event.
13538 * @see pwlib.appEvent.appInit
13540 this.initComplete = function () {
13541 if (!this.initCommands()) {
13542 this.initError(lang.errorInitCommands);
13546 // The initial blank state of the image
13549 // The global keyboard events handler implements everything needed for
13550 // switching between tools and for accessing any other functionality of the
13551 // Web application.
13552 kbListener_ = new pwlib.dom.KeyboardEventListener(this.config.guiPlaceholder,
13553 {keydown: this.ev_keyboard,
13554 keypress: this.ev_keyboard,
13555 keyup: this.ev_keyboard});
13557 this.updateCanvasScaling();
13558 this.win.addEventListener('resize', this.updateCanvasScaling, false);
13560 this.events.add('configChange', this.configChangeHandler);
13562 this.initialized = PaintWeb.INIT_DONE;
13564 this.events.dispatch(new appEvent.appInit(this.initialized));
13568 * Load all the configured drawing tools.
13571 this.initTools = function () {
13574 n = cfg.tools.length,
13575 base = PaintWeb.baseFolder + cfg.toolsFolder + '/';
13578 this.initError(lang.noToolConfigured);
13582 temp_.toolsLoadQueue = n;
13584 for (var i = 0; i < n; i++) {
13586 if (id in pwlib.tools) {
13589 this.scriptLoad(base + id + '.js' , this.toolLoaded);
13595 * The <code>load</code> event handler for each tool script.
13598 this.toolLoaded = function () {
13599 temp_.toolsLoadQueue--;
13601 if (temp_.toolsLoadQueue === 0) {
13602 var t = _self.config.tools,
13605 for (var i = 0; i < n; i++) {
13606 if (!_self.toolRegister(t[i])) {
13607 _self.initError(pwlib.strf(lang.toolRegisterFailed, {id: t[i]}));
13612 _self.initExtensions();
13617 * Load all the extensions.
13620 this.initExtensions = function () {
13623 n = cfg.extensions.length,
13624 base = PaintWeb.baseFolder + cfg.extensionsFolder + '/';
13627 this.initComplete();
13631 temp_.extensionsLoadQueue = n;
13633 for (var i = 0; i < n; i++) {
13634 id = cfg.extensions[i];
13635 if (id in pwlib.extensions) {
13636 this.extensionLoaded();
13638 this.scriptLoad(base + id + '.js', this.extensionLoaded);
13644 * The <code>load</code> event handler for each extension script.
13647 this.extensionLoaded = function () {
13648 temp_.extensionsLoadQueue--;
13650 if (temp_.extensionsLoadQueue === 0) {
13651 var e = _self.config.extensions,
13654 for (var i = 0; i < n; i++) {
13655 if (!_self.extensionRegister(e[i])) {
13656 _self.initError(pwlib.strf(lang.extensionRegisterFailed, {id: e[i]}));
13661 _self.initComplete();
13666 * Update the canvas scaling. This method determines the DPI and/or zoom level
13667 * used by the browser to render the application. Based on these values, the
13668 * canvas elements are scaled down to cancel any upscaling performed by the
13671 * <p>The {@link pwlib.appEvent.canvasSizeChange} application event is
13674 this.updateCanvasScaling = function () {
13675 var res = _self.resolution,
13676 cs = win.getComputedStyle(res.elem, null),
13677 image = _self.image;
13678 bufferStyle = _self.buffer.canvas.style,
13679 layerStyle = _self.layer.canvas.style,
13682 var width = parseInt(cs.width),
13683 height = parseInt(cs.height);
13685 if (pwlib.browser.opera) {
13686 // Opera zoom level detection.
13687 // The scaling factor is sufficiently accurate for zoom levels between
13688 // 100% and 200% (in steps of 10%).
13690 scaleNew = win.innerHeight / height;
13691 scaleNew = MathRound(scaleNew * 10) / 10;
13693 } else if (width && !isNaN(width) && width !== res.dpiOptimal) {
13694 // Page DPI detection. This only works in Gecko 1.9.1.
13696 res.dpiLocal = width;
13698 // The scaling factor is the same as in Gecko.
13699 scaleNew = MathFloor(res.dpiLocal / res.dpiOptimal);
13701 } else if (pwlib.browser.olpcxo) {
13702 // Support for the default Gecko included on the OLPC XO-1 system.
13705 // http://mxr.mozilla.org/mozilla-central/source/gfx/src/thebes/nsThebesDeviceContext.cpp#725
13706 // dotsArePixels = false on the XO due to a hard-coded patch.
13707 // Thanks go to roc from Mozilla for his feedback on making this work.
13709 res.dpiLocal = 134; // hard-coded value, we cannot determine it
13711 var appUnitsPerCSSPixel = 60, // hard-coded internally in Gecko
13712 devPixelsPerCSSPixel = res.dpiLocal / res.dpiOptimal; // 1.3958333333
13713 appUnitsPerDevPixel = appUnitsPerCSSPixel / devPixelsPerCSSPixel; // 42.9850746278...
13715 scaleNew = appUnitsPerCSSPixel / MathFloor(appUnitsPerDevPixel); // 1.4285714285...
13718 if (scaleNew === res.scale) {
13722 res.scale = scaleNew;
13724 var styleWidth = image.width / res.scale * image.zoom,
13725 styleHeight = image.height / res.scale * image.zoom;
13727 image.canvasScale = styleWidth / image.width;
13729 bufferStyle.width = layerStyle.width = styleWidth + 'px';
13730 bufferStyle.height = layerStyle.height = styleHeight + 'px';
13732 _self.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight,
13733 image.canvasScale));
13737 * The Canvas events handler.
13739 * <p>This method determines the mouse position relative to the canvas
13740 * element, after which it invokes the method of the currently active tool
13741 * with the same name as the current event type. For example, for the
13742 * <code>mousedown</code> event the <code><var>tool</var>.mousedown()</code>
13743 * method is invoked.
13745 * <p>The mouse coordinates are stored in the {@link PaintWeb#mouse} object.
13746 * These properties take into account the current zoom level and the image
13751 * @param {Event} ev The DOM Event object.
13753 * @returns {Boolean} True if the tool event handler executed, or false
13756 this.ev_canvas = function (ev) {
13764 * If the mouse is down already, skip the event.
13765 * This is needed to allow the user to go out of the drawing canvas,
13766 * release the mouse button, then come back and click to end the drawing
13768 * Additionally, this is needed to allow extensions like MouseKeys to
13769 * perform their actions during a drawing operation, even when a real
13770 * mouse is used. For example, allow the user to start drawing with the
13771 * keyboard (press 0) then use the real mouse to move and click to end
13772 * the drawing operation.
13774 if (_self.mouse.buttonDown) {
13777 _self.mouse.buttonDown = true;
13781 // Skip the event if the mouse button was not down.
13782 if (!_self.mouse.buttonDown) {
13785 _self.mouse.buttonDown = false;
13789 * Update the event, to include the mouse position, relative to the canvas
13792 if ('layerX' in ev) {
13793 if (_self.image.canvasScale === 1) {
13794 _self.mouse.x = ev.layerX;
13795 _self.mouse.y = ev.layerY;
13797 _self.mouse.x = MathRound(ev.layerX / _self.image.canvasScale);
13798 _self.mouse.y = MathRound(ev.layerY / _self.image.canvasScale);
13800 } else if ('offsetX' in ev) {
13801 if (_self.image.canvasScale === 1) {
13802 _self.mouse.x = ev.offsetX;
13803 _self.mouse.y = ev.offsetY;
13805 _self.mouse.x = MathRound(ev.offsetX / _self.image.canvasScale);
13806 _self.mouse.y = MathRound(ev.offsetY / _self.image.canvasScale);
13810 // The event handler of the current tool.
13811 if (ev.type in _self.tool && _self.tool[ev.type](ev)) {
13812 ev.preventDefault();
13820 * The global keyboard events handler. This makes all the keyboard shortcuts
13821 * work in the web application.
13823 * <p>This method determines the key the user pressed, based on the
13824 * <var>ev</var> DOM Event object, taking into consideration any browser
13825 * differences. Two new properties are added to the <var>ev</var> object:
13828 * <li><var>ev.kid_</var> is a string holding the key and the modifiers list
13829 * (<kbd>Control</kbd>, <kbd>Alt</kbd> and/or <kbd>Shift</kbd>). For
13830 * example, if the user would press the key <kbd>A</kbd> while holding down
13831 * <kbd>Control</kbd>, then <var>ev.kid_</var> would be "Control A". If the
13832 * user would press "9" while holding down <kbd>Shift</kbd>, then
13833 * <var>ev.kid_</var> would be "Shift 9".
13835 * <li><var>ev.kobj_</var> holds a reference to the keyboard shortcut
13836 * definition object from the configuration. This is useful for reuse, for
13837 * passing parameters from the keyboard shortcut configuration object to the
13841 * <p>In {@link PaintWeb.config.keys} one can setup the keyboard shortcuts.
13842 * If the keyboard combination is found in that list, then the associated tool
13845 * <p>Note: this method includes some work-around for making the image zoom
13846 * keys work well both in Opera and Firefox.
13850 * @param {Event} ev The DOM Event object.
13852 * @see PaintWeb.config.keys The keyboard shortcuts configuration.
13853 * @see pwlib.dom.KeyboardEventListener The class dealing with the
13854 * cross-browser differences in the DOM keyboard events.
13856 this.ev_keyboard = function (ev) {
13857 // Do not continue if the key was not recognized by the lib.
13862 if (ev.target && ev.target.nodeName) {
13863 switch (ev.target.nodeName.toLowerCase()) {
13865 if (ev.type === 'keypress' && (ev.key_ === 'Up' || ev.key_ === 'Down')
13866 && ev.target.getAttribute('type') === 'number') {
13867 _self.ev_numberInput(ev);
13876 // Rather ugly, but the only way, at the moment, to detect these keys in
13877 // Opera and Firefox.
13878 if (ev.type === 'keypress' && ev.char_) {
13879 var isZoomKey = true,
13880 imageZoomKeys = _self.config.imageZoomKeys;
13882 // Check if this is a zoom key and execute the commands as needed.
13883 switch (ev.char_) {
13884 case imageZoomKeys['in']:
13885 _self.imageZoomIn(ev);
13888 case imageZoomKeys['out']:
13889 _self.imageZoomOut(ev);
13891 case imageZoomKeys['reset']:
13892 _self.imageZoomReset(ev);
13899 ev.preventDefault();
13904 // Determine the key ID.
13906 var i, kmods = {altKey: 'Alt', ctrlKey: 'Control', shiftKey: 'Shift'};
13908 if (ev[i] && ev.key_ !== kmods[i]) {
13909 ev.kid_ += kmods[i] + ' ';
13912 ev.kid_ += ev.key_;
13914 // Send the keyboard event to the event handler of the active tool. If it
13915 // returns true, we consider it recognized the keyboard shortcut.
13916 if (_self.tool && ev.type in _self.tool && _self.tool[ev.type](ev)) {
13920 // If there's no event handler within the active tool, or if the event
13921 // handler does otherwise return false, then we continue with the global
13922 // keyboard shortcuts.
13924 var gkey = _self.config.keys[ev.kid_];
13931 // Check if the keyboard shortcut has some extension associated.
13932 if ('extension' in gkey) {
13933 var extension = _self.extensions[gkey.extension],
13934 method = gkey.method || ev.type;
13936 // Call the extension method.
13937 if (method in extension) {
13938 extension[method].call(this, ev);
13941 } else if ('command' in gkey && gkey.command in _self.commands) {
13942 // Invoke the command associated with the key.
13943 _self.commands[gkey.command].call(this, ev);
13945 } else if (ev.type === 'keydown' && 'toolActivate' in gkey) {
13947 // Active the tool associated to the key.
13948 _self.toolActivate(gkey.toolActivate, ev);
13952 if (ev.type === 'keypress') {
13953 ev.preventDefault();
13958 * This is the <code>keypress</code> event handler for inputs of type=number.
13959 * This function only handles cases when the key is <kbd>Up</kbd> or
13960 * <kbd>Down</kbd>. For the <kbd>Up</kbd> key the input value is increased,
13961 * and for the <kbd>Down</kbd> the value is decreased.
13964 * @param {Event} ev The DOM Event object.
13965 * @see PaintWeb#ev_keyboard
13967 this.ev_numberInput = function (ev) {
13968 var target = ev.target;
13970 // Process the value.
13972 max = parseFloat(target.getAttribute('max')),
13973 min = parseFloat(target.getAttribute('min')),
13974 step = parseFloat(target.getAttribute('step'));
13976 if (target.value === '' || target.value === null) {
13977 val = !isNaN(min) ? min : 0;
13979 val = target.value.replace(/[,.]+/g, '.').replace(/[^0-9.\-]/g, '');
13980 val = parseFloat(val);
13983 // If target is not a number, then set the old value, or the minimum value. If all fails, set 0.
13996 if (ev.key_ === 'Down') {
14002 if (!isNaN(max) && val > max) {
14004 } else if (!isNaN(min) && val < min) {
14008 if (val == target.value) {
14012 target.value = val;
14014 // Dispatch the 'change' events to make sure that any associated event
14015 // handlers pick up the changes.
14016 if (doc.createEvent && target.dispatchEvent) {
14017 var ev_change = doc.createEvent('HTMLEvents');
14018 ev_change.initEvent('change', true, true);
14019 target.dispatchEvent(ev_change);
14024 * Zoom into the image.
14026 * @param {mixed} ev An event object which might have the <var>shiftKey</var>
14027 * property. If the property evaluates to true, then the zoom level will
14028 * increase twice more than normal.
14030 * @returns {Boolean} True if the operation was successful, or false if not.
14032 * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
14033 * @see PaintWeb.config.zoomStep The value used for increasing the zoom level.
14035 this.imageZoomIn = function (ev) {
14036 if (ev && ev.shiftKey) {
14037 _self.config.imageZoomStep *= 2;
14040 var res = _self.imageZoomTo('+');
14042 if (ev && ev.shiftKey) {
14043 _self.config.imageZoomStep /= 2;
14050 * Zoom out of the image.
14052 * @param {mixed} ev An event object which might have the <var>shiftKey</var>
14053 * property. If the property evaluates to true, then the zoom level will
14054 * decrease twice more than normal.
14056 * @returns {Boolean} True if the operation was successful, or false if not.
14058 * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
14059 * @see PaintWeb.config.zoomStep The value used for decreasing the zoom level.
14061 this.imageZoomOut = function (ev) {
14062 if (ev && ev.shiftKey) {
14063 _self.config.imageZoomStep *= 2;
14066 var res = _self.imageZoomTo('-');
14068 if (ev && ev.shiftKey) {
14069 _self.config.imageZoomStep /= 2;
14076 * Reset the image zoom level to normal.
14078 * @returns {Boolean} True if the operation was successful, or false if not.
14080 * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
14082 this.imageZoomReset = function (ev) {
14083 return _self.imageZoomTo(1);
14087 * Change the image zoom level.
14089 * <p>This method dispatches the {@link pwlib.appEvent.imageZoom} application
14090 * event before zooming the image. Once the image zoom is applied, the {@link
14091 * pwlib.appEvent.canvasSizeChange} event is dispatched.
14093 * @param {Number|String} level The level you want to zoom the image to.
14095 * <p>If the value is a number, it must be a floating point positive number,
14096 * where 0.5 means 50%, 1 means 100% (normal) zoom, 4 means 400% and so on.
14098 * <p>If the value is a string it must be "+" or "-". This means that the zoom
14099 * level will increase/decrease using the configured {@link
14100 * PaintWeb.config.zoomStep}.
14102 * @returns {Boolean} True if the image zoom level changed successfully, or
14105 this.imageZoomTo = function (level) {
14106 var image = this.image,
14107 config = this.config,
14108 res = this.resolution;
14112 } else if (level === '+') {
14113 level = image.zoom + config.imageZoomStep;
14114 } else if (level === '-') {
14115 level = image.zoom - config.imageZoomStep;
14116 } else if (typeof level !== 'number') {
14120 if (level > config.imageZoomMax) {
14121 level = config.imageZoomMax;
14122 } else if (level < config.imageZoomMin) {
14123 level = config.imageZoomMin;
14126 if (level === image.zoom) {
14130 var cancel = this.events.dispatch(new appEvent.imageZoom(level));
14135 var styleWidth = image.width / res.scale * level,
14136 styleHeight = image.height / res.scale * level,
14137 bufferStyle = this.buffer.canvas.style,
14138 layerStyle = this.layer.canvas.style;
14140 image.canvasScale = styleWidth / image.width;
14142 bufferStyle.width = layerStyle.width = styleWidth + 'px';
14143 bufferStyle.height = layerStyle.height = styleHeight + 'px';
14145 image.zoom = level;
14147 this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight,
14148 image.canvasScale));
14156 * <p>The content of the image is retained only if the browser implements the
14157 * <code>getImageData</code> and <code>putImageData</code> methods.
14159 * <p>This method dispatches three application events: {@link
14160 * pwlib.appEvent.imageSizeChange}, {@link pwlib.appEvent.canvasSizeChange}
14161 * and {@link pwlib.appEvent.imageCrop}. The <code>imageCrop</code> event is
14162 * dispatched before the image is cropped. The <code>imageSizeChange</code>
14163 * and <code>canvasSizeChange</code> events are dispatched after the image is
14166 * @param {Number} cropX Image cropping start position on the x-axis.
14167 * @param {Number} cropY Image cropping start position on the y-axis.
14168 * @param {Number} cropWidth Image crop width.
14169 * @param {Number} cropHeight Image crop height.
14171 * @returns {Boolean} True if the image was cropped successfully, or false if
14174 this.imageCrop = function (cropX, cropY, cropWidth, cropHeight) {
14175 var bufferCanvas = this.buffer.canvas,
14176 bufferContext = this.buffer.context,
14177 image = this.image,
14178 layerCanvas = this.layer.canvas,
14179 layerContext = this.layer.context;
14181 cropX = parseInt(cropX);
14182 cropY = parseInt(cropY);
14183 cropWidth = parseInt(cropWidth);
14184 cropHeight = parseInt(cropHeight);
14186 if (!cropWidth || !cropHeight || isNaN(cropX) || isNaN(cropY) ||
14187 isNaN(cropWidth) || isNaN(cropHeight) || cropX >= image.width || cropY
14192 var cancel = this.events.dispatch(new appEvent.imageCrop(cropX, cropY,
14193 cropWidth, cropHeight));
14198 if (cropWidth > this.config.imageWidthMax) {
14199 cropWidth = this.config.imageWidthMax;
14202 if (cropHeight > this.config.imageHeightMax) {
14203 cropHeight = this.config.imageHeightMax;
14206 if (cropX === 0 && cropY === 0 && image.width === cropWidth && image.height
14211 var scaledWidth = cropWidth * image.canvasScale,
14212 scaledHeight = cropHeight * image.canvasScale;
14214 bufferCanvas.style.width = layerCanvas.style.width = scaledWidth + 'px';
14215 bufferCanvas.style.height = layerCanvas.style.height = scaledHeight + 'px';
14217 // The canvas state is reset once the dimensions change.
14218 var state = this.stateSave(layerContext),
14219 dataWidth = MathMin(image.width, cropWidth),
14220 dataHeight = MathMin(image.height, cropHeight),
14221 sumX = cropX + dataWidth,
14222 sumY = cropY + dataHeight;
14224 if (sumX > image.width) {
14225 dataWidth -= sumX - image.width;
14227 if (sumY > image.height) {
14228 dataHeight -= sumY - image.height;
14231 // The image is cleared once the dimensions change. We need to restore the image.
14234 if (layerContext.getImageData) {
14235 // TODO: handle "out of memory" errors.
14237 idata = layerContext.getImageData(cropX, cropY, dataWidth, dataHeight);
14239 // do not continue if we can't store the image in memory.
14244 layerCanvas.width = cropWidth;
14245 layerCanvas.height = cropHeight;
14247 if (idata && layerContext.putImageData) {
14248 layerContext.putImageData(idata, 0, 0);
14251 this.stateRestore(layerContext, state);
14252 state = this.stateSave(bufferContext);
14255 if (bufferContext.getImageData) {
14257 idata = bufferContext.getImageData(cropX, cropY, dataWidth, dataHeight);
14261 bufferCanvas.width = cropWidth;
14262 bufferCanvas.height = cropHeight;
14264 if (idata && bufferContext.putImageData) {
14265 bufferContext.putImageData(idata, 0, 0);
14268 this.stateRestore(bufferContext, state);
14270 image.width = cropWidth;
14271 image.height = cropHeight;
14273 this.events.dispatch(new appEvent.imageSizeChange(cropWidth, cropHeight));
14274 this.events.dispatch(new appEvent.canvasSizeChange(scaledWidth,
14275 scaledHeight, image.canvasScale));
14281 * Save the state of a Canvas context.
14283 * @param {CanvasRenderingContext2D} context The 2D context of the Canvas
14284 * element you want to save the state.
14286 * @returns {Object} The object has all the state properties and values.
14288 this.stateSave = function (context) {
14289 if (!context || !context.canvas || !this.stateProperties) {
14295 n = this.stateProperties.length;
14297 for (var i = 0; i < n; i++) {
14298 prop = this.stateProperties[i];
14299 stateObj[prop] = context[prop];
14306 * Restore the state of a Canvas context.
14308 * @param {CanvasRenderingContext2D} context The 2D context where you want to
14309 * restore the state.
14311 * @param {Object} stateObj The state object saved by the {@link
14312 * PaintWeb#stateSave} method.
14314 * @returns {Boolean} True if the operation was successful, or false if not.
14316 this.stateRestore = function (context, stateObj) {
14317 if (!context || !context.canvas) {
14321 for (var state in stateObj) {
14322 context[state] = stateObj[state];
14329 * Allow shadows. This method re-enabled shadow rendering, if it was enabled
14330 * before shadows were disallowed.
14332 * <p>The {@link pwlib.appEvent.shadowAllow} event is dispatched.
14334 this.shadowAllow = function () {
14335 if (this.shadowAllowed || !this.shadowSupported) {
14339 // Note that some daily builds of Webkit in Chromium fail to render the
14340 // shadow when context.drawImage() is used (see the this.layerUpdate()).
14341 var context = this.layer.context,
14342 cfg = this.config.shadow;
14345 context.shadowColor = cfg.shadowColor;
14346 context.shadowOffsetX = cfg.shadowOffsetX;
14347 context.shadowOffsetY = cfg.shadowOffsetY;
14348 context.shadowBlur = cfg.shadowBlur;
14351 this.shadowAllowed = true;
14353 this.events.dispatch(new appEvent.shadowAllow(true));
14357 * Disallow shadows. This method disables shadow rendering, if it is enabled.
14359 * <p>The {@link pwlib.appEvent.shadowAllow} event is dispatched.
14361 this.shadowDisallow = function () {
14362 if (!this.shadowAllowed || !this.shadowSupported) {
14366 if (this.config.shadow.enable) {
14367 var context = this.layer.context;
14368 context.shadowColor = 'rgba(0,0,0,0)';
14369 context.shadowOffsetX = 0;
14370 context.shadowOffsetY = 0;
14371 context.shadowBlur = 0;
14374 this.shadowAllowed = false;
14376 this.events.dispatch(new appEvent.shadowAllow(false));
14380 * Update the current image layer by moving the pixels from the buffer onto
14381 * the layer. This method also adds a point into the history.
14383 * @returns {Boolean} True if the operation was successful, or false if not.
14385 this.layerUpdate = function () {
14386 this.layer.context.drawImage(this.buffer.canvas, 0, 0);
14387 this.buffer.context.clearRect(0, 0, this.image.width, this.image.height);
14394 * Add the current image layer to the history.
14396 * <p>Once the history state has been updated, this method dispatches the
14397 * {@link pwlib.appEvent.historyUpdate} event.
14399 * @returns {Boolean} True if the operation was successful, or false if not.
14401 // TODO: some day it would be nice to implement a hybrid history system.
14402 this.historyAdd = function () {
14403 var layerContext = this.layer.context,
14404 history = this.history,
14405 prevPos = history.pos;
14407 if (!layerContext.getImageData) {
14411 // We are in an undo-step, trim until the end, eliminating any possible redo-steps.
14412 if (prevPos < history.states.length) {
14413 history.states.splice(prevPos, history.states.length);
14416 // TODO: in case of "out of memory" errors... I should show up some error.
14418 history.states.push(layerContext.getImageData(0, 0, this.image.width,
14419 this.image.height));
14424 // If we have too many history ImageDatas, remove the oldest ones
14425 if ('historyLimit' in this.config &&
14426 history.states.length > this.config.historyLimit) {
14428 history.states.splice(0, history.states.length
14429 - this.config.historyLimit);
14431 history.pos = history.states.length;
14433 this.events.dispatch(new appEvent.historyUpdate(history.pos, prevPos,
14440 * Jump to any ImageData/position in the history.
14442 * <p>Once the history state has been updated, this method dispatches the
14443 * {@link pwlib.appEvent.historyUpdate} event.
14445 * @param {Number|String} pos The history position to jump to.
14447 * <p>If the value is a number, then it must point to an existing index in the
14448 * <var>{@link PaintWeb#history}.states</var> array.
14450 * <p>If the value is a string, it must be "undo" or "redo".
14452 * @returns {Boolean} True if the operation was successful, or false if not.
14454 this.historyGoto = function (pos) {
14455 var layerContext = this.layer.context,
14456 image = this.image,
14457 history = this.history;
14459 if (!history.states.length || !layerContext.putImageData) {
14463 var cpos = history.pos;
14465 if (pos === 'undo') {
14467 } else if (pos === 'redo') {
14471 if (pos === cpos || pos < 1 || pos > history.states.length) {
14475 var himg = history.states[pos-1];
14480 // Each image in the history can have a different size. As such, the script
14481 // must take this into consideration.
14482 var w = MathMin(image.width, himg.width),
14483 h = MathMin(image.height, himg.height);
14485 layerContext.clearRect(0, 0, image.width, image.height);
14488 // Firefox 3 does not clip the image, if needed.
14489 layerContext.putImageData(himg, 0, 0, 0, 0, w, h);
14492 // The workaround is to use a new canvas from which we can copy the
14493 // history image without causing any exceptions.
14494 var tmp = doc.createElement('canvas');
14495 tmp.width = himg.width;
14496 tmp.height = himg.height;
14498 var tmp2 = tmp.getContext('2d');
14499 tmp2.putImageData(himg, 0, 0);
14501 layerContext.drawImage(tmp, 0, 0);
14507 this.events.dispatch(new appEvent.historyUpdate(pos, cpos,
14508 history.states.length));
14514 * Clear the image history.
14516 * <p>This method dispatches the {@link pwlib.appEvent.historyUpdate} event.
14520 this.historyReset = function () {
14521 this.history.pos = 0;
14522 this.history.states = [];
14524 this.events.dispatch(new appEvent.historyUpdate(0, 0, 0));
14528 * Perform horizontal/vertical line snapping. This method updates the mouse
14529 * coordinates to "snap" with the given coordinates.
14531 * @param {Number} x The x-axis location.
14532 * @param {Number} y The y-axis location.
14534 this.toolSnapXY = function (x, y) {
14535 var diffx = MathAbs(_self.mouse.x - x),
14536 diffy = MathAbs(_self.mouse.y - y);
14538 if (diffx > diffy) {
14546 * Activate a drawing tool by ID.
14548 * <p>The <var>id</var> provided must be of an existing drawing tool, one that
14549 * has been installed.
14551 * <p>The <var>ev</var> argument is an optional DOM Event object which is
14552 * useful when dealing with different types of tool activation, either by
14553 * keyboard or by mouse events. Tool-specific code can implement different
14554 * functionality based on events.
14556 * <p>This method dispatches the {@link pwlib.appEvent.toolPreactivate} event
14557 * before creating the new tool instance. Once the new tool is successfully
14558 * activated, the {@link pwlib.appEvent.toolActivate} event is also
14561 * @param {String} id The ID of the drawing tool to be activated.
14562 * @param {Event} [ev] The DOM Event object.
14564 * @returns {Boolean} True if the tool has been activated, or false if not.
14566 * @see PaintWeb#toolRegister Register a new drawing tool.
14567 * @see PaintWeb#toolUnregister Unregister a drawing tool.
14569 * @see pwlib.tools The object holding all the drawing tools.
14570 * @see pwlib.appEvent.toolPreactivate
14571 * @see pwlib.appEvent.toolActivate
14573 this.toolActivate = function (id, ev) {
14574 if (!id || !(id in pwlib.tools) || typeof pwlib.tools[id] !== 'function') {
14578 var tool = pwlib.tools[id],
14579 prevId = this.tool ? this.tool._id : null;
14581 if (prevId && this.tool instanceof pwlib.tools[id]) {
14585 var cancel = this.events.dispatch(new appEvent.toolPreactivate(id, prevId));
14590 var tool_obj = new tool(this, ev);
14596 * Each tool can implement its own mouse and keyboard events handler.
14597 * Additionally, tool objects can implement handlers for the deactivation
14598 * and activation events.
14599 * Given tool1 is active and tool2 is going to be activated, then the
14600 * following event handlers will be called:
14602 * tool2.preActivate
14606 * In the "preActivate" event handler you can cancel the tool activation by
14607 * returning a value which evaluates to false.
14610 if ('preActivate' in tool_obj && !tool_obj.preActivate(ev)) {
14615 // Deactivate the previously active tool
14616 if (this.tool && 'deactivate' in this.tool) {
14617 this.tool.deactivate(ev);
14620 this.tool = tool_obj;
14622 this.mouse.buttonDown = false;
14624 // Besides the "constructor", each tool can also have code which is run
14625 // after the deactivation of the previous tool.
14626 if ('activate' in this.tool) {
14627 this.tool.activate(ev);
14630 this.events.dispatch(new appEvent.toolActivate(id, prevId));
14636 * Register a new drawing tool into PaintWeb.
14638 * <p>This method dispatches the {@link pwlib.appEvent.toolRegister}
14639 * application event.
14641 * @param {String} id The ID of the new tool. The tool object must exist in
14642 * {@link pwlib.tools}.
14644 * @returns {Boolean} True if the tool was successfully registered, or false
14647 * @see PaintWeb#toolUnregister allows you to unregister tools.
14648 * @see pwlib.tools Holds all the drawing tools.
14649 * @see pwlib.appEvent.toolRegister
14651 this.toolRegister = function (id) {
14652 if (typeof id !== 'string' || !id) {
14656 // TODO: it would be very nice to create the tool instance on register, for
14657 // further extensibility.
14659 var tool = pwlib.tools[id];
14660 if (typeof tool !== 'function') {
14664 tool.prototype._id = id;
14666 this.events.dispatch(new appEvent.toolRegister(id));
14668 if (!this.tool && id === this.config.toolDefault) {
14669 return this.toolActivate(id);
14676 * Unregister a drawing tool from PaintWeb.
14678 * <p>This method dispatches the {@link pwlib.appEvent.toolUnregister}
14679 * application event.
14681 * @param {String} id The ID of the tool you want to unregister.
14683 * @returns {Boolean} True if the tool was unregistered, or false if it does
14684 * not exist or some error occurred.
14686 * @see PaintWeb#toolRegister allows you to register new drawing tools.
14687 * @see pwlib.tools Holds all the drawing tools.
14688 * @see pwlib.appEvent.toolUnregister
14690 this.toolUnregister = function (id) {
14691 if (typeof id !== 'string' || !id || !(id in pwlib.tools)) {
14695 this.events.dispatch(new appEvent.toolUnregister(id));
14701 * Register a new extension into PaintWeb.
14703 * <p>If the extension object being constructed has the
14704 * <code>extensionRegister()</code> method, then it will be invoked, allowing
14705 * any custom extension registration code to run. If the method returns false,
14706 * then the extension will not be registered.
14708 * <p>Once the extension is successfully registered, this method dispatches
14709 * the {@link pwlib.appEvent.extensionRegister} application event.
14711 * @param {String} id The ID of the new extension. The extension object
14712 * constructor must exist in {@link pwlib.extensions}.
14714 * @returns {Boolean} True if the extension was successfully registered, or
14717 * @see PaintWeb#extensionUnregister allows you to unregister extensions.
14718 * @see PaintWeb#extensions Holds all the instances of registered extensions.
14719 * @see pwlib.extensions Holds all the extension classes.
14721 this.extensionRegister = function (id) {
14722 if (typeof id !== 'string' || !id) {
14726 var func = pwlib.extensions[id];
14727 if (typeof func !== 'function') {
14731 func.prototype._id = id;
14733 var obj = new func(_self);
14735 if ('extensionRegister' in obj && !obj.extensionRegister()) {
14739 this.extensions[id] = obj;
14740 this.events.dispatch(new appEvent.extensionRegister(id));
14746 * Unregister an extension from PaintWeb.
14748 * <p>If the extension object being destructed has the
14749 * <code>extensionUnregister()</code> method, then it will be invoked,
14750 * allowing any custom extension removal code to run.
14752 * <p>Before the extension is unregistered, this method dispatches the {@link
14753 * pwlib.appEvent.extensionUnregister} application event.
14755 * @param {String} id The ID of the extension object you want to unregister.
14757 * @returns {Boolean} True if the extension was removed, or false if it does
14758 * not exist or some error occurred.
14760 * @see PaintWeb#extensionRegister allows you to register new extensions.
14761 * @see PaintWeb#extensions Holds all the instances of registered extensions.
14762 * @see pwlib.extensions Holds all the extension classes.
14764 this.extensionUnregister = function (id) {
14765 if (typeof id !== 'string' || !id || !(id in this.extensions)) {
14769 this.events.dispatch(new appEvent.extensionUnregister(id));
14771 if ('extensionUnregister' in this.extensions[id]) {
14772 this.extensions[id].extensionUnregister();
14774 delete this.extensions[id];
14780 * Register a new command in PaintWeb. Commands are simple function objects
14781 * which can be invoked by keyboard shortcuts or by GUI elements.
14783 * <p>Once the command is successfully registered, this method dispatches the
14784 * {@link pwlib.appEvent.commandRegister} application event.
14786 * @param {String} id The ID of the new command.
14787 * @param {Function} func The command function.
14789 * @returns {Boolean} True if the command was successfully registered, or
14792 * @see PaintWeb#commandUnregister allows you to unregister commands.
14793 * @see PaintWeb#commands Holds all the registered commands.
14795 this.commandRegister = function (id, func) {
14796 if (typeof id !== 'string' || !id || typeof func !== 'function' || id in
14801 this.commands[id] = func;
14802 this.events.dispatch(new appEvent.commandRegister(id));
14808 * Unregister a command from PaintWeb.
14810 * <p>Before the command is unregistered, this method dispatches the {@link
14811 * pwlib.appEvent.commandUnregister} application event.
14813 * @param {String} id The ID of the command you want to unregister.
14815 * @returns {Boolean} True if the command was removed successfully, or false
14818 * @see PaintWeb#commandRegister allows you to register new commands.
14819 * @see PaintWeb#commands Holds all the registered commands.
14821 this.commandUnregister = function (id) {
14822 if (typeof id !== 'string' || !id || !(id in this.commands)) {
14826 this.events.dispatch(new appEvent.commandUnregister(id));
14828 delete this.commands[id];
14834 * Load a script into the document.
14836 * @param {String} url The script URL you want to insert.
14837 * @param {Function} [handler] The <code>load</code> event handler you want.
14839 this.scriptLoad = function (url, handler) {
14841 var elem = doc.createElement('script');
14842 elem.type = 'text/javascript';
14844 this.elems.head.appendChild(elem);
14848 // huh, use XHR then eval() the code.
14849 // browsers do not dispatch the 'load' event reliably for script elements.
14850 var xhr = new XMLHttpRequest();
14852 xhr.onreadystatechange = function () {
14853 if (!xhr || xhr.readyState !== 4) {
14856 } else if ((xhr.status !== 304 && xhr.status !== 200) ||
14857 !xhr.responseText) {
14858 handler(false, xhr);
14862 eval.call(win, xhr.responseText);
14864 eval(xhr.responseText, win);
14866 handler(true, xhr);
14872 xhr.open('GET', url);
14877 * Insert a stylesheet into the document.
14879 * @param {String} id The stylesheet ID. This is used to avoid inserting the
14880 * same style in the document.
14881 * @param {String} url The URL of the stylesheet you want to insert.
14882 * @param {String} [media='screen, projection'] The media attribute.
14883 * @param {Function} [handler] The <code>load</code> event handler.
14885 this.styleLoad = function (id, url, media, handler) {
14886 id = 'paintweb_style_' + id;
14888 var elem = doc.getElementById(id);
14894 media = 'screen, projection';
14897 elem = doc.createElement('link');
14900 elem.addEventListener('load', handler, false);
14904 elem.rel = 'stylesheet';
14905 elem.type = 'text/css';
14906 elem.media = media;
14909 this.elems.head.appendChild(elem);
14913 * Perform action undo.
14915 * @returns {Boolean} True if the operation was successful, or false if not.
14917 * @see PaintWeb#historyGoto The method invoked by this command.
14919 this.historyUndo = function () {
14920 return _self.historyGoto('undo');
14924 * Perform action redo.
14926 * @returns {Boolean} True if the operation was successful, or false if not.
14928 * @see PaintWeb#historyGoto The method invoked by this command.
14930 this.historyRedo = function () {
14931 return _self.historyGoto('redo');
14935 * Load an image. By loading an image the history is cleared and the Canvas
14936 * dimensions are updated to fit the new image.
14938 * <p>This method dispatches two application events: {@link
14939 * pwlib.appEvent.imageSizeChange} and {@link
14940 * pwlib.appEvent.canvasSizeChange}.
14942 * @param {Element} importImage The image element you want to load into the
14945 * @returns {Boolean} True if the operation was successful, or false if not.
14947 this.imageLoad = function (importImage) {
14948 if (!importImage || !importImage.width || !importImage.height ||
14949 importImage.nodeType !== Node.ELEMENT_NODE ||
14950 !pwlib.isSameHost(importImage.src, win.location.host)) {
14954 this.historyReset();
14956 var layerContext = this.layer.context,
14957 layerCanvas = this.layer.canvas,
14958 layerStyle = layerCanvas.style,
14959 bufferCanvas = this.buffer.canvas,
14960 bufferStyle = bufferCanvas.style,
14961 image = this.image,
14962 styleWidth = importImage.width * image.canvasScale,
14963 styleHeight = importImage.height * image.canvasScale,
14966 bufferCanvas.width = layerCanvas.width = importImage.width;
14967 bufferCanvas.height = layerCanvas.height = importImage.height;
14970 layerContext.drawImage(importImage, 0, 0);
14973 bufferCanvas.width = layerCanvas.width = image.width;
14974 bufferCanvas.height = layerCanvas.height = image.height;
14978 image.width = importImage.width;
14979 image.height = importImage.height;
14980 bufferStyle.width = layerStyle.width = styleWidth + 'px';
14981 bufferStyle.height = layerStyle.height = styleHeight + 'px';
14982 _self.config.imageLoad = importImage;
14984 this.events.dispatch(new appEvent.imageSizeChange(image.width,
14987 this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight,
14988 image.canvasScale));
14999 this.imageClear = function (ev) {
15000 _self.layer.context.clearRect(0, 0, _self.image.width, _self.image.height);
15001 _self.historyAdd();
15007 * <p>This method dispatches the {@link pwlib.appEvent.imageSave} event.
15009 * <p><strong>Note:</strong> the "Save image" operation relies on integration
15010 * extensions. A vanilla configuration of PaintWeb will simply open the the
15011 * image in a new tab using a data: URL. You must have some event listener for
15012 * the <code>imageSave</code> event and you must prevent the default action.
15014 * <p>If the default action for the <code>imageSave</code> application event
15015 * is not prevented, then this method will also dispatch the {@link
15016 * pwlib.appEvent.imageSaveResult} application event.
15018 * <p>Your event handler for the <code>imageSave</code> event must dispatch
15019 * the <code>imageSaveResult</code> event.
15021 * @param {String} [type="auto"] Image MIME type. This tells the browser which
15022 * format to use when saving the image. If the image format type is not
15023 * supported, then the image is saved as PNG.
15025 * <p>You can use the resulting data URL to check which is the actual image
15028 * <p>When <var>type</var> is "auto" then PaintWeb checks the type of the
15029 * image currently loaded ({@link PaintWeb.config.imageLoad}). If the format
15030 * is recognized, then the same format is used to save the image.
15032 * @returns {Boolean} True if the operation was successful, or false if not.
15034 this.imageSave = function (type) {
15035 var canvas = _self.layer.canvas,
15036 imageLoad = _self.config.imageLoad,
15037 ext = 'png', idata = null, src = null, pos;
15039 if (!canvas.toDataURL) {
15043 var extMap = {'jpg' : 'image/jpeg', 'jpeg' : 'image/jpeg', 'png'
15044 : 'image/png', 'gif' : 'image/gif'};
15046 // Detect the MIME type of the image currently loaded.
15048 if (imageLoad && imageLoad.src && imageLoad.src.substr(0, 5) !== 'data:') {
15049 src = imageLoad.src;
15050 pos = src.indexOf('?');
15052 src = src.substr(0, pos);
15054 ext = src.substr(src.lastIndexOf('.') + 1).toLowerCase();
15057 type = extMap[ext] || 'image/png';
15061 if (type === 'image/jpeg') {
15062 idata = canvas.toDataURL(type, _self.config.jpegSaveQuality);
15064 idata = canvas.toDataURL(type);
15067 alert(lang.errorImageSave + "\n" + err);
15071 if (!idata || idata === 'data:,') {
15075 var img = _self.image,
15076 ev = new appEvent.imageSave(idata, img.width, img.height),
15077 cancel = _self.events.dispatch(ev);
15083 var imgwin = _self.win.open();
15088 imgwin.location = idata;
15091 _self.events.dispatch(new appEvent.imageSaveResult(true));
15097 * Swap the fill and stroke styles. This is just like in Photoshop, if the
15098 * user presses X, the fill/stroke colors are swapped.
15100 * <p>This method dispatches the {@link pwlib.appEvent.configChange} event
15101 * twice for each color (strokeStyle and fillStyle).
15103 this.swapFillStroke = function () {
15104 var fillStyle = _self.config.fillStyle,
15105 strokeStyle = _self.config.strokeStyle;
15107 _self.config.fillStyle = strokeStyle;
15108 _self.config.strokeStyle = fillStyle;
15110 var ev = new appEvent.configChange(strokeStyle, fillStyle, 'fillStyle', '',
15113 _self.events.dispatch(ev);
15115 ev = new appEvent.configChange(fillStyle, strokeStyle, 'strokeStyle', '',
15118 _self.events.dispatch(ev);
15122 * Select all the pixels. This activates the selection tool, and selects the
15125 * @param {Event} [ev] The DOM Event object which generated the request.
15126 * @returns {Boolean} True if the operation was successful, or false if not.
15128 * @see {pwlib.tools.selection.selectAll} The command implementation.
15130 this.selectAll = function (ev) {
15131 if (_self.toolActivate('selection', ev)) {
15132 return _self.tool.selectAll(ev);
15139 * Cut the available selection. This only works when the selection tool is
15140 * active and when some selection is available.
15142 * @param {Event} [ev] The DOM Event object which generated the request.
15143 * @returns {Boolean} True if the operation was successful, or false if not.
15145 * @see {pwlib.tools.selection.selectionCut} The command implementation.
15147 this.selectionCut = function (ev) {
15148 if (!_self.tool || _self.tool._id !== 'selection') {
15151 return _self.tool.selectionCut(ev);
15156 * Copy the available selection. This only works when the selection tool is
15157 * active and when some selection is available.
15159 * @param {Event} [ev] The DOM Event object which generated the request.
15160 * @returns {Boolean} True if the operation was successful, or false if not.
15162 * @see {pwlib.tools.selection.selectionCopy} The command implementation.
15164 this.selectionCopy = function (ev) {
15165 if (!_self.tool || _self.tool._id !== 'selection') {
15168 return _self.tool.selectionCopy(ev);
15173 * Paste the current clipboard image. This only works when some ImageData is
15174 * available in {@link PaintWeb#clipboard}.
15176 * @param {Event} [ev] The DOM Event object which generated the request.
15177 * @returns {Boolean} True if the operation was successful, or false if not.
15179 * @see {pwlib.tools.selection.clipboardPaste} The command implementation.
15181 this.clipboardPaste = function (ev) {
15182 if (!_self.clipboard || !_self.toolActivate('selection', ev)) {
15185 return _self.tool.clipboardPaste(ev);
15190 * The <code>configChange</code> application event handler. This method
15191 * updates the Canvas context properties depending on which configuration
15192 * property changed.
15195 * @param {pwlib.appEvent.configChange} ev The application event object.
15197 this.configChangeHandler = function (ev) {
15198 if (ev.group === 'shadow' && _self.shadowSupported && _self.shadowAllowed) {
15199 var context = _self.layer.context,
15202 // Enable/disable shadows
15203 if (ev.config === 'enable') {
15205 context.shadowColor = cfg.shadowColor;
15206 context.shadowOffsetX = cfg.shadowOffsetX;
15207 context.shadowOffsetY = cfg.shadowOffsetY;
15208 context.shadowBlur = cfg.shadowBlur;
15210 context.shadowColor = 'rgba(0,0,0,0)';
15211 context.shadowOffsetX = 0;
15212 context.shadowOffsetY = 0;
15213 context.shadowBlur = 0;
15218 // Do not update any context properties if shadows are not enabled.
15223 switch (ev.config) {
15225 case 'shadowOffsetX':
15226 case 'shadowOffsetY':
15227 ev.value = parseInt(ev.value);
15228 case 'shadowColor':
15229 context[ev.config] = ev.value;
15232 } else if (ev.group === 'line') {
15233 switch (ev.config) {
15236 ev.value = parseInt(ev.value);
15239 _self.buffer.context[ev.config] = ev.value;
15242 } else if (ev.group === 'text') {
15243 switch (ev.config) {
15245 case 'textBaseline':
15246 _self.buffer.context[ev.config] = ev.value;
15249 } else if (!ev.group) {
15250 switch (ev.config) {
15252 case 'strokeStyle':
15253 _self.buffer.context[ev.config] = ev.value;
15259 * Destroy a PaintWeb instance. This method allows you to unload a PaintWeb
15260 * instance. Extensions, tools and commands are unregistered, and the GUI
15261 * elements are removed.
15263 * <p>The scripts and styles loaded are not removed, since they might be used
15264 * by other PaintWeb instances.
15266 * <p>The {@link pwlib.appEvent.appDestroy} application event is dispatched
15267 * before the current instance is destroyed.
15269 this.destroy = function () {
15270 this.events.dispatch(new appEvent.appDestroy());
15272 for (var cmd in this.commands) {
15273 this.commandUnregister(cmd);
15276 for (var ext in this.extensions) {
15277 this.extensionUnregister(ext);
15280 for (var tool in this.gui.tools) {
15281 this.toolUnregister(tool);
15284 this.gui.destroy();
15286 this.initialized = PaintWeb.INIT_NOT_STARTED;
15289 this.toString = function () {
15290 return 'PaintWeb v' + this.version + ' (build ' + this.build + ')';
15298 * Application initialization not started.
15301 PaintWeb.INIT_NOT_STARTED = 0;
15304 * Application initialization started.
15307 PaintWeb.INIT_STARTED = 1;
15310 * Application initialization completed successfully.
15313 PaintWeb.INIT_DONE = 2;
15316 * Application initialization failed.
15319 PaintWeb.INIT_ERROR = -1;
15322 * PaintWeb base folder. This is determined automatically when the PaintWeb
15323 * script is added in a page.
15326 PaintWeb.baseFolder = '';
15329 var scripts = document.getElementsByTagName('script'),
15330 n = scripts.length,
15333 // Determine the baseFolder.
15335 for (var i = 0; i < n; i++) {
15336 src = scripts[i].src;
15337 if (!src || !/paintweb(\.dev|\.src)?\.js/.test(src)) {
15341 pos = src.lastIndexOf('/');
15343 PaintWeb.baseFolder = src.substr(0, pos + 1);
15350 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: